'use client'; import { useRef, useState, useEffect, useCallback } from 'react'; import Hls from 'hls.js'; import { AnnotationCanvas, COLORS } from './AnnotationCanvas'; import { Timeline } from './Timeline'; import { AnnotationData, Comment } from '@/lib/api'; type Tool = 'pen' | 'arrow' | 'rect' | 'ellipse'; interface Props { src: string; mimeType: string; fps?: number; comments: Comment[]; // Draw mode — controlled externally by parent drawMode: boolean; drawTool: Tool; drawColor: string; onDrawModeChange: (active: boolean) => void; onDrawToolChange: (tool: Tool) => void; onDrawColorChange: (color: string) => void; // Called after each completed stroke (mouseUp) onStrokeComplete: (stroke: AnnotationData) => void; // Called when video time updates onTimeUpdate: (time: number) => void; // Called when user clicks a comment marker on timeline onCommentClick: (comment: Comment) => void; } export function VideoPlayer({ src, mimeType, fps = 30, comments, drawMode, drawTool, drawColor, onDrawModeChange, onDrawToolChange, onDrawColorChange, onStrokeComplete, onTimeUpdate, onCommentClick, }: Props) { const videoRef = useRef(null); const containerRef = useRef(null); const [playing, setPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(1); const [muted, setMuted] = useState(false); const [playbackRate, setPlaybackRate] = useState(1); const [fullscreen, setFullscreen] = useState(false); const [showControls, setShowControls] = useState(true); const [dims, setDims] = useState({ width: 0, height: 0 }); const hideTimer = useRef>(undefined); // HLS setup useEffect(() => { const video = videoRef.current; if (!video || !src) return; if (mimeType === 'application/x-mpegURL' || src.endsWith('.m3u8')) { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(src); hls.attachMedia(video); return () => hls.destroy(); } } else { video.src = src; } }, [src, mimeType]); // Measure container useEffect(() => { const obs = new ResizeObserver(entries => { for (const entry of entries) { setDims({ width: entry.contentRect.width, height: entry.contentRect.height }); } }); if (containerRef.current) obs.observe(containerRef.current); return () => obs.disconnect(); }, []); // Keyboard shortcuts useEffect(() => { const handleKey = (e: KeyboardEvent) => { const video = videoRef.current; if (!video || e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.code === 'Space') { e.preventDefault(); video.paused ? video.play() : video.pause(); } if (e.code === 'ArrowLeft') { e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 5); } if (e.code === 'ArrowRight') { e.preventDefault(); video.currentTime = Math.min(duration, video.currentTime + 5); } if (e.code === 'KeyC') { e.preventDefault(); // Entering draw mode → auto-pause; exiting → no auto-play if (!drawMode) { video.pause(); onDrawModeChange(true); } else { onDrawModeChange(false); } } if (e.code === 'KeyF') { e.preventDefault(); toggleFullscreen(); } if (e.code === 'KeyM') { e.preventDefault(); video.muted = !video.muted; } if (e.code === 'KeyU') { e.preventDefault(); stepFrame(-1); } if (e.code === 'KeyI') { e.preventDefault(); stepFrame(1); } if (e.code === 'Escape' && drawMode) { e.preventDefault(); onDrawModeChange(false); } }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [duration, drawMode, onDrawModeChange]); // Auto-hide controls const resetHideTimer = useCallback(() => { setShowControls(true); clearTimeout(hideTimer.current); if (playing) { hideTimer.current = setTimeout(() => setShowControls(false), 3000); } }, [playing]); function stepFrame(dir: 1 | -1) { const video = videoRef.current; if (!video) return; const frameTime = 1 / (fps || 30); video.pause(); video.currentTime = Math.max(0, Math.min(duration, video.currentTime + dir * frameTime)); } function toggleFullscreen() { if (!containerRef.current) return; if (!document.fullscreenElement) { containerRef.current.requestFullscreen(); setFullscreen(true); } else { document.exitFullscreen(); setFullscreen(false); } } function togglePlay() { const video = videoRef.current; if (!video) return; video.paused ? video.play() : video.pause(); } const handleVolume = (v: number) => { const video = videoRef.current; if (!video) return; video.volume = v; setVolume(v); setMuted(v === 0); }; const handleSpeed = (rate: number) => { const video = videoRef.current; if (!video) return; video.playbackRate = rate; setPlaybackRate(rate); }; const handleSeek = (time: number) => { const video = videoRef.current; if (!video) return; video.currentTime = time; setCurrentTime(time); onTimeUpdate(time); }; return (
playing && setShowControls(false)} > {/* Video */}