mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-16 09:13:42 +08:00
feat: Introduce shouldAutoPlay prop for video players and add a video content proxy API.
This commit is contained in:
@@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user