import { useState, useRef, useEffect } from "react"; import { Play } from "lucide-react"; import { type Video } from "@shared/schema"; import Hls from "hls.js"; interface HLSPreviewThumbnailProps { video: Video; onClick: (video: Video) => void; className?: string; } export default function HLSPreviewThumbnail({ video, onClick, className = "" }: HLSPreviewThumbnailProps) { const [isHovering, setIsHovering] = useState(false); const [previewTime, setPreviewTime] = useState(0); const [previewThumbnail, setPreviewThumbnail] = useState(null); const [isVideoLoaded, setIsVideoLoaded] = useState(false); const [hlsInstance, setHlsInstance] = useState(null); const videoRef = useRef(null); const containerRef = useRef(null); const hoverTimeoutRef = useRef(); const previewIntervalRef = useRef(); // Initialize HLS for preview only when hovering const initializeVideoOnDemand = () => { if (!video.videoUrl.includes('.m3u8') && !video.videoUrlMp4) return; const videoElement = videoRef.current; if (!videoElement || isVideoLoaded || hlsInstance) return; let hls: Hls | null = null; // Use MP4 if available for better seek performance if (video.videoUrlMp4) { videoElement.src = video.videoUrlMp4; videoElement.muted = true; videoElement.playsInline = true; videoElement.preload = 'metadata'; const handleLoadedMetadata = () => { setIsVideoLoaded(true); videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata); }; videoElement.addEventListener('loadedmetadata', handleLoadedMetadata); return; } // Use HLS with reduced quality for preview if (Hls.isSupported() && video.videoUrl.includes('.m3u8')) { hls = new Hls({ startLevel: 0, // Start with lowest quality for faster loading capLevelToPlayerSize: true, maxLoadingDelay: 2, maxBufferLength: 10, // Minimal buffering for previews lowLatencyMode: true, }); hls.loadSource(video.videoUrl); hls.attachMedia(videoElement); hls.on(Hls.Events.MANIFEST_PARSED, () => { setIsVideoLoaded(true); }); hls.on(Hls.Events.ERROR, (event, data) => { console.warn('HLS preview error:', data); setIsVideoLoaded(false); }); setHlsInstance(hls); } }; // Clean up on unmount useEffect(() => { return () => { if (hlsInstance) { hlsInstance.destroy(); } }; }, [hlsInstance]); const generatePreviewThumbnail = (time: number) => { const videoElement = videoRef.current; if (!videoElement || !isVideoLoaded) return; // Clamp time to video duration const clampedTime = Math.max(0, Math.min(time, video.duration - 1)); videoElement.currentTime = clampedTime; const handleSeeked = () => { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return; canvas.width = videoElement.videoWidth || 320; canvas.height = videoElement.videoHeight || 180; ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.7); setPreviewThumbnail(thumbnailUrl); } catch (error) { console.warn('Failed to generate preview thumbnail:', error); } videoElement.removeEventListener('seeked', handleSeeked); }; videoElement.addEventListener('seeked', handleSeeked); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isHovering || !containerRef.current || !isVideoLoaded) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = Math.max(0, Math.min(1, x / rect.width)); const time = percentage * video.duration; setPreviewTime(time); // Throttle thumbnail generation if (previewIntervalRef.current) { clearTimeout(previewIntervalRef.current); } previewIntervalRef.current = setTimeout(() => { generatePreviewThumbnail(time); }, 200); }; const handleMouseEnter = () => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); } hoverTimeoutRef.current = setTimeout(() => { setIsHovering(true); // Initialize video only when user actually hovers initializeVideoOnDemand(); if (isVideoLoaded) { generatePreviewThumbnail(video.duration * 0.25); // Start at 25% of video } }, 300); // Reduced delay for better UX }; const handleMouseLeave = () => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); } if (previewIntervalRef.current) { clearTimeout(previewIntervalRef.current); } setIsHovering(false); setPreviewThumbnail(null); setPreviewTime(0); }; const formatTime = (seconds: number): string => { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; }; const formatDuration = (seconds: number): string => { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; }; return (
{/* Hidden video element for thumbnail generation */}