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
This commit is contained in:
sebastjanartic 2025-08-07 12:51:03 +00:00
parent 293b3a555e
commit 809fdf8fb1
3 changed files with 88 additions and 55 deletions

View File

@ -20,22 +20,27 @@ export default function HLSPreviewThumbnail({ video, onClick, className = "" }:
const hoverTimeoutRef = useRef<NodeJS.Timeout>(); const hoverTimeoutRef = useRef<NodeJS.Timeout>();
const previewIntervalRef = useRef<NodeJS.Timeout>(); const previewIntervalRef = useRef<NodeJS.Timeout>();
// Initialize HLS for preview // Initialize HLS for preview only when hovering
useEffect(() => { const initializeVideoOnDemand = () => {
if (!video.videoUrl.includes('.m3u8') && !video.videoUrlMp4) return; if (!video.videoUrl.includes('.m3u8') && !video.videoUrlMp4) return;
const videoElement = videoRef.current; const videoElement = videoRef.current;
if (!videoElement) return; if (!videoElement || isVideoLoaded || hlsInstance) return;
let hls: Hls | null = null; let hls: Hls | null = null;
const initializeVideo = () => {
// Use MP4 if available for better seek performance // Use MP4 if available for better seek performance
if (video.videoUrlMp4) { if (video.videoUrlMp4) {
videoElement.src = video.videoUrlMp4; videoElement.src = video.videoUrlMp4;
videoElement.muted = true; videoElement.muted = true;
videoElement.playsInline = true; videoElement.playsInline = true;
videoElement.preload = 'metadata'; videoElement.preload = 'metadata';
const handleLoadedMetadata = () => {
setIsVideoLoaded(true);
videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata);
};
videoElement.addEventListener('loadedmetadata', handleLoadedMetadata);
return; return;
} }
@ -44,8 +49,9 @@ export default function HLSPreviewThumbnail({ video, onClick, className = "" }:
hls = new Hls({ hls = new Hls({
startLevel: 0, // Start with lowest quality for faster loading startLevel: 0, // Start with lowest quality for faster loading
capLevelToPlayerSize: true, capLevelToPlayerSize: true,
maxLoadingDelay: 1, maxLoadingDelay: 2,
maxBufferLength: 5, // Minimal buffering for previews maxBufferLength: 10, // Minimal buffering for previews
lowLatencyMode: true,
}); });
hls.loadSource(video.videoUrl); hls.loadSource(video.videoUrl);
@ -64,20 +70,14 @@ export default function HLSPreviewThumbnail({ video, onClick, className = "" }:
} }
}; };
const handleLoadedMetadata = () => { // Clean up on unmount
setIsVideoLoaded(true); useEffect(() => {
};
videoElement.addEventListener('loadedmetadata', handleLoadedMetadata);
initializeVideo();
return () => { return () => {
videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata); if (hlsInstance) {
if (hls) { hlsInstance.destroy();
hls.destroy();
} }
}; };
}, [video.videoUrl, video.videoUrlMp4]); }, [hlsInstance]);
const generatePreviewThumbnail = (time: number) => { const generatePreviewThumbnail = (time: number) => {
const videoElement = videoRef.current; const videoElement = videoRef.current;
@ -131,13 +131,18 @@ export default function HLSPreviewThumbnail({ video, onClick, className = "" }:
}; };
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (!isVideoLoaded) return; if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
// Delay before showing preview to avoid flickering
hoverTimeoutRef.current = setTimeout(() => { hoverTimeoutRef.current = setTimeout(() => {
setIsHovering(true); setIsHovering(true);
// Initialize video only when user actually hovers
initializeVideoOnDemand();
if (isVideoLoaded) {
generatePreviewThumbnail(video.duration * 0.25); // Start at 25% of video generatePreviewThumbnail(video.duration * 0.25); // Start at 25% of video
}, 600); }
}, 300); // Reduced delay for better UX
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {

View File

@ -42,13 +42,42 @@ function formatDate(date: Date | string): string {
export default function VideoCard({ video, onClick }: VideoCardProps) { export default function VideoCard({ video, onClick }: VideoCardProps) {
return ( return (
<div data-testid={`card-video-${video.id}`}> <div
<HLSPreviewThumbnail data-testid={`card-video-${video.id}`}
video={video} className="transition-transform duration-200 hover:scale-[1.02]"
onClick={onClick} >
className="" {/* Simple thumbnail with fallback - no HLS loading until needed */}
<div
className="relative bg-bunny-gray rounded-xl overflow-hidden mb-4 aspect-video cursor-pointer group"
onClick={() => onClick(video)}
>
<img
src={video.thumbnailUrl}
alt={video.title}
className="w-full h-full object-cover transition-all duration-300 group-hover:scale-105"
data-testid={`img-thumbnail-${video.id}`}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
if (target.parentElement) {
target.parentElement.style.background = 'linear-gradient(45deg, #374151, #4b5563)';
}
}}
/> />
{/* Duration badge */}
<div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded">
{formatDuration(video.duration)}
</div>
{/* Play button overlay */}
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-all duration-300 flex items-center justify-center">
<div className="bg-white/20 backdrop-blur-sm rounded-full w-16 h-16 flex items-center justify-center group-hover:bg-white/30 group-hover:scale-110 transition-all duration-300">
<Play className="text-white ml-1 text-xl" />
</div>
</div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<h3 <h3
className="font-semibold line-clamp-2 hover:text-bunny-blue transition-colors text-bunny-light cursor-pointer" className="font-semibold line-clamp-2 hover:text-bunny-blue transition-colors text-bunny-light cursor-pointer"

View File

@ -44,7 +44,6 @@ export default function Home() {
// Update videos when new data comes in // Update videos when new data comes in
useEffect(() => { useEffect(() => {
if (videosResponse) { if (videosResponse) {
console.log('Videos response:', videosResponse);
if (offset === 0) { if (offset === 0) {
setAllVideos(videosResponse.videos); setAllVideos(videosResponse.videos);
} else { } else {