2026-06-12 17:46:25 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// CameraManager — 摄像头流采集
|
|
|
|
|
|
// 职责:获取用户摄像头 MediaStream,提供给 VideoPreview 和 EdgeProcessor
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
import { useCallback, useRef, useState } from "react";
|
2026-06-14 14:34:30 +08:00
|
|
|
|
import { useI18n } from "../../lib/i18n";
|
2026-06-12 17:46:25 +08:00
|
|
|
|
|
|
|
|
|
|
export interface CameraManagerHandle {
|
|
|
|
|
|
/** 获取当前视频轨道 */
|
|
|
|
|
|
stream: MediaStream | null;
|
|
|
|
|
|
/** 捕获当前帧为 JPEG DataURL */
|
|
|
|
|
|
captureFrame: () => string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useCamera() {
|
2026-06-14 14:34:30 +08:00
|
|
|
|
const { t } = useI18n();
|
2026-06-12 17:46:25 +08:00
|
|
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
|
|
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
2026-06-21 16:49:48 +08:00
|
|
|
|
const startCamera = useCallback(async (deviceId?: string) => {
|
2026-06-12 17:46:25 +08:00
|
|
|
|
try {
|
2026-06-21 16:49:48 +08:00
|
|
|
|
const videoConstraints: MediaTrackConstraints = deviceId
|
|
|
|
|
|
? { deviceId: { exact: deviceId }, width: 640, height: 480 }
|
|
|
|
|
|
: { facingMode: "environment", width: 640, height: 480 };
|
2026-06-12 17:46:25 +08:00
|
|
|
|
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
2026-06-21 16:49:48 +08:00
|
|
|
|
video: videoConstraints,
|
2026-06-12 17:46:25 +08:00
|
|
|
|
audio: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
setStream(mediaStream);
|
|
|
|
|
|
if (videoRef.current) {
|
|
|
|
|
|
videoRef.current.srcObject = mediaStream;
|
|
|
|
|
|
}
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
} catch (err) {
|
2026-06-14 14:34:30 +08:00
|
|
|
|
const message = err instanceof Error ? err.message : t("error.cameraAccess");
|
2026-06-12 17:46:25 +08:00
|
|
|
|
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 };
|
|
|
|
|
|
}
|