From 809fdf8fb10665f0ab54c2237e837d957f156588 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Thu, 7 Aug 2025 12:51:03 +0000 Subject: [PATCH] Improve video preview loading and display on the platform Refactor HLS preview thumbnail to load videos on hover, introduce MP4 fallback, and optimize HLS settings. Update VideoCard to display static thumbnails until hover, preventing unnecessary initial video loading. Replit-Commit-Author: Agent Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/QjWGafC --- .../src/components/hls-preview-thumbnail.tsx | 101 +++++++++--------- client/src/components/video-card.tsx | 41 +++++-- client/src/pages/home.tsx | 1 - 3 files changed, 88 insertions(+), 55 deletions(-) diff --git a/client/src/components/hls-preview-thumbnail.tsx b/client/src/components/hls-preview-thumbnail.tsx index 6798471..9d946e7 100644 --- a/client/src/components/hls-preview-thumbnail.tsx +++ b/client/src/components/hls-preview-thumbnail.tsx @@ -20,64 +20,64 @@ export default function HLSPreviewThumbnail({ video, onClick, className = "" }: const hoverTimeoutRef = useRef(); const previewIntervalRef = useRef(); - // Initialize HLS for preview - useEffect(() => { + // Initialize HLS for preview only when hovering + const initializeVideoOnDemand = () => { if (!video.videoUrl.includes('.m3u8') && !video.videoUrlMp4) return; const videoElement = videoRef.current; - if (!videoElement) return; + if (!videoElement || isVideoLoaded || hlsInstance) return; let hls: Hls | null = null; - const initializeVideo = () => { - // Use MP4 if available for better seek performance - if (video.videoUrlMp4) { - videoElement.src = video.videoUrlMp4; - videoElement.muted = true; - videoElement.playsInline = true; - videoElement.preload = 'metadata'; - return; - } + // 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: 1, - maxBufferLength: 5, // Minimal buffering for previews - }); + // 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.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); - }); + hls.on(Hls.Events.ERROR, (event, data) => { + console.warn('HLS preview error:', data); + setIsVideoLoaded(false); + }); - setHlsInstance(hls); - } - }; - - const handleLoadedMetadata = () => { - setIsVideoLoaded(true); - }; - - videoElement.addEventListener('loadedmetadata', handleLoadedMetadata); - initializeVideo(); + setHlsInstance(hls); + } + }; + // Clean up on unmount + useEffect(() => { return () => { - videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata); - if (hls) { - hls.destroy(); + if (hlsInstance) { + hlsInstance.destroy(); } }; - }, [video.videoUrl, video.videoUrlMp4]); + }, [hlsInstance]); const generatePreviewThumbnail = (time: number) => { const videoElement = videoRef.current; @@ -131,13 +131,18 @@ export default function HLSPreviewThumbnail({ video, onClick, className = "" }: }; const handleMouseEnter = () => { - if (!isVideoLoaded) return; + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } - // Delay before showing preview to avoid flickering hoverTimeoutRef.current = setTimeout(() => { setIsHovering(true); - generatePreviewThumbnail(video.duration * 0.25); // Start at 25% of video - }, 600); + // 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 = () => { diff --git a/client/src/components/video-card.tsx b/client/src/components/video-card.tsx index 7be769e..b0e6306 100644 --- a/client/src/components/video-card.tsx +++ b/client/src/components/video-card.tsx @@ -42,12 +42,41 @@ function formatDate(date: Date | string): string { export default function VideoCard({ video, onClick }: VideoCardProps) { return ( -
- +
+ {/* Simple thumbnail with fallback - no HLS loading until needed */} +
onClick(video)} + > + {video.title} { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + if (target.parentElement) { + target.parentElement.style.background = 'linear-gradient(45deg, #374151, #4b5563)'; + } + }} + /> + + {/* Duration badge */} +
+ {formatDuration(video.duration)} +
+ + {/* Play button overlay */} +
+
+ +
+
+

{ if (videosResponse) { - console.log('Videos response:', videosResponse); if (offset === 0) { setAllVideos(videosResponse.videos); } else {