From 12231e43debdd59da74d02432fdfd13805f673d6 Mon Sep 17 00:00:00 2001 From: ravindu somawansa Date: Mon, 12 Oct 2026 15:10:43 +0200 Subject: [PATCH] feat(cameras): ghost overlay of a saved reference frame to realign the rig --- .../control/BrowserCameraPreview.test.tsx | 10 +++ .../control/BrowserCameraPreview.tsx | 16 ++++ .../src/components/landing/RecordingModal.tsx | 5 +- .../recording/CameraConfiguration.test.tsx | 16 ++++ .../recording/CameraConfiguration.tsx | 32 ++++++- .../recording/CameraReferenceControls.tsx | 67 ++++++++++++++ frontend/src/hooks/useCameraReference.ts | 68 ++++++++++++++ frontend/src/lib/cameraReferenceApi.test.ts | 25 ++++++ frontend/src/lib/cameraReferenceApi.ts | 57 ++++++++++++ lelab/camera_reference.py | 70 +++++++++++++++ lelab/server.py | 36 +++++++- lelab/utils/config.py | 3 + tests/test_camera_reference.py | 89 +++++++++++++++++++ 13 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/recording/CameraReferenceControls.tsx create mode 100644 frontend/src/hooks/useCameraReference.ts create mode 100644 frontend/src/lib/cameraReferenceApi.test.ts create mode 100644 frontend/src/lib/cameraReferenceApi.ts create mode 100644 lelab/camera_reference.py create mode 100644 tests/test_camera_reference.py diff --git a/frontend/src/components/control/BrowserCameraPreview.test.tsx b/frontend/src/components/control/BrowserCameraPreview.test.tsx index 818c762a7..c6bca6f70 100644 --- a/frontend/src/components/control/BrowserCameraPreview.test.tsx +++ b/frontend/src/components/control/BrowserCameraPreview.test.tsx @@ -39,3 +39,13 @@ test("a stream at another size keeps the recorded aspect ratio", () => { expect(video.style.width).toBe("100%"); expect(video.style.objectFit).toBe("cover"); }); +test("the reference ghost sits over the video with its rotation and opacity", () => { + act(() => root.render()); + const video = container.querySelector("video")!, ghost = container.querySelector("img")!; + expect(ghost.getAttribute("src")).toBe("blob:ref"); + expect(ghost.style.opacity).toBe("0.3"); + expect(ghost.style.transform).toBe(video.style.transform); + expect(ghost.style.width).toBe(video.style.width); + act(() => root.render()); + expect(container.querySelector("img")).toBeNull(); +}); diff --git a/frontend/src/components/control/BrowserCameraPreview.tsx b/frontend/src/components/control/BrowserCameraPreview.tsx index de6107d99..b2b693345 100644 --- a/frontend/src/components/control/BrowserCameraPreview.tsx +++ b/frontend/src/components/control/BrowserCameraPreview.tsx @@ -11,6 +11,9 @@ interface Props { height?: number; className?: string; fallback?: string; + /** Saved reference frame, drawn over the live video as a translucent ghost. */ + ghostSrc?: string; + ghostOpacity?: number; } /** @@ -26,6 +29,8 @@ export default function BrowserCameraPreview({ height = 480, className = "", fallback, + ghostSrc, + ghostOpacity = 0.5, }: Props) { const rotationError = cameraRotationError({ rotation }); const { videoRef, hasError } = useCameraStream(deviceId, paused || !!rotationError, { width, height }); @@ -39,6 +44,7 @@ export default function BrowserCameraPreview({ style={{ aspectRatio: geometry.aspectRatio }} > {showVideo ? ( + <>