'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; import { useSearchParams } from 'next/navigation'; import { useHistory } from '@/lib/store/history-store'; import { CustomVideoPlayer } from './CustomVideoPlayer'; import { VideoPlayerError } from './VideoPlayerError'; import { VideoPlayerEmpty } from './VideoPlayerEmpty'; import { usePlayerSettings } from './hooks/usePlayerSettings'; interface VideoPlayerProps { playUrl: string; videoId?: string; currentEpisode: number; onBack: () => void; // Episode navigation props for auto-skip/auto-next totalEpisodes?: number; onNextEpisode?: () => void; isReversed?: boolean; isPremium?: boolean; // Danmaku props videoTitle?: string; episodeName?: string; // Expose current time to parent externalTimeRef?: React.MutableRefObject; // Resolution callback onResolutionDetected?: (info: import('./hooks/useVideoResolution').VideoResolutionInfo) => void; } export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack, totalEpisodes, onNextEpisode, isReversed = false, isPremium = false, videoTitle, episodeName, externalTimeRef, onResolutionDetected, }: VideoPlayerProps) { const [videoError, setVideoError] = useState(''); const [useProxy, setUseProxy] = useState(false); const [shouldAutoPlay, setShouldAutoPlay] = useState(true); const [retryCount, setRetryCount] = useState(0); const MAX_MANUAL_RETRIES = 20; const lastSaveTimeRef = useRef(0); const currentTimeRef = useRef(0); const durationRef = useRef(0); const SAVE_INTERVAL = 5000; // 5 seconds throttle const { showModeIndicator, proxyMode } = usePlayerSettings(isPremium); const effectiveUseProxy = proxyMode === 'always' ? true : proxyMode === 'none' ? false : useProxy; // Use reactive hook to subscribe to history updates // This ensures the component re-renders when history is hydrated from localStorage const { viewingHistory, addToHistory } = useHistory(isPremium); const searchParams = useSearchParams(); // Get video metadata from URL params const source = searchParams.get('source') || ''; const title = searchParams.get('title') || '未知视频'; // Get saved progress for this video const getSavedProgress = () => { // Check for explicit time parameter (from source switch) const timeParam = searchParams.get('t'); if (timeParam) { const t = parseFloat(timeParam); if (t > 0 && isFinite(t)) return t; } if (!videoId) return 0; // Match by normalized title + episode index (source-agnostic) const normalizedTitle = title.toLowerCase().trim(); const historyItem = viewingHistory.find(item => item.title.toLowerCase().trim() === normalizedTitle && item.episodeIndex === currentEpisode ); return historyItem ? historyItem.playbackPosition : 0; }; // Save progress function (used by throttle and beforeunload) const saveProgress = useCallback((currentTime: number, duration: number) => { if (!videoId || !playUrl || duration === 0 || currentTime <= 1) return; addToHistory( videoId, title, playUrl, currentEpisode, source, currentTime, duration, undefined, [] ); }, [videoId, playUrl, title, currentEpisode, source, addToHistory]); // Handle time updates and save progress (throttled to every 5 seconds) const handleTimeUpdate = useCallback((currentTime: number, duration: number) => { // Always track current time for beforeunload currentTimeRef.current = currentTime; durationRef.current = duration; // Expose to parent for source switching if (externalTimeRef) externalTimeRef.current = currentTime; if (!videoId || !playUrl || duration === 0) return; const now = Date.now(); // Only save if enough time has passed since last save if (currentTime > 1 && now - lastSaveTimeRef.current >= SAVE_INTERVAL) { lastSaveTimeRef.current = now; saveProgress(currentTime, duration); } }, [videoId, playUrl, saveProgress]); // Save on page leave/refresh useEffect(() => { const handleBeforeUnload = () => { // Save current progress before leaving if (currentTimeRef.current > 1 && durationRef.current > 0) { saveProgress(currentTimeRef.current, durationRef.current); } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, [saveProgress]); // Handle video errors const handleVideoError = (error: string) => { console.error('Video playback error:', error); // Auto-retry with proxy if: // 1. Not already using proxy // 2. Proxy mode is NOT 'none' (so 'retry' or potentially 'always' if it somehow failed locally) // 3. Proxy mode is 'retry' (specifically for the auto-switch logic) // Note: If mode is 'always', we are already using proxy. If it fails, we show error. if (!effectiveUseProxy && proxyMode === 'retry') { setUseProxy(true); setShouldAutoPlay(true); // Force autoplay after proxy retry setVideoError(''); return; } setVideoError(error); }; const handleRetry = () => { if (retryCount >= MAX_MANUAL_RETRIES) return; setRetryCount(prev => prev + 1); setVideoError(''); setShouldAutoPlay(true); // Toggle proxy to try different path, but since we are already in error state which likely means proxy failed (or direct failed), // we can try toggling or just force re-render. // Requirement says: "try without proxy and proxy and same as before" // We will just toggle useProxy state to force a refresh with/without proxy. // However, if we want to cycle, we can just toggle. // But the requirement says "proxy attempt count to 20". // So we just increment count and maybe toggle proxy or keep it. // Let's toggle it to give best chance. // Actually requirement says "try no proxy and proxy and same as before". // So simple toggle is fine. setUseProxy(prev => proxyMode === 'none' ? false : !prev); }; const finalPlayUrl = effectiveUseProxy ? `/api/proxy?url=${encodeURIComponent(playUrl)}&retry=${retryCount}` // Add retry param to force fresh request : playUrl; if (!playUrl) { return ; } return (
{/* Mode Indicator Badge - controlled by settings */} {showModeIndicator && (
{effectiveUseProxy ? '代理模式' : '直连模式'}
)} {videoError ? ( ) : ( )}
); }