Files
dora-webcam/README.md
T
cristhian 7628a8515b
validate / validate (push) Successful in 59m14s
discover a camera when none is configured
CAPTURE_PATH defaulted to index 0, which is a coin flip on a machine with one
camera. A UVC camera exposes a second device node for its metadata stream --
/dev/video1 beside /dev/video0 -- and that node opens happily and never yields
an image, so the lowest device number is wrong about as often as it is right.

With neither CAMERA_ID nor CAPTURE_PATH set, the node now opens each
/dev/video* in numeric order and keeps the first that actually delivers a
frame. Reading one is the test, because opening is not evidence.

Discovery finding nothing falls back to index 0 rather than raising, which
keeps the failure in one place: the first read, where CI publishes a
placeholder and a normal run raises with a message naming the device.

CAMERA_ID is still worth preferring on a robot. Neither an index nor discovery
order is stable across reboots or across which port a camera is in; a by-id
name is.
2026-09-15 06:16:14 -03:00

4.7 KiB

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.

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.