import React, { useState, useRef, useEffect, useCallback } from 'react' export function VideoThumbnailCard({ videoUrl, thumbnailUrl }: { videoUrl: string; thumbnailUrl?: string }) { const containerRef = useRef(null) const videoRef = useRef(null) const canvasRef = useRef(null) const [isHovering, setIsHovering] = useState(false) const [videoReady, setVideoReady] = useState(false) const [scrubProgress, setScrubProgress] = useState(0) const [scrubTime, setScrubTime] = useState('') const rafRef = useRef(0) const drawFrame = useCallback(() => { const video = videoRef.current const canvas = canvasRef.current if (!video || !canvas || video.readyState < 2) return const ctx = canvas.getContext('2d') if (!ctx) return const rect = canvas.getBoundingClientRect() if (canvas.width !== rect.width || canvas.height !== rect.height) { canvas.width = rect.width canvas.height = rect.height } ctx.drawImage(video, 0, 0, canvas.width, canvas.height) }, []) const handleMouseMove = useCallback((e: React.MouseEvent) => { const video = videoRef.current const container = containerRef.current if (!video || !container || !video.duration || isNaN(video.duration)) return const rect = container.getBoundingClientRect() const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)) const pct = x / rect.width const targetTime = pct * video.duration video.currentTime = targetTime setScrubProgress(pct) const mins = Math.floor(targetTime / 60) const secs = Math.floor(targetTime % 60) const frames = Math.floor((targetTime % 1) * 24) setScrubTime(`${mins}:${secs.toString().padStart(2, '0')}:${frames.toString().padStart(2, '0')}`) cancelAnimationFrame(rafRef.current) rafRef.current = requestAnimationFrame(drawFrame) }, [drawFrame]) const handleMouseEnter = useCallback(() => { setIsHovering(true) }, []) const handleMouseLeave = useCallback(() => { setIsHovering(false) setVideoReady(false) setScrubProgress(0) setScrubTime('') }, []) useEffect(() => { if (!isHovering) return const video = document.createElement('video') video.muted = true video.playsInline = true video.preload = 'auto' video.src = videoUrl videoRef.current = video const onLoaded = () => { setVideoReady(true) requestAnimationFrame(drawFrame) } const onSeeked = () => requestAnimationFrame(drawFrame) video.addEventListener('loadeddata', onLoaded) video.addEventListener('seeked', onSeeked) video.load() return () => { video.removeEventListener('loadeddata', onLoaded) video.removeEventListener('seeked', onSeeked) video.pause() video.removeAttribute('src') video.load() if (videoRef.current === video) { videoRef.current = null } } }, [isHovering, videoUrl, drawFrame]) return (
{thumbnailUrl ? ( ) : (
)} {isHovering && (
)} {isHovering && videoReady && scrubTime && (
{scrubTime}
)}
) }