feat: 初始化前端项目,搭建 React + TypeScript + Vite 工程结构
- 创建 Vite + React 18 + TypeScript 项目(严格模式) - 安装 @ricky0123/vad-web、onnxruntime-web、uuid 依赖 - 定义全部 WebSocket 消息类型和数据模型(对齐接口文档) - 实现 WebSocket 连接管理(心跳保活、指数退避重连) - 实现音频编码工具(PCM↔Base64、DataURL 转换) - 创建组件骨架:CameraManager、MicManager、EdgeProcessor、WebSocketManager、ChatPanel、VideoPreview - 实现核心 useVisionSession Hook 骨架 - 配置 ESLint(_前缀变量忽略规则) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
59
frontend/src/components/CameraManager/index.tsx
Normal file
59
frontend/src/components/CameraManager/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// ============================================================
|
||||
// CameraManager — 摄像头流采集
|
||||
// 职责:获取用户摄像头 MediaStream,提供给 VideoPreview 和 EdgeProcessor
|
||||
// ============================================================
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
export interface CameraManagerHandle {
|
||||
/** 获取当前视频轨道 */
|
||||
stream: MediaStream | null;
|
||||
/** 捕获当前帧为 JPEG DataURL */
|
||||
captureFrame: () => string | null;
|
||||
}
|
||||
|
||||
export function useCamera() {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [stream, setStream] = useState<MediaStream | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const startCamera = useCallback(async () => {
|
||||
try {
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment", width: 640, height: 480 },
|
||||
audio: false,
|
||||
});
|
||||
setStream(mediaStream);
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = mediaStream;
|
||||
}
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "无法访问摄像头";
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user