import { useRef, useState, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react' import { FaPlay, FaPause, FaStop, FaStepBackward, FaStepForward, FaVolumeMute, FaVolumeUp } from 'react-icons/fa' import type { Media } from '../../types' interface Props { media: Media onTimeUpdate: (time: number) => void children?: React.ReactNode } const STEP_BTN_CLASS = 'w-9 h-8 flex items-center justify-center bg-az-bg rounded hover:bg-az-border text-az-text text-xs font-mono' const ICON_BTN_CLASS = 'w-10 h-10 flex items-center justify-center bg-az-bg rounded hover:bg-az-border text-white' export interface VideoPlayerHandle { seek: (seconds: number) => void getVideoElement: () => HTMLVideoElement | null } const VideoPlayer = forwardRef(function VideoPlayer({ media, onTimeUpdate, children }, ref) { const videoRef = useRef(null) useImperativeHandle(ref, () => ({ seek(seconds: number) { if (videoRef.current) { videoRef.current.currentTime = seconds setCurrentTime(seconds) } }, getVideoElement() { return videoRef.current }, })) const [error, setError] = useState(null) const [playing, setPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) const [muted, setMuted] = useState(false) const videoUrl = media.path.startsWith('blob:') ? media.path : `/api/annotations/media/${media.id}/file` const stepFrames = useCallback((count: number) => { const video = videoRef.current if (!video) return const fps = 30 video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + count / fps)) }, []) const togglePlay = useCallback(() => { const v = videoRef.current if (!v) return if (v.paused) { v.play(); setPlaying(true) } else { v.pause(); setPlaying(false) } }, []) const stop = useCallback(() => { const v = videoRef.current if (!v) return v.pause() v.currentTime = 0 setPlaying(false) }, []) useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return switch (e.key) { case ' ': e.preventDefault(); togglePlay(); break case 'ArrowLeft': e.preventDefault(); stepFrames(e.ctrlKey ? -150 : -1); break case 'ArrowRight': e.preventDefault(); stepFrames(e.ctrlKey ? 150 : 1); break case 'm': case 'M': setMuted(m => !m); break } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [togglePlay, stepFrames]) const formatTime = (s: number) => { const m = Math.floor(s / 60) const sec = Math.floor(s % 60) return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}` } return (
{error &&
{error}
}
{/* Progress row: time | slider | remaining */}
{formatTime(currentTime)} { const v = Number(e.target.value) setCurrentTime(v) if (videoRef.current) videoRef.current.currentTime = v }} className="flex-1 accent-az-orange h-1 cursor-pointer" style={{ background: `linear-gradient(to right, #fd7e14 0%, #fd7e14 ${(currentTime / (duration || 1)) * 100}%, #495057 ${(currentTime / (duration || 1)) * 100}%, #495057 100%)`, }} /> -{formatTime(Math.max(0, duration - currentTime))}
{/* Buttons row */}
{[1, 5, 10, 30, 60].map(n => ( ))} {[1, 5, 10, 30, 60].map(n => ( ))}
) }) export default VideoPlayer