import React, { useState, useEffect, useRef } from 'react'; import { ArrowLeft, FastForward, Keyboard, Maximize, Minimize, Pause, Play, Rewind, SkipBack, SkipForward, Volume2, VolumeX, X } from 'lucide-react'; import { formatTime } from '../lib/utils'; interface VideoPlayerProps { url: string; title?: string; poster?: string; startTime?: number; onClose?: () => void; onProgressUpdate?: (currentTime: number, duration: number) => void; onVideoEnded?: () => void; showNextButton?: boolean; contentRating?: { rating: string, description: string } | null; hideTitleInPlayer?: boolean; showControls?: boolean; containerRef?: React.RefObject; videoRef?: React.RefObject; customOverlay?: React.ReactNode; } const VideoPlayer: React.FC = ({ url, title, poster, startTime = 0, onClose, onProgressUpdate, onVideoEnded, showNextButton = false, contentRating, hideTitleInPlayer = false, showControls: initialShowControls = true, containerRef, videoRef: externalVideoRef, customOverlay }) => { const internalVideoRef = useRef(null); const videoRef = externalVideoRef || internalVideoRef; const [isPlaying, setIsPlaying] = useState(false); const [volume, setVolume] = useState(1); const [isMuted, setIsMuted] = useState(false); const [progress, setProgress] = useState(startTime); const [duration, setDuration] = useState(0); const [showControls, setShowControls] = useState(initialShowControls); const [isFullscreen, setIsFullscreen] = useState(false); const [buffered, setBuffered] = useState(0); const [showRating, setShowRating] = useState(true); const [hoverTime, setHoverTime] = useState(null); const [hoverPosition, setHoverPosition] = useState<{ x: number, y: number } | null>(null); const [showKeyboardControls, setShowKeyboardControls] = useState(false); const controlsTimerRef = useRef(null); const playerContainerRef = useRef(null); const progressBarRef = useRef(null); const ratingTimerRef = useRef(null); // Format time manually (in case utils import fails) const formatTimeBackup = (time: number): string => { const hours = Math.floor(time / 3600); const minutes = Math.floor((time % 3600) / 60); const seconds = Math.floor(time % 60); const minutesStr = minutes.toString().padStart(2, '0'); const secondsStr = seconds.toString().padStart(2, '0'); return hours > 0 ? `${hours}:${minutesStr}:${secondsStr}` : `${minutesStr}:${secondsStr}`; }; // Hide content rating after a few seconds useEffect(() => { if (showRating && contentRating) { ratingTimerRef.current = setTimeout(() => { setShowRating(false); }, 8000); } return () => { if (ratingTimerRef.current) { clearTimeout(ratingTimerRef.current); } }; }, [showRating, contentRating]); useEffect(() => { const videoElement = videoRef.current; if (videoElement) { const handleLoadedMetadata = () => { setDuration(videoElement.duration); videoElement.currentTime = startTime; setProgress(startTime); }; const handleTimeUpdate = () => { setProgress(videoElement.currentTime); onProgressUpdate?.(videoElement.currentTime, videoElement.duration); }; const handleEnded = () => { setIsPlaying(false); onVideoEnded?.(); }; const handleBufferUpdate = () => { if (videoElement.buffered.length > 0) { setBuffered(videoElement.buffered.end(videoElement.buffered.length - 1)); } }; videoElement.addEventListener('loadedmetadata', handleLoadedMetadata); videoElement.addEventListener('timeupdate', handleTimeUpdate); videoElement.addEventListener('ended', handleEnded); videoElement.addEventListener('progress', handleBufferUpdate); return () => { videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata); videoElement.removeEventListener('timeupdate', handleTimeUpdate); videoElement.removeEventListener('ended', handleEnded); videoElement.removeEventListener('progress', handleBufferUpdate); }; } }, [url, startTime, onProgressUpdate, onVideoEnded, videoRef]); useEffect(() => { if (isPlaying) { videoRef.current?.play(); } else { videoRef.current?.pause(); } }, [isPlaying, videoRef]); useEffect(() => { if (videoRef.current) { videoRef.current.volume = isMuted ? 0 : volume; } }, [volume, isMuted, videoRef]); const hideControlsTimer = () => { if (controlsTimerRef.current) { clearTimeout(controlsTimerRef.current); } controlsTimerRef.current = setTimeout(() => { if (isPlaying && !showKeyboardControls) { setShowControls(false); } }, 3000); }; const handleMouseMove = () => { setShowControls(true); hideControlsTimer(); }; useEffect(() => { hideControlsTimer(); return () => { if (controlsTimerRef.current) { clearTimeout(controlsTimerRef.current); } }; }, [isPlaying, showKeyboardControls]); // Keyboard controls useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case ' ': case 'k': e.preventDefault(); setIsPlaying(prev => !prev); setShowControls(true); break; case 'ArrowRight': e.preventDefault(); skipForward(); setShowControls(true); break; case 'ArrowLeft': e.preventDefault(); skipBackward(); setShowControls(true); break; case 'f': e.preventDefault(); toggleFullscreen(); break; case 'm': e.preventDefault(); setIsMuted(prev => !prev); setShowControls(true); break; case '?': e.preventDefault(); setShowKeyboardControls(prev => !prev); setShowControls(true); break; case 'Escape': if (showKeyboardControls) { setShowKeyboardControls(false); } else if (isFullscreen) { document.exitFullscreen(); } else if (onClose) { onClose(); } break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isFullscreen, onClose, showKeyboardControls]); // Fullscreen handlers useEffect(() => { const handleFullScreenChange = () => { setIsFullscreen(document.fullscreenElement === (containerRef?.current || playerContainerRef.current)); }; document.addEventListener('fullscreenchange', handleFullScreenChange); return () => document.removeEventListener('fullscreenchange', handleFullScreenChange); }, [containerRef]); const toggleFullscreen = async () => { const fullscreenElement = containerRef?.current || playerContainerRef.current; if (!fullscreenElement) return; if (!isFullscreen) { await fullscreenElement.requestFullscreen(); } else { await document.exitFullscreen(); } }; // Player control handlers const handlePlayPause = () => { setIsPlaying(!isPlaying); }; const handleMute = () => { setIsMuted(!isMuted); }; const handleVolumeChange = (e: React.ChangeEvent) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); if (newVolume === 0) { setIsMuted(true); } else if (isMuted) { setIsMuted(false); } }; const handleProgressChange = (e: React.ChangeEvent) => { const newTime = parseFloat(e.target.value); setProgress(newTime); if (videoRef.current) { videoRef.current.currentTime = newTime; } }; // Direct progress bar click handler const handleProgressBarClick = (e: React.MouseEvent) => { if (!progressBarRef.current || !duration) return; const rect = progressBarRef.current.getBoundingClientRect(); const clickPosition = (e.clientX - rect.left) / rect.width; const newTime = duration * clickPosition; if (videoRef.current) { videoRef.current.currentTime = newTime; setProgress(newTime); } }; // Progress bar hover handler for time preview const handleProgressBarHover = (e: React.MouseEvent) => { if (!progressBarRef.current || !duration) return; const rect = progressBarRef.current.getBoundingClientRect(); const hoverPosition = (e.clientX - rect.left) / rect.width; const hoverTimeValue = duration * hoverPosition; setHoverTime(hoverTimeValue); setHoverPosition({ x: e.clientX, y: rect.top }); }; const handleProgressBarLeave = () => { setHoverTime(null); setHoverPosition(null); }; // Use the imported formatTime function with a fallback const formatTimeDisplay = formatTime || formatTimeBackup; const skipForward = () => { if (videoRef.current) { videoRef.current.currentTime = Math.min( videoRef.current.duration, videoRef.current.currentTime + 10 ); } }; const skipBackward = () => { if (videoRef.current) { videoRef.current.currentTime = Math.max( 0, videoRef.current.currentTime - 10 ); } }; const toggleKeyboardControls = () => { setShowKeyboardControls(prev => !prev); setShowControls(true); }; return (
{/* Content rating overlay - only shown briefly */} {contentRating && showRating && (
{contentRating.rating}
|
{contentRating.description}
)}
); }; export default VideoPlayer;