'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { detectVideoFormat, VideoFile } from '@/lib/video-format-detector'; import ArtPlayerWrapper from '@/components/artplayer-wrapper'; import LocalPlayerLauncher from '@/components/local-player-launcher'; interface UnifiedVideoPlayerProps { video: VideoFile; isOpen: boolean; onClose: () => void; playerType?: 'modal' | 'inline'; useArtPlayer?: boolean; onProgress?: (time: number) => void; onBookmark?: (videoId: number) => void; onUnbookmark?: (videoId: number) => void; onRate?: (videoId: number, rating: number) => void; showBookmarks?: boolean; showRatings?: boolean; scrollPosition?: number; formatFileSize?: (bytes: number) => string; autoplay?: boolean; } export default function UnifiedVideoPlayer({ video, isOpen, onClose, playerType = 'modal', useArtPlayer: forceArtPlayer = true, // Always use ArtPlayer now onProgress, onBookmark, onUnbookmark, onRate, showBookmarks = false, showRatings = false, scrollPosition, formatFileSize, autoplay = true }: UnifiedVideoPlayerProps) { const [format, setFormat] = useState | null>(null); const [isLoading, setIsLoading] = useState(true); const [isBookmarked, setIsBookmarked] = useState(false); const [bookmarkCheckLoading, setBookmarkCheckLoading] = useState(true); const [currentRating, setCurrentRating] = useState(0); const [ratingCheckLoading, setRatingCheckLoading] = useState(true); const [transcodeLoading, setTranscodeLoading] = useState(false); const [transcodeError, setTranscodeError] = useState(null); const [activeTranscodeJobId, setActiveTranscodeJobId] = useState(null); const activeTranscodeJobIdRef = useRef(null); // Keep ref in sync with state useEffect(() => { activeTranscodeJobIdRef.current = activeTranscodeJobId; }, [activeTranscodeJobId]); const releaseTranscodeJob = useCallback((jobId: string | null) => { if (!jobId) return; fetch(`/api/transcode/${jobId}`, { method: 'DELETE', keepalive: true }).catch(error => { console.warn('[UnifiedVideoPlayer] Failed to release transcode job:', error); }); }, []); const resetDetectedFormat = useCallback(() => { const detectedFormat = detectVideoFormat(video); setFormat(detectedFormat); setTranscodeError(null); setTranscodeLoading(false); return detectedFormat; }, [video]); const handleClose = useCallback(() => { // Read from ref to get the latest value, not a stale closure const jobId = activeTranscodeJobIdRef.current; if (jobId) { releaseTranscodeJob(jobId); setActiveTranscodeJobId(null); } resetDetectedFormat(); onClose(); }, [onClose, releaseTranscodeJob, resetDetectedFormat]); // Check current bookmark status and rating when video opens useEffect(() => { if (isOpen && video.id) { checkBookmarkStatus(); checkRatingStatus(); } }, [isOpen, video.id]); const checkBookmarkStatus = async () => { if (!video.id) return; setBookmarkCheckLoading(true); try { const response = await fetch(`/api/bookmarks/${video.id}`); if (response.ok) { const data = await response.json(); setIsBookmarked(data.isBookmarked || false); } else { setIsBookmarked(false); } } catch (error) { console.error('Error checking bookmark status:', error); setIsBookmarked(false); } finally { setBookmarkCheckLoading(false); } }; const checkRatingStatus = async () => { if (!video.id) return; setRatingCheckLoading(true); try { const response = await fetch(`/api/stars/${video.id}`); if (response.ok) { const data = await response.json(); setCurrentRating(data.rating || 0); } else { setCurrentRating(0); } } catch (error) { console.error('Error checking rating status:', error); setCurrentRating(0); } finally { setRatingCheckLoading(false); } }; // Detect/reset format whenever a video is opened. This avoids reusing a killed HLS job URL // after closing and reopening the same video. useEffect(() => { if (video && isOpen) { console.log('[UnifiedVideoPlayer] Detecting format for video:', video); // Read from ref to get latest jobId without needing it in deps const prevJobId = activeTranscodeJobIdRef.current; if (prevJobId) { releaseTranscodeJob(prevJobId); setActiveTranscodeJobId(null); } const detectedFormat = resetDetectedFormat(); console.log('[UnifiedVideoPlayer] Detected format:', detectedFormat); setIsLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [video, isOpen]); useEffect(() => { if (!isOpen || format?.type !== 'local-player' || process.env.NEXT_PUBLIC_ENABLE_LIVE_TRANSCODE !== 'true') { return; } let cancelled = false; setTranscodeLoading(true); setTranscodeError(null); fetch('/api/transcode/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mediaId: video.id }), }) .then(async response => { const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.error || 'Failed to start live transcode'); } if (!cancelled) { setActiveTranscodeJobId(data.jobId || null); setFormat({ type: 'hls', supportLevel: 'hls', url: data.playlistUrl, warning: `Live transcoding (${data.profile})`, qualities: [{ html: data.profile || 'Transcoded', url: data.playlistUrl, default: true }], }); } }) .catch(error => { if (!cancelled) { console.warn('[UnifiedVideoPlayer] Live transcode unavailable, falling back to local player:', error); setTranscodeError(error instanceof Error ? error.message : 'Live transcode unavailable'); } }) .finally(() => { if (!cancelled) setTranscodeLoading(false); }); return () => { cancelled = true; }; }, [isOpen, format?.type, video.id]); // When the modal closes while a transcode job is active, kill it. useEffect(() => { if (isOpen) return; const jobId = activeTranscodeJobIdRef.current; if (!jobId) return; releaseTranscodeJob(jobId); setActiveTranscodeJobId(null); resetDetectedFormat(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); // Handle ArtPlayer errors with recovery const handleArtPlayerError = useCallback((error: string) => { console.log('ArtPlayer encountered error:', error); // Try to recover by using direct streaming if HLS failed if (format?.type === 'hls') { console.log('HLS failed, trying direct streaming fallback...'); const directFormat = { ...format, type: 'direct' as const, url: `/api/stream/direct/${video.id}`, supportLevel: 'native' as const }; setFormat(directFormat); } else { console.log('ArtPlayer error with direct streaming, logging only'); // Just log the error, no more fallbacks needed } }, [format, video.id]); // Handle progress updates const handleProgressUpdate = useCallback((time: number) => { if (onProgress) { onProgress(time); } }, [onProgress]); // Handle bookmark toggle const handleBookmarkToggle = useCallback(async (videoId: number) => { if (onBookmark) { await onBookmark(videoId); setIsBookmarked(true); // Update local state } }, [onBookmark]); // Handle unbookmark const handleUnbookmark = useCallback(async (videoId: number) => { if (onUnbookmark) { await onUnbookmark(videoId); setIsBookmarked(false); // Update local state } }, [onUnbookmark]); // Handle rating const handleRatingUpdate = useCallback(async (videoId: number, rating: number) => { if (onRate) { await onRate(videoId, rating); setCurrentRating(rating); // Update local state } }, [onRate]); // Render appropriate player based on format const renderPlayer = () => { console.log('[UnifiedVideoPlayer] renderPlayer called with format:', format); console.log('[UnifiedVideoPlayer] format?.type:', format?.type); console.log('[UnifiedVideoPlayer] format?.supportLevel:', format?.supportLevel); // Check if format requires local player if (format?.type === 'local-player') { console.log('[UnifiedVideoPlayer] Rendering LocalPlayerLauncher'); return ( { console.log(`Selected player: ${playerId}`); }} formatFileSize={formatFileSize} onBookmark={handleBookmarkToggle} onUnbookmark={handleUnbookmark} onRate={handleRatingUpdate} showBookmarks={showBookmarks} showRatings={showRatings} notice={transcodeError ? `Live transcode unavailable: ${transcodeError}` : undefined} /> ); } // Default to ArtPlayer for supported formats console.log('[UnifiedVideoPlayer] Rendering ArtPlayerWrapper'); return ( ); }; if (isLoading || bookmarkCheckLoading || ratingCheckLoading || transcodeLoading) { return (

{transcodeLoading ? 'Preparing live transcode...' : 'Loading ArtPlayer...'}

); } console.log('[UnifiedVideoPlayer] Main render - format:', format, 'isLoading:', isLoading); return (
{/* Format indicator (for debugging) */} {process.env.NODE_ENV === 'development' && format && (
{format.type === 'local-player' ? 'Local Player' : 'ArtPlayer'} - {format.supportLevel}
)} {renderPlayer()}
); } // Re-export for external use export { detectVideoFormat };