Files
CamTalk/frontend/src/hooks/useObservationMode.ts
cfy666 f5536005f6 feat: 实现观察模式,支持持续场景监控(US-05/US-10)
- 新增 useObservationMode Hook:定时 5s 采帧,similarity < 0.85 触发变化回调
- useVisionSession 新增 dialogue/observation 模式切换
- App Footer 加观察模式切换按钮(激活时蓝色脉冲)
- 视频区左上角显示'👁️ 观察中'绿色角标
- 观察模式下画面变化自动发送 query

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 14:46:42 +08:00

89 lines
2.6 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.
// ============================================================
// useObservationMode — 观察模式 Hook
// 职责:定时采帧 → 关键帧检测 → 画面变化时触发回调
// 来源docs/05-用户故事.md US-05持续场景监控
// ============================================================
import { useCallback, useEffect, useRef, useState } from "react";
import { sampleFrame, compareFrames } from "../components/EdgeProcessor";
/** 画面变化显著阈值 */
const CHANGE_THRESHOLD = 0.85;
/** 采样间隔ms */
const SAMPLE_INTERVAL = 5000;
export interface ObservationOptions {
/** 画面变化回调,携带当前帧的 DataURL */
onChange?: (frameDataUrl: string) => void;
}
export function useObservationMode(options?: ObservationOptions) {
const [isObserving, setIsObserving] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const prevFrameRef = useRef<Uint8ClampedArray | null>(null);
const optionsRef = useRef(options);
useEffect(() => {
optionsRef.current = options;
}, [options]);
/**
* 启动观察模式
* @param video 摄像头 video 元素
* @param captureFrame 从 video 捕获 DataURL 的函数
*/
const startObserving = useCallback(
(video: HTMLVideoElement | null, captureFrame: () => string | null) => {
if (!video) return;
// 立即采一帧作为基准
prevFrameRef.current = sampleFrame(video);
intervalRef.current = setInterval(() => {
const current = sampleFrame(video);
if (!current) return;
if (prevFrameRef.current) {
const { similarity } = compareFrames(prevFrameRef.current, current);
if (similarity < CHANGE_THRESHOLD) {
console.log(
`[Observation] 画面变化 (similarity=${similarity.toFixed(2)})`,
);
const frameDataUrl = captureFrame();
if (frameDataUrl) {
optionsRef.current?.onChange?.(frameDataUrl);
}
}
}
prevFrameRef.current = current;
}, SAMPLE_INTERVAL);
setIsObserving(true);
},
[],
);
/** 停止观察模式 */
const stopObserving = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
prevFrameRef.current = null;
setIsObserving(false);
}, []);
// 组件卸载时清理
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return { isObserving, startObserving, stopObserving };
}