# 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`](https://github.com/dora-rs/dora-hub/tree/main/node-hub/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: ```yaml nodes: - id: camera git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git tag: v0.1.0 build: pip install . path: dora-webcam inputs: tick: dora/timer/millis/33 outputs: - image ``` Run the dataflow with `--uv` (`dora run dataflow.yml --uv`) so the node gets its own managed environment. ### Runnable dataflows Two dataflows live here, run one at a time — dora has no cross-dataflow wiring, so each declares the camera itself: Run either with `dora run --uv`: | File | Contents | |---|---| | [`dataflow.yml`](dataflow.yml) | The camera alone | | [`dataflow_view.yml`](dataflow_view.yml) | The camera into `dora-image-view` | `dataflow_view.yml` is the end-to-end check of the pair: it wires this node's `image` output into the viewer's `image` input, so a window opens showing the webcam. It fetches the viewer over the network, which is why CI validates and runs `dataflow.yml` only. ## 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` | `0` | 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`. | | `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. `CAMERA_ID` is worth preferring on a robot: a by-id name is stable across reboots and across which USB port the camera is in, and an index is not. `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 ```bash 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. On a bare Linux image `opencv-python` also needs the libGL shared libraries (`libgl1`, `libglib2.0-0`), which it links at import time. ## Test ```bash 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.