// use-camera-session.ts
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CameraManager, type ManagedCamera } from "./camera-manager";
type ServerState = "starting" | "running" | "error";
export interface CameraSessionState {
serverState: ServerState;
serverError: string | null;
cameras: ManagedCamera[];
selectedCameraId: string | null;
}
export interface CameraSessionActions {
selectCamera: (cameraId: string) => void;
refreshCameras: () => void;
disconnectCamera: () => Promise<void>;
}
export function useCameraSession(): CameraSessionState & CameraSessionActions {
const [serverState, setServerState] = useState<ServerState>("starting");
const [serverError, setServerError] = useState<string | null>(null);
const [cameras, setCameras] = useState<ManagedCamera[]>([]);
const [selectedCameraId, setSelectedCameraId] = useState<string | null>(null);
const managerRef = useRef<CameraManager | null>(null);
const selectedCameraIdRef = useRef<string | null>(null);
selectedCameraIdRef.current = selectedCameraId;
const syncCameras = useCallback(() => {
const manager = managerRef.current;
if (!manager) return;
const next = manager.getCameras();
setCameras(next);
}, []);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const response = await fetch("/api/server", { method: "POST" });
const payload = await response.json();
if (cancelled) return;
if (payload.status !== "started" && payload.status !== "already_running") {
setServerState("error");
setServerError(payload.message ?? "Failed to start server");
return;
}
const manager = new CameraManager({
baseUrl: `http://localhost:${payload.port}`,
autoConnect: true,
connectionMode: "remote-transfer",
pollInterval: 5000,
autoReconnect: true,
});
managerRef.current = manager;
manager.on("camera-found", ({ camera }) => {
syncCameras();
if (!selectedCameraIdRef.current) {
setSelectedCameraId(camera.id);
}
});
manager.on("camera-lost", () => {
syncCameras();
});
manager.on("camera-ready", ({ cameraId }) => {
syncCameras();
if (!selectedCameraIdRef.current) {
setSelectedCameraId(cameraId);
}
});
manager.on("camera-disconnected", () => {
syncCameras();
});
manager.on("connection-failed", () => {
syncCameras();
});
manager.on("error", ({ message }) => {
setServerError(message);
});
await manager.start();
if (cancelled) return;
syncCameras();
setServerState("running");
} catch (error) {
if (cancelled) return;
setServerState("error");
setServerError(error instanceof Error ? error.message : String(error));
}
})();
return () => {
cancelled = true;
managerRef.current?.close();
managerRef.current = null;
};
}, [syncCameras]);
const disconnectCamera = useCallback(async () => {
const manager = managerRef.current;
const cameraId = selectedCameraIdRef.current;
if (!manager || !cameraId) return;
await manager.disconnect(cameraId);
syncCameras();
}, [syncCameras]);
return useMemo(
() => ({
serverState,
serverError,
cameras,
selectedCameraId,
selectCamera: setSelectedCameraId,
refreshCameras: syncCameras,
disconnectCamera,
}),
[serverState, serverError, cameras, selectedCameraId, syncCameras, disconnectCamera]
);
}