# 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: 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`](dataflow.yml) is the camera on its own, and is what CI runs: ```bash dora run dataflow.yml --uv ``` Nothing consumes the frames, so to actually see them set `PREVIEW`: ```bash 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 a viewer such as `dora-image-view` from your own dataflow. ## 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`. | | `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. `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. `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 ```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.