import React, { useState, useEffect, useRef } from 'react'; function FocusPageLocal({ videoManager, sessionResult, setSessionResult, isActive }) { const [currentFrame, setCurrentFrame] = useState(15); const [timelineEvents, setTimelineEvents] = useState([]); const [stats, setStats] = useState(null); const localVideoRef = useRef(null); const displayCanvasRef = useRef(null); const pipVideoRef = useRef(null); // 用于 PiP 的隐藏 video 元素 const pipStreamRef = useRef(null); // 辅助函数:格式化时间 const formatDuration = (seconds) => { if (seconds === 0) return "0s"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}m ${secs}s`; }; useEffect(() => { if (!videoManager) return; // 设置回调函数来更新时间轴 const originalOnStatusUpdate = videoManager.callbacks.onStatusUpdate; videoManager.callbacks.onStatusUpdate = (isFocused) => { setTimelineEvents(prev => { const newEvents = [...prev, { isFocused, timestamp: Date.now() }]; if (newEvents.length > 60) newEvents.shift(); return newEvents; }); if (originalOnStatusUpdate) originalOnStatusUpdate(isFocused); }; // 定期更新统计信息 const statsInterval = setInterval(() => { if (videoManager && videoManager.getStats) { setStats(videoManager.getStats()); } }, 1000); return () => { if (videoManager) { videoManager.callbacks.onStatusUpdate = originalOnStatusUpdate; } clearInterval(statsInterval); }; }, [videoManager]); const handleStart = async () => { try { if (videoManager) { setSessionResult(null); setTimelineEvents([]); console.log('Initializing local camera...'); await videoManager.initCamera(localVideoRef.current, displayCanvasRef.current); console.log('Camera initialized'); console.log('Starting local streaming...'); await videoManager.startStreaming(); console.log('Streaming started successfully'); } } catch (err) { console.error('Start error:', err); let errorMessage = "Failed to start: "; if (err.name === 'NotAllowedError') { errorMessage += "Camera permission denied. Please allow camera access."; } else if (err.name === 'NotFoundError') { errorMessage += "No camera found. Please connect a camera."; } else if (err.name === 'NotReadableError') { errorMessage += "Camera is already in use by another application."; } else { errorMessage += err.message || "Unknown error occurred."; } alert(errorMessage + "\n\nCheck browser console for details."); } }; const handleStop = async () => { if (videoManager) { videoManager.stopStreaming(); } try { if (document.pictureInPictureElement === pipVideoRef.current) { await document.exitPictureInPicture(); } } catch (_) {} if (pipVideoRef.current) { pipVideoRef.current.pause(); pipVideoRef.current.srcObject = null; } if (pipStreamRef.current) { pipStreamRef.current.getTracks().forEach(t => t.stop()); pipStreamRef.current = null; } }; const handlePiP = async () => { try { // 检查是否有视频管理器和是否在运行 if (!videoManager || !videoManager.isStreaming) { alert('Please start the video first.'); return; } if (!displayCanvasRef.current) { alert('Video not ready.'); return; } // 如果已经在 PiP 模式,且是本视频,退出 if (document.pictureInPictureElement === pipVideoRef.current) { await document.exitPictureInPicture(); console.log('PiP exited'); return; } // 检查浏览器支持 if (!document.pictureInPictureEnabled) { alert('Picture-in-Picture is not supported in this browser.'); return; } // 创建或获取 PiP video 元素 const pipVideo = pipVideoRef.current; if (!pipVideo) { alert('PiP video element not ready.'); return; } const isSafariPiP = typeof pipVideo.webkitSetPresentationMode === 'function'; // 优先用画布流(带检测框),失败再回退到摄像头流 let stream = pipStreamRef.current; if (!stream) { const capture = displayCanvasRef.current.captureStream; if (typeof capture === 'function') { stream = capture.call(displayCanvasRef.current, 30); } if (!stream || stream.getTracks().length === 0) { const cameraStream = localVideoRef.current?.srcObject; if (!cameraStream) { alert('Camera stream not ready.'); return; } stream = cameraStream; } pipStreamRef.current = stream; } // 确保流有轨道 if (!stream || stream.getTracks().length === 0) { alert('Failed to capture video stream from canvas.'); return; } pipVideo.srcObject = stream; // 播放视频(Safari 可能不会触发 onloadedmetadata) if (pipVideo.readyState < 2) { await new Promise((resolve) => { const onReady = () => { pipVideo.removeEventListener('loadeddata', onReady); pipVideo.removeEventListener('canplay', onReady); resolve(); }; pipVideo.addEventListener('loadeddata', onReady); pipVideo.addEventListener('canplay', onReady); // 兜底:短延迟后继续尝试 setTimeout(resolve, 600); }); } try { await pipVideo.play(); } catch (_) { // Safari 可能拒绝自动播放,但仍可进入 PiP } // Safari 支持(优先) if (isSafariPiP) { try { pipVideo.webkitSetPresentationMode('picture-in-picture'); console.log('PiP activated (Safari)'); return; } catch (e) { // 如果画布流失败,回退到摄像头流再试一次 const cameraStream = localVideoRef.current?.srcObject; if (cameraStream && cameraStream !== pipVideo.srcObject) { pipVideo.srcObject = cameraStream; try { await pipVideo.play(); } catch (_) {} pipVideo.webkitSetPresentationMode('picture-in-picture'); console.log('PiP activated (Safari fallback)'); return; } throw e; } } // 标准 API if (typeof pipVideo.requestPictureInPicture === 'function') { await pipVideo.requestPictureInPicture(); console.log('PiP activated'); } else { alert('Picture-in-Picture is not supported in this browser.'); } } catch (err) { console.error('PiP error:', err); alert('Failed to enter Picture-in-Picture: ' + err.message); } }; const handleFloatingWindow = () => { handlePiP(); }; const handleFrameChange = (val) => { const rate = parseInt(val); setCurrentFrame(rate); if (videoManager) { videoManager.setFrameRate(rate); } }; const handlePreview = () => { if (!videoManager || !videoManager.isStreaming) { alert('Please start a session first.'); return; } // 获取当前统计数据 const currentStats = videoManager.getStats(); if (!currentStats.sessionId) { alert('No active session.'); return; } // 计算当前持续时间(从 session 开始到现在) const sessionDuration = Math.floor((Date.now() - (videoManager.sessionStartTime || Date.now())) / 1000); // 计算当前专注分数 const focusScore = currentStats.framesProcessed > 0 ? (currentStats.framesProcessed * (currentStats.currentStatus ? 1 : 0)) / currentStats.framesProcessed : 0; // 显示当前实时数据 setSessionResult({ duration_seconds: sessionDuration, focus_score: focusScore, total_frames: currentStats.framesProcessed, focused_frames: Math.floor(currentStats.framesProcessed * focusScore) }); }; const handleCloseOverlay = () => { setSessionResult(null); }; const pageStyle = isActive ? undefined : { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden', opacity: 0, pointerEvents: 'none' }; useEffect(() => { return () => { if (pipVideoRef.current) { pipVideoRef.current.pause(); pipVideoRef.current.srcObject = null; } if (pipStreamRef.current) { pipStreamRef.current.getTracks().forEach(t => t.stop()); pipStreamRef.current = null; } }; }, []); return (
{/* 1. Camera / Display Area */}
{/* 用于 PiP 的隐藏 video 元素(保持在 DOM 以提高兼容性) */}
{/* 2. Timeline Area */}
Timeline
{timelineEvents.map((event, index) => (
))}
{/* 3. Control Buttons */}
{/* 4. Frame Control */}
handleFrameChange(e.target.value)} /> handleFrameChange(e.target.value)} />
); } export default FocusPageLocal;