`scan *.md` only matched the repository root, so a file added under any subdirectory would have stopped being checked without the CI step noticing. Scanning recursively follows the files instead of the layout, and --respect-gitignore keeps it off .venv and .dora.
dora-webcam
Dora node that captures frames from a webcam and publishes them as
std/media/v1/Image. A source for a vision pipeline: it takes a tick and produces
frames.
Based on opencv-video-capture
from dora-hub, brought up to the dora 1.0 node contract, narrowed to Linux camera
selection, and changed to publish the typed Arrow image struct rather than a flat
buffer plus metadata.
Usage
Add to your dataflow:
nodes:
- id: camera
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
tag: v0.1.0
build: uv pip install .
path: dora-webcam
inputs:
tick: dora/timer/millis/33
outputs:
- image
Run the dataflow with --uv (dora run dataflow.yml --uv): the build command is
uv pip install ., so the node gets its own managed environment and never installs into
an ambient one.
Runnable dataflow
dataflow.yml is the camera on its own, and is what CI runs:
dora run dataflow.yml --uv
Nothing consumes the frames, so to actually see them set PREVIEW:
PREVIEW=true dora run dataflow.yml --uv
A window opens showing the frames as they are published. Press q to stop. That is
enough to check a camera, an encoding or a flip without wiring a second node — which is
why there is one dataflow here rather than one per consumer.
For a real pipeline, wire image into any node that takes std/media/v1/Image, from
your own dataflow. This repository names no consumer: it is a source, and depending on
something downstream of it only to demonstrate it would be a cycle.
Output
| Output | Payload | Metadata |
|---|---|---|
image |
std/media/v1/Image struct |
none — the struct describes itself |
The struct carries width: UInt32, height: UInt32, encoding: Utf8 and
data: LargeBinary, so a receiver needs nothing from the message metadata. For a
compressed encoding, width and height are the image's dimensions, not the length of
data.
Inputs
| Input | Description |
|---|---|
tick |
Publishes one frame. Wire to dora/timer/millis/N; the node stops when it closes. |
Configuring it
Every setting below is an environment variable, which is convenient for one and awkward for several. A dataflow can carry them instead, per node, which keeps the configuration with the wiring it belongs to and out of whatever shell happens to start it:
nodes:
- id: camera
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
tag: v0.1.0
build: uv pip install .
path: dora-webcam
env:
CAMERA_ID: usb-Acme_Webcam
IMAGE_WIDTH: 1280
IMAGE_HEIGHT: 720
ENCODING: jpeg
JPEG_QUALITY: 85
FLIP: HORIZONTAL
inputs:
tick: dora/timer/millis/33
outputs:
- image
Values may be strings, numbers or booleans, and dora applies them to both the build and the run.
A node's env: wins over the environment the dataflow was started in. A variable
the node does not set still passes through, so PREVIEW=true dora run ... works
alongside the block above — but a variable pinned there cannot be overridden from the
shell, which is worth remembering before pinning something a test needs to vary.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
CAMERA_ID |
unset | Substring of a /dev/v4l/by-id/ entry. Wins over CAPTURE_PATH. |
CAPTURE_PATH |
auto | Device path (/dev/video1) or index. A numeric string is an index. |
IMAGE_WIDTH |
native | Requested capture width; frames are also resized to it. |
IMAGE_HEIGHT |
native | Requested capture height; frames are also resized to it. |
ENCODING |
bgr8 |
One of bgr8, rgb8, yuv420, jpeg/jpg/jpe, png, bmp, webp. |
JPEG_QUALITY |
95 |
Quality 0-100, used only by the JPEG encodings. |
FLIP |
unset | VERTICAL, HORIZONTAL or BOTH. |
PREVIEW |
unset | true opens a window of the published frames; q stops the node. |
CAPTURE_FRAMES |
0 |
Stop after this many frames. 0 runs until the tick closes. |
Resizing only happens when both IMAGE_WIDTH and IMAGE_HEIGHT are set.
With neither CAMERA_ID nor CAPTURE_PATH set, the node opens the first /dev/video*
that actually delivers a frame. Merely opening is not enough to go on: a UVC camera
exposes a second node for its metadata stream — /dev/video1 beside /dev/video0 on a
one-camera machine — which opens happily and never yields an image, so the lowest device
number is wrong about as often as it is right.
CAMERA_ID is worth preferring on a robot anyway: a by-id name is stable across reboots
and across which USB port the camera is in, and neither an index nor discovery order is.
CI=true makes a failed read publish a labelled black frame instead of raising, which
is what lets the dataflow run on a machine with no camera. It is not a configuration
knob.
Build
uv sync
Every dependency ships prebuilt wheels for linux-x86_64, linux-aarch64 and macos-arm64, so the node installs with no system packages and no compiler.
Requirements: dora 1.0+ (verified with dora-cli 1.0.1), Python 3.11+, uv, and a
V4L2 camera. PREVIEW also needs a display. On a bare Linux image opencv-python needs
the libGL shared libraries (libgl1, libglib2.0-0), which it links at import time.
Test
uv run pytest
Unit tests over device resolution, encoding and the image struct; they need neither a
camera nor a dataflow. The headless dataflow.yml run covers the wiring and the event
loop.
Camera selection on other platforms
Selecting by CAMERA_ID reads /dev/v4l/by-id/, so it is Linux only. Upstream also
enumerates cameras through macOS system_profiler and Windows PowerShell; that is not
carried here. CAPTURE_PATH works everywhere.