// ============================================================ // CameraManager — 摄像头流采集 // 职责:获取用户摄像头 MediaStream,提供给 VideoPreview 和 EdgeProcessor // ============================================================ import { useCallback, useRef, useState } from "react"; import { useI18n } from "../../lib/i18n"; export interface CameraManagerHandle { /** 获取当前视频轨道 */ stream: MediaStream | null; /** 捕获当前帧为 JPEG DataURL */ captureFrame: () => string | null; } export function useCamera() { const { t } = useI18n(); const videoRef = useRef(null); const [stream, setStream] = useState(null); const [error, setError] = useState(null); const startCamera = useCallback(async (deviceId?: string) => { try { const videoConstraints: MediaTrackConstraints = deviceId ? { deviceId: { exact: deviceId }, width: 640, height: 480 } : { facingMode: "environment", width: 640, height: 480 }; const mediaStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: false, }); setStream(mediaStream); if (videoRef.current) { videoRef.current.srcObject = mediaStream; } setError(null); } catch (err) { const message = err instanceof Error ? err.message : t("error.cameraAccess"); setError(message); console.error("[Camera] 获取摄像头失败:", err); } }, []); const stopCamera = useCallback(() => { stream?.getTracks().forEach((track) => track.stop()); setStream(null); }, [stream]); /** 从 video 元素捕获当前帧为 JPEG DataURL */ const captureFrame = useCallback((): string | null => { const video = videoRef.current; if (!video || video.readyState < 2) return null; const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) return null; ctx.drawImage(video, 0, 0); return canvas.toDataURL("image/jpeg", 0.7); }, []); return { videoRef, stream, error, startCamera, stopCamera, captureFrame }; }