feat: Introduce shouldAutoPlay prop for video players and add a video content proxy API.

This commit is contained in:
kuekhaoyang
2025-11-22 16:41:11 +08:00
parent c7045f8f51
commit 4489165a08
10 changed files with 153 additions and 16 deletions
+86
View File
@@ -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',
},
});
}
+4 -3
View File
@@ -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
? <MobileVideoPlayer {...props} />
return isMobile
? <MobileVideoPlayer {...props} />
: <DesktopVideoPlayer {...props} />;
}
+4 -1
View File
@@ -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,
+4 -1
View File
@@ -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,
+19 -9
View File
@@ -17,6 +17,8 @@ interface VideoPlayerProps {
export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoPlayerProps) {
const [videoError, setVideoError] = useState<string>('');
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 (
<Card hover={false} className="p-0 overflow-hidden">
@@ -96,14 +112,6 @@ export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoP
<p className="text-lg font-semibold mb-2"></p>
<p className="text-sm text-gray-300 mb-4">{videoError}</p>
<div className="flex gap-2 justify-center flex-wrap">
<Button
variant="primary"
onClick={() => setVideoError('')}
className="flex items-center gap-2"
>
<Icons.RefreshCw size={16} />
<span></span>
</Button>
<Button
variant="secondary"
onClick={onBack}
@@ -117,10 +125,12 @@ export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoP
</div>
) : (
<CustomVideoPlayer
src={playUrl}
key={useProxy ? 'proxy' : 'direct'} // Force remount when switching modes
src={finalPlayUrl}
onError={handleVideoError}
onTimeUpdate={handleTimeUpdate}
initialTime={getSavedProgress()}
shouldAutoPlay={shouldAutoPlay}
/>
)}
</Card>
@@ -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) {
@@ -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,
@@ -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) {
@@ -13,6 +13,7 @@ type DesktopPlayerState = ReturnType<typeof useDesktopPlayerState>;
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
});
@@ -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
}));