Files
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

124 lines
3.7 KiB
Python

"""A dora webcam source: captures frames and publishes them as std/media/v1/Image.
Each `tick` reads one frame, optionally flips and resizes it, encodes it, and
sends it. The node stops when the tick input closes, after CAPTURE_FRAMES
frames if that is set, or when `q` is pressed in the preview window.
"""
import argparse
import os
import cv2
from dora import Node
from dora_webcam import camera, encode, image, preview
FLIPS = {
"VERTICAL": 0,
"HORIZONTAL": 1,
"BOTH": -1,
}
def main() -> None:
"""Open the camera and publish one frame per tick."""
args = _parse_args()
device = camera.resolve(os.getenv("CAMERA_ID"), os.getenv("CAPTURE_PATH"))
capture = cv2.VideoCapture(device)
width = _int("IMAGE_WIDTH")
height = _int("IMAGE_HEIGHT")
# Ask the camera for the size rather than only resizing after the fact: a
# camera that can deliver it natively avoids a scale per frame.
if width and height:
capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
encoding = os.getenv("ENCODING", "bgr8").lower()
quality = _int("JPEG_QUALITY") or 95
flip = FLIPS.get(os.getenv("FLIP", "").upper())
ci = os.getenv("CI") == "true"
# 0 runs until the tick closes; a positive count is what lets CI run the
# dataflow to completion instead of killing it on a timeout.
limit = _int("CAPTURE_FRAMES") or 0
# Never under CI: there is no display there, and imshow would fail.
previewing = os.getenv("PREVIEW", "").lower() in ("1", "true") and not ci
node = Node(args.name)
sent = 0
try:
for event in node:
if event["type"] == "ERROR":
raise RuntimeError(event["error"])
if event["type"] == "INPUT_CLOSED" and event.get("id") == "tick":
break
if event["type"] != "INPUT" or event["id"] != "tick":
continue
frame = camera.read(capture, device, ci)
if flip is not None:
frame = cv2.flip(frame, flip)
if width and height and (frame.shape[1], frame.shape[0]) != (width, height):
frame = cv2.resize(frame, (width, height))
# After the flip and the resize, so what is on screen is what is
# published rather than what came off the sensor.
if previewing and preview.show(frame):
break
# Taken before encoding: a compressed payload's byte count is not
# its pixel dimensions.
frame_height, frame_width = frame.shape[:2]
node.send_output(
"image",
image.pack(
encode.encode(frame, encoding, quality),
encoding,
frame_width,
frame_height,
),
{},
)
sent += 1
if limit and sent >= limit:
break
finally:
capture.release()
if previewing:
preview.close()
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Dora webcam source: publishes captured frames as std/media/v1/Image.",
)
parser.add_argument(
"--name",
type=str,
default="dora-webcam",
# Passing the name lets the node also run standalone as a dynamic node;
# when dora spawns it, the id from the dataflow wins.
help="The name of the node in the dataflow.",
)
return parser.parse_args()
def _int(name: str) -> int | None:
"""Read an integer environment variable, treating unset and empty as absent."""
value = os.getenv(name)
return int(value) if value else None
if __name__ == "__main__":
main()