Skip to content

GUIDES

8. Camera streaming

Live snapshot and live video from a USB or CSI camera, following the same opt-in, explicitly-allowlisted posture as 4. Industrial buses's CAN capture. This is the device-discovery and live-viewing half of the "Edge AI bridge" line item in ROADMAP.md — local inference and hardware-accelerator support are a separate, still-unscoped future increment, not covered here.

1. Build with the camera feature

Off by default, like hotplug/tpm2:

cargo build --release --features camera

v4l (pure-Rust V4L2 ioctls) and jpeg-encoder (pure-Rust JPEG encoder, used only for cameras that don't offer native MJPG) compile in only with this flag — a plain cargo build is unaffected. V4L2 is Linux-only; on any other platform the capture code compiles to a no-op stub, same as hardware::hotplug.

2. Declare a camera

No camera is ever opened unless explicitly listed — same rule as CAN capture's interface allowlist:

[camera]
devices = []

[[camera.devices]]
id = "front-dock"
path = "/dev/video0"
enabled = true
max_frames_per_second = 10
jpeg_quality = 75
max_stream_clients = 4
publish_to_nodra = true

Restart the daemon after changing this — camera capture threads are opened once at startup, same as CAN capture's interfaces.

3. Take a live snapshot

curl -sS -o snapshot.jpg http://127.0.0.1:9188/api/v1/camera/front-dock/snapshot

Returns the single most recently captured frame as image/jpeg. 404 if front-dock isn't a configured camera id; 503 if it's configured but hasn't captured a frame yet (not yet enabled, still starting, or the capture thread is currently erroring — check GET /api/v1/camera for last_error).

4. View the live stream

curl -sS http://127.0.0.1:9188/api/v1/camera/front-dock/stream -o /dev/null

Or, simplest of all, point a browser or a plain <img> tag straight at it:

<img src="http://127.0.0.1:9188/api/v1/camera/front-dock/stream">

This is multipart/x-mixed-replace — the same shape a classic IP camera serves — so it renders as a live-updating image with no WebRTC/signaling setup. Capture runs exactly once regardless of how many viewers connect; max_stream_clients (0 = unlimited) caps concurrent viewers with a 429 past the limit.

If auth.mode = "bearer" is set (see 3. Securing your agent), an <img src> can't set an Authorization header — append the token as a query parameter instead, same fallback the two SSE routes already use:

<img src="https://<host>:9188/api/v1/camera/front-dock/stream?ticket=<STREAM_TICKET>">

Mint the ticket with `POST /api/v1/stream-tickets` under bearer or mTLS auth.
Never put the long-lived bearer on the query string.

5. Check status

curl -sS http://127.0.0.1:9188/api/v1/camera | jq .

Returns, per configured camera: capturing, frames_total, dropped_total (rate-limit drops), encode_errors_total, subscribers (current live viewers), and last_error.

6. Nodra hand-off (optional)

publish_to_nodra = true publishes only health/presence — capturing state and the counters above — to a retained <topic-prefix>/<serial>/camera/<id>/status topic. Raw frame bytes never reach Nodra; this is deliberate, matching the product boundary described in the main README — Device Agent reports capabilities, Nodra owns higher-level semantics.

Next steps