// ============================================================ // 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 | null>(null); const prevFrameRef = useRef(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 }; }