diff --git a/app/api/proxy/route.ts b/app/api/proxy/route.ts new file mode 100644 index 0000000..b8a9939 --- /dev/null +++ b/app/api/proxy/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const url = request.nextUrl.searchParams.get('url'); + + if (!url) { + return new NextResponse('Missing URL parameter', { status: 400 }); + } + + try { + // Beijing IP address to simulate request from China + const chinaIP = '202.108.22.5'; + + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'X-Forwarded-For': chinaIP, + 'Client-IP': chinaIP, + 'Referer': new URL(url).origin, + }, + }); + + const contentType = response.headers.get('Content-Type'); + + // Handle m3u8 playlists: rewrite URLs to go through proxy + if (contentType && (contentType.includes('application/vnd.apple.mpegurl') || contentType.includes('application/x-mpegurl') || url.endsWith('.m3u8'))) { + const text = await response.text(); + const baseUrl = new URL(url); + + const modifiedText = text.split('\n').map(line => { + // Skip comments and empty lines + if (line.trim().startsWith('#') || !line.trim()) { + return line; + } + + // Resolve relative URLs + try { + const absoluteUrl = new URL(line.trim(), baseUrl).toString(); + // Wrap in proxy + return `${request.nextUrl.origin}/api/proxy?url=${encodeURIComponent(absoluteUrl)}`; + } catch (e) { + return line; + } + }).join('\n'); + + return new NextResponse(modifiedText, { + status: response.status, + statusText: response.statusText, + headers: { + 'Content-Type': contentType, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } + + // For non-m3u8 content (segments, mp4, etc.), stream directly + const newResponse = new NextResponse(response.body, { + status: response.status, + statusText: response.statusText, + headers: new Headers(response.headers), + }); + + // Add CORS headers to allow playback + newResponse.headers.set('Access-Control-Allow-Origin', '*'); + newResponse.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); + newResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + return newResponse; + } catch (error) { + console.error('Proxy error:', error); + return new NextResponse('Proxy failed', { status: 500 }); + } +} + +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 204, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} diff --git a/components/player/CustomVideoPlayer.tsx b/components/player/CustomVideoPlayer.tsx index 3434483..d9fb4df 100644 --- a/components/player/CustomVideoPlayer.tsx +++ b/components/player/CustomVideoPlayer.tsx @@ -10,6 +10,7 @@ interface CustomVideoPlayerProps { onError?: (error: string) => void; onTimeUpdate?: (currentTime: number, duration: number) => void; initialTime?: number; + shouldAutoPlay?: boolean; } /** @@ -19,8 +20,8 @@ interface CustomVideoPlayerProps { */ export function CustomVideoPlayer(props: CustomVideoPlayerProps) { const isMobile = useIsMobile(); - - return isMobile - ? + + return isMobile + ? : ; } diff --git a/components/player/DesktopVideoPlayer.tsx b/components/player/DesktopVideoPlayer.tsx index e58bc8b..b016105 100644 --- a/components/player/DesktopVideoPlayer.tsx +++ b/components/player/DesktopVideoPlayer.tsx @@ -11,6 +11,7 @@ interface DesktopVideoPlayerProps { onError?: (error: string) => void; onTimeUpdate?: (currentTime: number, duration: number) => void; initialTime?: number; + shouldAutoPlay?: boolean; } export function DesktopVideoPlayer({ @@ -18,7 +19,8 @@ export function DesktopVideoPlayer({ poster, onError, onTimeUpdate, - initialTime = 0 + initialTime = 0, + shouldAutoPlay = false }: DesktopVideoPlayerProps) { const { refs, state } = useDesktopPlayerState(); const { @@ -35,6 +37,7 @@ export function DesktopVideoPlayer({ const logic = useDesktopPlayerLogic({ src, initialTime, + shouldAutoPlay, onError, onTimeUpdate, refs, diff --git a/components/player/MobileVideoPlayer.tsx b/components/player/MobileVideoPlayer.tsx index f8a76c8..54b5b01 100644 --- a/components/player/MobileVideoPlayer.tsx +++ b/components/player/MobileVideoPlayer.tsx @@ -15,6 +15,7 @@ interface MobileVideoPlayerProps { onError?: (error: string) => void; onTimeUpdate?: (currentTime: number, duration: number) => void; initialTime?: number; + shouldAutoPlay?: boolean; } export function MobileVideoPlayer({ @@ -22,7 +23,8 @@ export function MobileVideoPlayer({ poster, onError, onTimeUpdate, - initialTime = 0 + initialTime = 0, + shouldAutoPlay = false }: MobileVideoPlayerProps) { const { refs, state } = useMobilePlayerState(); const { @@ -48,6 +50,7 @@ export function MobileVideoPlayer({ const logic = useMobilePlayerLogic({ src, initialTime, + shouldAutoPlay, onError, onTimeUpdate, refs, diff --git a/components/player/VideoPlayer.tsx b/components/player/VideoPlayer.tsx index 3ba4aa2..2533090 100644 --- a/components/player/VideoPlayer.tsx +++ b/components/player/VideoPlayer.tsx @@ -17,6 +17,8 @@ interface VideoPlayerProps { export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoPlayerProps) { const [videoError, setVideoError] = useState(''); + const [useProxy, setUseProxy] = useState(false); + const [shouldAutoPlay, setShouldAutoPlay] = useState(false); // Use reactive hook to subscribe to history updates // This ensures the component re-renders when history is hydrated from localStorage const viewingHistory = useHistoryStore(state => state.viewingHistory); @@ -66,9 +68,23 @@ export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoP // Handle video errors const handleVideoError = (error: string) => { console.error('Video playback error:', error); + + // Auto-retry with proxy if not already using it + if (!useProxy) { + console.log('Attempting to retry with proxy...'); + setUseProxy(true); + setShouldAutoPlay(true); // Force autoplay after proxy retry + setVideoError(''); + return; + } + setVideoError(error); }; + const finalPlayUrl = useProxy + ? `/api/proxy?url=${encodeURIComponent(playUrl)}` + : playUrl; + if (!playUrl) { return ( @@ -96,14 +112,6 @@ export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoP 播放失败 {videoError} - setVideoError('')} - className="flex items-center gap-2" - > - - 重试 - ) : ( )} diff --git a/components/player/hooks/desktop/usePlaybackControls.ts b/components/player/hooks/desktop/usePlaybackControls.ts index 788d288..0cc53fa 100644 --- a/components/player/hooks/desktop/usePlaybackControls.ts +++ b/components/player/hooks/desktop/usePlaybackControls.ts @@ -6,6 +6,7 @@ interface UsePlaybackControlsProps { setIsPlaying: (playing: boolean) => void; setIsLoading: (loading: boolean) => void; initialTime: number; + shouldAutoPlay: boolean; setDuration: (duration: number) => void; setCurrentTime: (time: number) => void; onTimeUpdate?: (currentTime: number, duration: number) => void; @@ -22,6 +23,7 @@ export function usePlaybackControls({ setIsPlaying, setIsLoading, initialTime, + shouldAutoPlay, setDuration, setCurrentTime, onTimeUpdate, @@ -77,6 +79,18 @@ export function usePlaybackControls({ } }, [initialTime, videoRef]); + // Force autoplay when shouldAutoPlay is true (for proxy retry) + useEffect(() => { + if (shouldAutoPlay && videoRef.current) { + const playPromise = videoRef.current.play(); + if (playPromise !== undefined) { + playPromise.catch((err: Error) => { + console.warn('Force autoplay was prevented:', err); + }); + } + } + }, [shouldAutoPlay, videoRef]); + const handleVideoError = useCallback(() => { setIsLoading(false); if (onError) { diff --git a/components/player/hooks/mobile/mobile-player-params.ts b/components/player/hooks/mobile/mobile-player-params.ts index d06c685..454460d 100644 --- a/components/player/hooks/mobile/mobile-player-params.ts +++ b/components/player/hooks/mobile/mobile-player-params.ts @@ -9,6 +9,7 @@ export function buildPlaybackParams(props: any) { setIsPlaying, setIsLoading, initialTime, + shouldAutoPlay, setDuration, setCurrentTime, setPlaybackRate, @@ -27,6 +28,7 @@ export function buildPlaybackParams(props: any) { setIsPlaying, setIsLoading, initialTime, + shouldAutoPlay, setDuration, setCurrentTime, setPlaybackRate, diff --git a/components/player/hooks/mobile/useMobilePlaybackControls.ts b/components/player/hooks/mobile/useMobilePlaybackControls.ts index 09d3413..0c4d89c 100644 --- a/components/player/hooks/mobile/useMobilePlaybackControls.ts +++ b/components/player/hooks/mobile/useMobilePlaybackControls.ts @@ -6,6 +6,7 @@ interface UseMobilePlaybackProps { setIsPlaying: (playing: boolean) => void; setIsLoading: (loading: boolean) => void; initialTime: number; + shouldAutoPlay: boolean; setDuration: (duration: number) => void; setCurrentTime: (time: number) => void; setPlaybackRate: (rate: number) => void; @@ -24,6 +25,7 @@ export function useMobilePlaybackControls({ setIsPlaying, setIsLoading, initialTime, + shouldAutoPlay, setDuration, setCurrentTime, setPlaybackRate, @@ -92,6 +94,18 @@ export function useMobilePlaybackControls({ } }, [initialTime, videoRef]); + // Force autoplay when shouldAutoPlay is true (for proxy retry) + useEffect(() => { + if (shouldAutoPlay && videoRef.current) { + const playPromise = videoRef.current.play(); + if (playPromise !== undefined) { + playPromise.catch((err: Error) => { + console.warn('Force autoplay was prevented:', err); + }); + } + } + }, [shouldAutoPlay, videoRef]); + const handleVideoError = useCallback(() => { setIsLoading(false); if (onError) { diff --git a/components/player/hooks/useDesktopPlayerLogic.ts b/components/player/hooks/useDesktopPlayerLogic.ts index 69e2ee5..ef60077 100644 --- a/components/player/hooks/useDesktopPlayerLogic.ts +++ b/components/player/hooks/useDesktopPlayerLogic.ts @@ -13,6 +13,7 @@ type DesktopPlayerState = ReturnType; interface UseDesktopPlayerLogicProps { src: string; initialTime: number; + shouldAutoPlay: boolean; onError?: (error: string) => void; onTimeUpdate?: (currentTime: number, duration: number) => void; refs: DesktopPlayerState['refs']; @@ -22,6 +23,7 @@ interface UseDesktopPlayerLogicProps { export function useDesktopPlayerLogic({ src, initialTime, + shouldAutoPlay, onError, onTimeUpdate, refs, @@ -57,7 +59,7 @@ export function useDesktopPlayerLogic({ const playbackControls = usePlaybackControls({ videoRef, isPlaying, setIsPlaying, setIsLoading, - initialTime, setDuration, setCurrentTime, onTimeUpdate, onError, + initialTime, shouldAutoPlay, setDuration, setCurrentTime, onTimeUpdate, onError, isDraggingProgressRef, speedMenuTimeoutRef, setPlaybackRate, setShowSpeedMenu }); diff --git a/components/player/hooks/useMobilePlayerLogic.ts b/components/player/hooks/useMobilePlayerLogic.ts index 0058639..7d90a39 100644 --- a/components/player/hooks/useMobilePlayerLogic.ts +++ b/components/player/hooks/useMobilePlayerLogic.ts @@ -17,6 +17,7 @@ interface UseMobilePlayerLogicProps { src: string; poster?: string; initialTime: number; + shouldAutoPlay: boolean; onError?: (error: string) => void; onTimeUpdate?: (currentTime: number, duration: number) => void; refs: any; @@ -26,6 +27,7 @@ interface UseMobilePlayerLogicProps { export function useMobilePlayerLogic({ src, initialTime, + shouldAutoPlay, onError, onTimeUpdate, refs, @@ -67,7 +69,7 @@ export function useMobilePlayerLogic({ } = state; const playbackControls = useMobilePlaybackControls(buildPlaybackParams({ - videoRef, isPlaying, setIsPlaying, setIsLoading, initialTime, setDuration, + videoRef, isPlaying, setIsPlaying, setIsLoading, initialTime, shouldAutoPlay, setDuration, setCurrentTime, setPlaybackRate, setShowMoreMenu, setShowVolumeMenu, setShowSpeedMenu, onTimeUpdate, onError, isDraggingProgressRef, isTogglingRef }));
播放失败
{videoError}