Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions frontend/src/components/recording/CameraConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { NumberInput } from "@/components/ui/number-input";
import { Camera, Plus, X, VideoOff } from "lucide-react";
import { Camera, Plus, X, VideoOff, RefreshCw } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useAvailableCameras } from "@/hooks/useAvailableCameras";
import { useCameraStream } from "@/hooks/useCameraStream";
Expand Down Expand Up @@ -42,6 +42,7 @@ const CameraConfiguration: React.FC<CameraConfigurationProps> = ({
const {
cameras: availableCameras,
isLoading: isLoadingCameras,
refresh: refreshCameras,
} = useAvailableCameras();
const [selectedCameraIndex, setSelectedCameraIndex] = useState<string>("");
const [cameraName, setCameraName] = useState("");
Expand Down Expand Up @@ -176,9 +177,25 @@ const CameraConfiguration: React.FC<CameraConfigurationProps> = ({

<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-300">
Available Cameras
</Label>
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-300">
Available Cameras
</Label>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => refreshCameras()}
disabled={isLoadingCameras}
className="h-6 w-6 text-gray-400 hover:text-white"
title="Rescan for cameras (e.g. after plugging in a new USB camera)"
aria-label="Rescan for cameras"
>
<RefreshCw
className={`w-3.5 h-3.5 ${isLoadingCameras ? "animate-spin" : ""}`}
/>
</Button>
</div>
<Select
value={selectedCameraIndex}
onValueChange={setSelectedCameraIndex}
Expand Down
58 changes: 53 additions & 5 deletions frontend/src/pages/Calibration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import {
ArrowLeft,
Settings,
Expand All @@ -25,6 +26,8 @@ import {
Play,
Square,
Circle,
Camera,
ShieldQuestion,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import Logo from "@/components/Logo";
Expand Down Expand Up @@ -85,6 +88,10 @@ const Calibration = () => {
const [port, setPort] = useState<string>("");
const [robot, setRobot] = useState<RobotRecord | null>(null);
const [cameras, setCameras] = useState<CameraConfig[]>([]);
// Off by default so merely opening the calibration page never grabs a camera.
// The user explicitly starts a scan, which is when cameras are turned on,
// enumerated, and the browser permission prompt is requested.
const [camerasActive, setCamerasActive] = useState(false);
const cameraSaveTimerRef = useRef<NodeJS.Timeout | null>(null);

const fetchRobot = useCallback(async (): Promise<RobotRecord | null> => {
Expand Down Expand Up @@ -869,17 +876,58 @@ const Calibration = () => {

{robotName && (
<Card className="bg-slate-800/60 border-slate-700 backdrop-blur-sm mt-6">
<CardHeader>
<CardHeader className="flex-row items-center justify-between space-y-0">
<CardTitle className="flex items-center gap-2 text-slate-200">
<Settings className="w-5 h-5 text-blue-400" />
Attached cameras
</CardTitle>
<div className="flex items-center gap-2">
<Label
htmlFor="cameras-toggle"
className="text-sm text-slate-400 cursor-pointer"
>
{camerasActive ? "On" : "Off"}
</Label>
<Switch
id="cameras-toggle"
checked={camerasActive}
onCheckedChange={setCamerasActive}
className="data-[state=checked]:bg-green-500"
aria-label="Turn cameras on or off"
/>
</div>
</CardHeader>
<CardContent>
<CameraConfiguration
cameras={cameras}
onCamerasChange={handleCamerasChange}
/>
{camerasActive ? (
<CameraConfiguration
cameras={cameras}
onCamerasChange={handleCamerasChange}
/>
) : (
<div className="rounded-lg border border-slate-700 bg-slate-900/40 p-6 text-center space-y-3">
<Camera className="w-10 h-10 mx-auto text-slate-500" />
<div className="space-y-1">
<p className="text-slate-200 font-medium">Cameras are off</p>
<p className="text-sm text-slate-400 max-w-md mx-auto">
Turn cameras on to scan for connected devices and preview
them. The browser may briefly open a camera to read device
labels, and configured cameras stay active while previews
are visible; your browser will ask for camera permission.
Nothing is recorded.
</p>
{cameras.length > 0 && (
<p className="text-xs text-slate-500 pt-1">
{cameras.length} camera
{cameras.length === 1 ? "" : "s"} saved to this robot.
</p>
)}
</div>
<p className="flex items-center justify-center gap-1.5 text-xs text-slate-500">
<ShieldQuestion className="w-3.5 h-3.5" />
You'll be asked to grant camera access.
</p>
</div>
)}
</CardContent>
</Card>
)}
Expand Down
Loading