import { Button } from "@/components/ui/button"; import { RotateCcw } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; type Props = { /** Selects Nth `videoinput` from the browser (may differ from OpenCV index on the server). */ cameraIndex: number; }; /** * Live browser webcam with 2D pan / zoom / rotate — for framing checks only. * Server-side `automation_face` uses OpenCV indices; align devices in OS settings if needed. */ export function CameraPreview({ cameraIndex }: Props) { const videoRef = useRef(null); const [err, setErr] = useState(""); const [streamLabel, setStreamLabel] = useState(""); const [scale, setScale] = useState(1); const [pan, setPan] = useState({ x: 0, y: 0 }); const [rotateDeg, setRotateDeg] = useState(0); const drag = useRef<{ active: boolean; startX: number; startY: number; ox: number; oy: number; }>({ active: false, startX: 0, startY: 0, ox: 0, oy: 0, }); useEffect(() => { let stream: MediaStream | null = null; let cancelled = false; const videoEl = videoRef.current; async function run() { setErr(""); try { const perm = await navigator.mediaDevices.getUserMedia({ video: true }); perm.getTracks().forEach((t) => t.stop()); const devices = await navigator.mediaDevices.enumerateDevices(); const videos = devices.filter((d) => d.kind === "videoinput"); if (videos.length === 0) { setErr("No video input devices"); return; } const idx = Math.min(Math.max(0, cameraIndex), videos.length - 1); const deviceId = videos[idx].deviceId; setStreamLabel(videos[idx].label || `Camera ${idx}`); const videoConstraint: MediaTrackConstraints = deviceId ? { deviceId: { exact: deviceId } } : { width: { ideal: 1280 }, height: { ideal: 720 } }; stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraint, audio: false, }); if (cancelled || !videoRef.current) { stream.getTracks().forEach((t) => t.stop()); return; } videoRef.current.srcObject = stream; await videoRef.current.play(); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } } void run(); return () => { cancelled = true; if (stream) { stream.getTracks().forEach((t) => t.stop()); } if (videoEl) { videoEl.srcObject = null; } }; }, [cameraIndex]); const onWheel = useCallback((e: React.WheelEvent) => { e.preventDefault(); setScale((s) => Math.min(3, Math.max(0.5, s - e.deltaY * 0.001))); }, []); const onPointerDown = (e: React.PointerEvent) => { (e.target as HTMLElement).setPointerCapture(e.pointerId); drag.current = { active: true, startX: e.clientX, startY: e.clientY, ox: pan.x, oy: pan.y, }; }; const onPointerMove = (e: React.PointerEvent) => { if (!drag.current.active) return; setPan({ x: drag.current.ox + (e.clientX - drag.current.startX), y: drag.current.oy + (e.clientY - drag.current.startY), }); }; const onPointerUp = (e: React.PointerEvent) => { drag.current.active = false; try { (e.target as HTMLElement).releasePointerCapture(e.pointerId); } catch { /* ignore */ } }; const reset = () => { setScale(1); setPan({ x: 0, y: 0 }); setRotateDeg(0); }; return (
{streamLabel || "—"}

2D preview in your browser — pan (drag), zoom (wheel), rotate (slider). This is not a 3D scene graph of the Windows desktop; use MCP tools + screenshots for real UI work. Physical robots live in your fleet{" "} robotics-mcp, not here.

); }