Files
CamTalk/frontend/src/components/CameraManager/index.tsx
cfy666 6967dd7b2e feat: 实现摄像头和麦克风设备选择功能
- 新增 useDeviceList hook,枚举音视频输入设备并监听热插拔
- CameraManager/MicManager 的 startCamera/startMic 支持可选 deviceId 参数
- useVisionSession 集成设备选择:授权后自动枚举、切换设备时热重启
- 连接后显示设备下拉选择器,未连接时隐藏(避免未授权时空列表)
- SessionConfig 新增 cameraDeviceId/micDeviceId 持久化到 localStorage
2026-06-21 16:49:48 +08:00

65 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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<HTMLVideoElement>(null);
const [stream, setStream] = useState<MediaStream | null>(null);
const [error, setError] = useState<string | null>(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 };
}