Fix live camera analysis loop

这个提交包含在:
cryptocommuniums-afk
2026-03-14 22:54:15 +08:00
父节点 bc01a40564
当前提交 6943754838
修改 3 个文件,包含 96 行新增11 行删除

查看文件

@@ -33,6 +33,7 @@ export default function LiveCamera() {
const streamRef = useRef<MediaStream | null>(null);
const poseRef = useRef<any>(null);
const animFrameRef = useRef<number>(0);
const analyzingRef = useRef(false);
const [cameraActive, setCameraActive] = useState(false);
const [facing, setFacing] = useState<CameraFacing>("environment");
@@ -108,6 +109,7 @@ export default function LiveCamera() {
}, [facing]);
const stopCamera = useCallback(() => {
analyzingRef.current = false;
if (streamRef.current) {
streamRef.current.getTracks().forEach(t => t.stop());
streamRef.current = null;
@@ -115,9 +117,12 @@ export default function LiveCamera() {
if (animFrameRef.current) {
cancelAnimationFrame(animFrameRef.current);
}
poseRef.current?.close?.();
poseRef.current = null;
setCameraActive(false);
setAnalyzing(false);
setLiveScore(null);
setFeedback([]);
}, []);
const switchCamera = useCallback(() => {
@@ -131,17 +136,29 @@ export default function LiveCamera() {
// Start pose analysis
const startAnalysis = useCallback(async () => {
if (!videoRef.current || !canvasRef.current) return;
if (!videoRef.current || !canvasRef.current || !cameraActive) {
toast.error("请先启动摄像头");
return;
}
if (analyzingRef.current) return;
analyzingRef.current = true;
setAnalyzing(true);
toast.info("正在加载姿势识别模型...");
try {
const { Pose } = await import("@mediapipe/pose");
const { drawConnectors, drawLandmarks } = await import("@mediapipe/drawing_utils");
const testFactory = (
window as typeof window & {
__TEST_MEDIAPIPE_FACTORY__?: () => Promise<{ Pose: any }>;
}
).__TEST_MEDIAPIPE_FACTORY__;
const { Pose } = testFactory
? await testFactory()
: await import("@mediapipe/pose");
const allowSyntheticFrames = Boolean(testFactory);
const pose = new Pose({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`,
locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`,
});
pose.setOptions({
@@ -216,8 +233,8 @@ export default function LiveCamera() {
poseRef.current = pose;
const processFrame = async () => {
if (!videoRef.current || !analyzing) return;
if (videoRef.current.readyState >= 2) {
if (!videoRef.current || !analyzingRef.current) return;
if (allowSyntheticFrames || videoRef.current.readyState >= 2) {
await pose.send({ image: videoRef.current });
}
animFrameRef.current = requestAnimationFrame(processFrame);
@@ -228,16 +245,21 @@ export default function LiveCamera() {
} catch (err) {
console.error("Pose init error:", err);
toast.error("姿势识别模型加载失败");
analyzingRef.current = false;
setAnalyzing(false);
}
}, [analyzing, exerciseType]);
}, [cameraActive, exerciseType]);
const stopAnalysis = useCallback(() => {
analyzingRef.current = false;
if (animFrameRef.current) {
cancelAnimationFrame(animFrameRef.current);
}
poseRef.current?.close?.();
poseRef.current = null;
setAnalyzing(false);
setLiveScore(null);
setFeedback([]);
}, []);
// Cleanup on unmount
@@ -328,14 +350,14 @@ export default function LiveCamera() {
<div className="relative bg-black aspect-video w-full">
<video
ref={videoRef}
className={`absolute inset-0 w-full h-full object-contain ${analyzing ? "opacity-0" : ""}`}
className="absolute inset-0 w-full h-full object-contain"
playsInline
muted
autoPlay
/>
<canvas
ref={canvasRef}
className={`absolute inset-0 w-full h-full object-contain ${analyzing ? "" : "hidden"}`}
className={`pointer-events-none absolute inset-0 w-full h-full object-contain ${analyzing ? "" : "hidden"}`}
/>
{!cameraActive && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-white/60">
@@ -372,7 +394,7 @@ export default function LiveCamera() {
</Button>
)}
{!analyzing ? (
<Button size="sm" onClick={startAnalysis} className="gap-1.5">
<Button data-testid="live-camera-analyze-button" size="sm" onClick={startAnalysis} className="gap-1.5">
<Zap className="h-3.5 w-3.5" />
</Button>
) : (
@@ -402,7 +424,7 @@ export default function LiveCamera() {
{liveScore ? (
<>
<div className="text-center">
<p className="text-4xl font-bold text-primary">{liveScore.overall}</p>
<p data-testid="live-camera-score-overall" className="text-4xl font-bold text-primary">{liveScore.overall}</p>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="space-y-2">