'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; comments: Comment[]; pendingAnnotation: AnnotationData | null; onAnnotationCreated: (annotation: AnnotationData) => void; onTimeUpdate: (time: number) => void; onCommentClick: (comment: Comment) => void; } export function VideoPlayer({ src, mimeType, comments, pendingAnnotation, onAnnotationCreated, 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 [drawMode, setDrawMode] = useState(false); const [tool, setTool] = useState('pen'); const [color, setColor] = useState(COLORS[0].value); 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(); setDrawMode(v => !v); } 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); } }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [duration]); // 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; // Approximate frame step: 1/fps (assume 30fps) const frameTime = 1 / 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); } } const 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); }; // Annotations visible at current time (±0.5s) const visibleAnnotations = comments .filter(c => c.annotation && c.timestamp != null && Math.abs(c.timestamp - currentTime) < 0.5) .map(c => c.annotation as AnnotationData); return (
playing && setShowControls(false)} > {/* Video */}