Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState, useRef } from 'react'; | |
| import { getMovieLinkByTitle, getMovieCard } from '../lib/api'; | |
| import { useToast } from '@/hooks/use-toast'; | |
| import VideoPlayer from './VideoPlayer'; | |
| import VideoPlayerControls from './VideoPlayerControls'; | |
| import { Loader2, Play } from 'lucide-react'; | |
| interface ProgressData { | |
| status: string; | |
| progress: number; | |
| downloaded: number; | |
| total: number; | |
| } | |
| interface MoviePlayerProps { | |
| movieTitle: string; | |
| videoUrl?: string; | |
| contentRatings?: any[]; | |
| thumbnail?: string; | |
| poster?: string; | |
| startTime?: number; | |
| onClosePlayer?: () => void; | |
| onProgressUpdate?: (currentTime: number, duration: number) => void; | |
| onVideoEnded?: () => void; | |
| showNextButton?: boolean; | |
| } | |
| const MoviePlayer: React.FC<MoviePlayerProps> = ({ | |
| movieTitle, | |
| videoUrl, | |
| contentRatings, | |
| thumbnail, | |
| poster, | |
| startTime = 0, | |
| onClosePlayer, | |
| onProgressUpdate, | |
| onVideoEnded, | |
| showNextButton = false | |
| }) => { | |
| const [videoUrlState, setVideoUrlState] = useState<string | null>(videoUrl || null); | |
| const [loading, setLoading] = useState(!videoUrl); | |
| const [error, setError] = useState<string | null>(null); | |
| const [progress, setProgress] = useState<ProgressData | null>(null); | |
| const [videoFetched, setVideoFetched] = useState(!!videoUrl); | |
| const { toast } = useToast(); | |
| const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null); | |
| const timeoutRef = useRef<NodeJS.Timeout | null>(null); | |
| const videoFetchedRef = useRef(!!videoUrl); | |
| const [ratingInfo, setRatingInfo] = useState<{ rating: string, description: string } | null>(null); | |
| const [currentTime, setCurrentTime] = useState(startTime); | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| const videoRef = useRef<HTMLVideoElement>(null); | |
| // Update the onProgressUpdate handler to also update currentTime | |
| const handleProgressUpdate = (time: number, duration: number) => { | |
| setCurrentTime(time); | |
| onProgressUpdate?.(time, duration); | |
| }; | |
| // Handler for seeking from WatchTogether | |
| const handleSeek = (time: number) => { | |
| if (videoRef.current) { | |
| videoRef.current.currentTime = time; | |
| setCurrentTime(time); | |
| } | |
| }; | |
| // --- Link Fetching & Polling --- | |
| const fetchMovieLink = async () => { | |
| if (videoFetchedRef.current || videoUrlState) return; | |
| try { | |
| const response = await getMovieLinkByTitle(movieTitle); | |
| if (response && response.url) { | |
| // Stop any polling if running | |
| if (pollingIntervalRef.current) { | |
| clearInterval(pollingIntervalRef.current); | |
| pollingIntervalRef.current = null; | |
| } | |
| setVideoUrlState(response.url); | |
| setVideoFetched(true); | |
| videoFetchedRef.current = true; | |
| setLoading(false); // Ensure loading is set to false when URL is fetched | |
| console.log('Video URL fetched:', response.url); | |
| } else if (response && response.progress_url) { | |
| startPolling(response.progress_url); | |
| } else { | |
| console.error('No video URL or progress URL found in response:', response); | |
| setError('Video URL not available'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching movie link:', error); | |
| setError('Failed to load video'); | |
| toast({ | |
| title: "Error", | |
| description: "Could not load the video", | |
| variant: "destructive" | |
| }); | |
| } finally { | |
| // Only set loading to false if we don't have a video yet | |
| if (!videoFetchedRef.current && !videoUrlState) { | |
| setLoading(false); | |
| } | |
| } | |
| }; | |
| // Fetch content ratings if not provided | |
| useEffect(() => { | |
| const fetchRatingInfo = async () => { | |
| if (contentRatings && contentRatings.length > 0) { | |
| const usRating = contentRatings.find(r => r.country === 'usa') || contentRatings[0]; | |
| setRatingInfo({ | |
| rating: usRating.name || 'NR', | |
| description: usRating.description || '' | |
| }); | |
| return; | |
| } | |
| try { | |
| const movieData = await getMovieCard(movieTitle); | |
| if (movieData && movieData.content_ratings) { | |
| const ratings = movieData.content_ratings; | |
| const usRating = ratings.find((r: any) => r.country === 'US') || ratings[0]; | |
| setRatingInfo({ | |
| rating: usRating?.name || 'NR', | |
| description: usRating?.description || '' | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Failed to fetch movie ratings:', error); | |
| } | |
| }; | |
| fetchRatingInfo(); | |
| }, [movieTitle, contentRatings]); | |
| const pollProgress = async (progressUrl: string) => { | |
| try { | |
| const res = await fetch(progressUrl); | |
| const data = await res.json(); | |
| setProgress(data.progress); | |
| if (data.progress.progress >= 100) { | |
| if (pollingIntervalRef.current) { | |
| clearInterval(pollingIntervalRef.current); | |
| pollingIntervalRef.current = null; | |
| } | |
| if (!videoFetchedRef.current) { | |
| timeoutRef.current = setTimeout(fetchMovieLink, 5000); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error polling progress:', error); | |
| } | |
| }; | |
| const startPolling = (progressUrl: string) => { | |
| if (!pollingIntervalRef.current) { | |
| const interval = setInterval(() => pollProgress(progressUrl), 2000); | |
| pollingIntervalRef.current = interval; | |
| } | |
| }; | |
| // Cleanup on unmount and when dependencies change | |
| useEffect(() => { | |
| if (!videoUrlState) { | |
| fetchMovieLink(); | |
| } else { | |
| setVideoFetched(true); | |
| videoFetchedRef.current = true; | |
| setLoading(false); // Make sure loading is false when we have a URL | |
| } | |
| return () => { | |
| if (pollingIntervalRef.current) clearInterval(pollingIntervalRef.current); | |
| if (timeoutRef.current) clearTimeout(timeoutRef.current); | |
| }; | |
| }, [movieTitle, videoUrl]); | |
| // Add effect to update loading state when videoUrlState changes | |
| useEffect(() => { | |
| if (videoUrlState) { | |
| setLoading(false); | |
| } | |
| }, [videoUrlState]); | |
| if (error) { | |
| return ( | |
| <div className="fixed inset-0 z-50 bg-black flex flex-col items-center justify-center"> | |
| <div className="text-4xl mb-4 text-theme-error">😢</div> | |
| <h2 className="text-2xl font-bold mb-2 text-white">Error Playing Movie</h2> | |
| <p className="text-gray-400 mb-6">{error}</p> | |
| <button | |
| onClick={onClosePlayer} | |
| className="px-6 py-2 bg-theme-primary hover:bg-theme-primary-hover rounded font-medium transition-colors text-white" | |
| > | |
| Back to Browse | |
| </button> | |
| </div> | |
| ); | |
| } | |
| if (loading || !videoFetched || !videoUrlState) { | |
| return ( | |
| <div className="fixed inset-0 z-50 bg-gradient-to-br from-theme-background-dark to-black flex flex-col items-center justify-center"> | |
| <div className="text-center max-w-md px-6"> | |
| <div className="mb-6 flex justify-center"> | |
| {poster ? ( | |
| <img src={poster} alt={movieTitle} className="h-auto w-24 rounded-lg shadow-lg" /> | |
| ) : ( | |
| <div className="flex items-center justify-center h-24 w-24 bg-theme-primary/20 rounded-lg"> | |
| <Play className="h-12 w-12 text-theme-primary" /> | |
| </div> | |
| )} | |
| </div> | |
| <h2 className="text-2xl md:text-3xl font-bold text-white mb-4"> | |
| {progress && progress.progress < 100 | |
| ? `Preparing "${movieTitle}"` | |
| : `Loading "${movieTitle}"` | |
| } | |
| </h2> | |
| {progress ? ( | |
| <> | |
| <p className="text-gray-300 mb-4"> | |
| {progress.progress < 5 | |
| ? 'Initializing your stream...' | |
| : progress.progress < 100 | |
| ? 'Your stream is being prepared.' | |
| : 'Almost ready! Starting playback soon...'} | |
| </p> | |
| <div className="relative w-full h-2 bg-gray-800 rounded-full overflow-hidden mb-2"> | |
| <div | |
| className="absolute top-0 left-0 h-full bg-gradient-to-r from-theme-primary to-theme-primary-light transition-all duration-300" | |
| style={{ width: `${Math.min(100, Math.max(0, progress.progress))}%` }} | |
| /> | |
| </div> | |
| <p className="text-sm text-gray-400"> | |
| {Math.round(progress.progress)}% complete | |
| </p> | |
| </> | |
| ) : ( | |
| <div className="flex justify-center"> | |
| <Loader2 className="h-8 w-8 animate-spin text-theme-primary" /> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div ref={containerRef} className="fixed inset-0 h-screen w-screen overflow-hidden"> | |
| <VideoPlayer | |
| url={videoUrlState} | |
| title={movieTitle} | |
| poster={poster || thumbnail} | |
| startTime={startTime} | |
| onClose={onClosePlayer} | |
| onProgressUpdate={handleProgressUpdate} | |
| onVideoEnded={onVideoEnded} | |
| showNextButton={showNextButton} | |
| contentRating={ratingInfo} | |
| containerRef={containerRef} | |
| videoRef={videoRef} | |
| /> | |
| <VideoPlayerControls | |
| title={movieTitle} | |
| currentTime={currentTime} | |
| duration={videoRef.current?.duration || 0} | |
| onSeek={handleSeek} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default MoviePlayer; | |