"""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, or after CAPTURE_FRAMES frames if that is set. """ import argparse import os import cv2 from dora import Node from dora_webcam import camera, encode, image 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", "0")) 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 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)) # 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() 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()