web / frontend /src /components /MoviePlayer.tsx
Chandima Prabhath
Track bun.lockb with Git LFS
cc2caf9
raw
history blame
9.41 kB
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;