| import { useCallback, useEffect, useState } from "react"; |
| import { useApi } from "@/contexts/ApiContext"; |
|
|
| export interface AvailableCamera { |
| index: number; |
| name: string; |
| deviceId: string; |
| available: boolean; |
| } |
|
|
| const norm = (s: string) => s.toLowerCase().replace(/\s+/g, " ").trim(); |
|
|
| interface UseAvailableCamerasOptions { |
| |
| enabled?: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function useAvailableCameras({ |
| enabled = true, |
| }: UseAvailableCamerasOptions = {}) { |
| const { baseUrl, fetchWithHeaders } = useApi(); |
| const [cameras, setCameras] = useState<AvailableCamera[]>([]); |
| const [isLoading, setIsLoading] = useState(false); |
|
|
| const refresh = useCallback(async (): Promise<AvailableCamera[]> => { |
| setIsLoading(true); |
| try { |
| |
| try { |
| const probe = await navigator.mediaDevices.getUserMedia({ video: true }); |
| probe.getTracks().forEach((t) => t.stop()); |
| } catch { |
| |
| } |
|
|
| const browserDevices = (await navigator.mediaDevices.enumerateDevices()) |
| .filter((d) => d.kind === "videoinput") |
| .map((d) => ({ deviceId: d.deviceId, label: d.label })); |
|
|
| const r = await fetchWithHeaders(`${baseUrl}/available-cameras`); |
| if (!r.ok) { |
| setCameras([]); |
| return []; |
| } |
| const data = await r.json(); |
| const backendCams: { |
| index: number; |
| name?: string; |
| available: boolean; |
| }[] = data.cameras ?? []; |
|
|
| |
| |
| |
| const used = new Set<string>(); |
| const merged: AvailableCamera[] = backendCams.map((cam) => { |
| const label = cam.name || `Camera ${cam.index}`; |
| const target = norm(label); |
| const candidates = browserDevices.filter( |
| (d) => !used.has(d.deviceId) && d.label |
| ); |
| const match = |
| candidates.find((d) => norm(d.label) === target) || |
| candidates.find((d) => norm(d.label).startsWith(target)) || |
| candidates.find( |
| (d) => norm(d.label).includes(target) || target.includes(norm(d.label)) |
| ); |
| if (match) used.add(match.deviceId); |
| return { |
| index: cam.index, |
| name: label, |
| deviceId: match?.deviceId ?? "", |
| available: cam.available, |
| }; |
| }); |
| setCameras(merged); |
| return merged; |
| } catch { |
| setCameras([]); |
| return []; |
| } finally { |
| setIsLoading(false); |
| } |
| }, [baseUrl, fetchWithHeaders]); |
|
|
| useEffect(() => { |
| if (!enabled) return; |
| refresh(); |
| const handler = () => refresh(); |
| navigator.mediaDevices.addEventListener("devicechange", handler); |
| return () => |
| navigator.mediaDevices.removeEventListener("devicechange", handler); |
| }, [enabled, refresh]); |
|
|
| return { cameras, isLoading, refresh }; |
| } |
|
|