videofolxtv/client/src/components/video-preview-thumbnail.tsx
sebastjanartic c26d16da3d Fix issue with video preview functionality not working as expected
Introduce console logging to diagnose and fix an issue where video previews were not generating correctly, specifically when the video source was not an MP4.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 2cd2c0bc-434c-4bc9-ad3f-b99d3897a0d1
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2cd2c0bc-434c-4bc9-ad3f-b99d3897a0d1/d05DGZF
2025-09-02 15:24:40 +00:00

215 lines
7.2 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import { Play } from "lucide-react";
import { type Video } from "@shared/schema";
interface VideoPreviewThumbnailProps {
video: Video;
onClick: (video: Video) => void;
className?: string;
}
export default function VideoPreviewThumbnail({ video, onClick, className = "" }: VideoPreviewThumbnailProps) {
const [isHovering, setIsHovering] = useState(false);
const [previewTime, setPreviewTime] = useState(0);
const [previewThumbnail, setPreviewThumbnail] = useState<string | null>(null);
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<NodeJS.Timeout>();
const previewIntervalRef = useRef<NodeJS.Timeout>();
// Create hidden video element for thumbnail generation
useEffect(() => {
console.log(`Preview check for video ${video.id}:`, {
hasVideoUrlMp4: !!video.videoUrlMp4,
videoUrlMp4: video.videoUrlMp4,
videoUrlIncludesMp4: video.videoUrl.includes('.mp4'),
videoUrl: video.videoUrl
});
if (!video.videoUrlMp4 && !video.videoUrl.includes('.mp4')) {
console.log(`Preview disabled for video ${video.id} - no MP4 URL`);
return;
}
const videoElement = videoRef.current;
if (!videoElement) return;
const videoSrc = video.videoUrlMp4 || video.videoUrl;
videoElement.src = videoSrc;
videoElement.muted = true;
videoElement.playsInline = true;
videoElement.preload = 'metadata';
const handleLoadedMetadata = () => {
setIsVideoLoaded(true);
};
videoElement.addEventListener('loadedmetadata', handleLoadedMetadata);
return () => {
videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata);
};
}, [video.videoUrlMp4, video.videoUrl]);
const generatePreviewThumbnail = (time: number) => {
const videoElement = videoRef.current;
if (!videoElement || !isVideoLoaded) return;
videoElement.currentTime = time;
const handleSeeked = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.8);
setPreviewThumbnail(thumbnailUrl);
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);
}, 150);
};
const handleMouseEnter = () => {
if (!isVideoLoaded) return;
// Delay before showing preview to avoid flickering
hoverTimeoutRef.current = setTimeout(() => {
setIsHovering(true);
generatePreviewThumbnail(0);
}, 500);
};
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 (
<div className={`group cursor-pointer ${className}`}>
{/* Hidden video element for thumbnail generation */}
<video
ref={videoRef}
className="hidden"
crossOrigin="anonymous"
onError={() => setIsVideoLoaded(false)}
/>
<div
ref={containerRef}
className="relative bg-bunny-gray rounded-xl overflow-hidden mb-4 aspect-video"
onClick={() => onClick(video)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
data-testid={`preview-thumbnail-${video.id}`}
>
{/* Main thumbnail or preview */}
<img
src={isHovering && previewThumbnail ? previewThumbnail : video.thumbnailUrl}
alt={video.title}
className="w-full h-full object-cover transition-all duration-200"
data-testid={`img-preview-${video.id}`}
/>
{/* Overlay with play button */}
<div className={`absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-all duration-300 flex items-center justify-center ${
isHovering ? 'bg-black/50' : ''
}`}>
<div className={`bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center group-hover:bg-white/30 transition-all duration-300 ${
isHovering ? 'w-20 h-20 bg-white/40' : 'w-16 h-16'
}`}>
<Play className={`text-white ml-1 transition-all duration-300 ${
isHovering ? 'text-2xl' : 'text-xl'
}`} />
</div>
</div>
{/* Duration badge */}
<div className="absolute bottom-3 right-3 bg-black/80 px-2 py-1 rounded text-xs font-medium">
<span data-testid={`text-duration-${video.id}`}>
{formatDuration(video.duration)}
</span>
</div>
{/* Preview time indicator */}
{isHovering && isVideoLoaded && (
<>
{/* Progress bar */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-white/20">
<div
className="h-full bg-bunny-blue transition-all duration-150"
style={{ width: `${(previewTime / video.duration) * 100}%` }}
/>
</div>
{/* Time tooltip */}
<div
className="absolute bottom-12 bg-black/90 text-white text-xs px-2 py-1 rounded pointer-events-none transition-all duration-150"
style={{
left: `${(previewTime / video.duration) * 100}%`,
transform: 'translateX(-50%)'
}}
>
{formatTime(previewTime)}
</div>
</>
)}
{/* Loading indicator for video metadata */}
{!isVideoLoaded && (video.videoUrlMp4 || video.videoUrl.includes('.mp4')) && (
<div className="absolute top-2 left-2">
<div className="w-2 h-2 bg-bunny-blue rounded-full animate-pulse"></div>
</div>
)}
</div>
</div>
);
}