mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-17 17:53:43 +08:00
296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
import { useCallback, useEffect, useMemo } from 'react';
|
|
import type { FullscreenMode } from '../useDesktopPlayerState';
|
|
|
|
interface UseFullscreenControlsProps {
|
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
setIsFullscreen: (fullscreen: boolean) => void;
|
|
fullscreenMode: FullscreenMode;
|
|
setFullscreenMode: (mode: FullscreenMode) => void;
|
|
isPiPSupported: boolean;
|
|
isAirPlaySupported: boolean;
|
|
setIsPiPSupported: (supported: boolean) => void;
|
|
setIsAirPlaySupported: (supported: boolean) => void;
|
|
fullscreenType?: 'native' | 'window';
|
|
}
|
|
|
|
export function useFullscreenControls({
|
|
containerRef,
|
|
videoRef,
|
|
setIsFullscreen,
|
|
fullscreenMode,
|
|
setFullscreenMode,
|
|
isPiPSupported,
|
|
isAirPlaySupported,
|
|
setIsPiPSupported,
|
|
setIsAirPlaySupported,
|
|
fullscreenType = 'native'
|
|
}: UseFullscreenControlsProps) {
|
|
const lockLandscape = useCallback(async () => {
|
|
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.lock) {
|
|
try {
|
|
await (window.screen as any).orientation.lock('landscape');
|
|
} catch (error) {
|
|
console.warn('Orientation lock failed:', error);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const unlockOrientation = useCallback(() => {
|
|
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.unlock) {
|
|
try {
|
|
(window.screen as any).orientation.unlock();
|
|
} catch {
|
|
// Ignore unlock errors from unsupported browsers.
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const getNativeFullscreenElement = useCallback(() => (
|
|
document.fullscreenElement ||
|
|
(document as any).webkitFullscreenElement ||
|
|
(document as any).mozFullScreenElement ||
|
|
(document as any).msFullscreenElement
|
|
), []);
|
|
|
|
useEffect(() => {
|
|
if (typeof document !== 'undefined') {
|
|
const hasNativePiP = 'pictureInPictureEnabled' in document;
|
|
const hasWebkitPiP = videoRef.current && (
|
|
'webkitSupportsPresentationMode' in (videoRef.current as any) ||
|
|
'webkitPresentationMode' in (videoRef.current as any)
|
|
);
|
|
setIsPiPSupported(hasNativePiP || !!hasWebkitPiP);
|
|
}
|
|
if (typeof window !== 'undefined') {
|
|
setIsAirPlaySupported('WebKitPlaybackTargetAvailabilityEvent' in window);
|
|
}
|
|
}, [setIsPiPSupported, setIsAirPlaySupported, videoRef]);
|
|
|
|
const exitNativeFullscreen = useCallback(async () => {
|
|
try {
|
|
if (document.exitFullscreen) {
|
|
await document.exitFullscreen();
|
|
} else if ((document as any).webkitExitFullscreen) {
|
|
await (document as any).webkitExitFullscreen();
|
|
} else if ((document as any).mozCancelFullScreen) {
|
|
await (document as any).mozCancelFullScreen();
|
|
} else if ((document as any).msExitFullscreen) {
|
|
await (document as any).msExitFullscreen();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to exit fullscreen:', error);
|
|
} finally {
|
|
unlockOrientation();
|
|
setIsFullscreen(false);
|
|
setFullscreenMode('none');
|
|
}
|
|
}, [setFullscreenMode, setIsFullscreen, unlockOrientation]);
|
|
|
|
const exitWindowFullscreen = useCallback(() => {
|
|
unlockOrientation();
|
|
setIsFullscreen(false);
|
|
setFullscreenMode('none');
|
|
}, [setFullscreenMode, setIsFullscreen, unlockOrientation]);
|
|
|
|
const enterWindowFullscreen = useCallback(async () => {
|
|
if (fullscreenMode === 'native') {
|
|
await exitNativeFullscreen();
|
|
}
|
|
|
|
setFullscreenMode('window');
|
|
setIsFullscreen(true);
|
|
await lockLandscape();
|
|
}, [exitNativeFullscreen, fullscreenMode, lockLandscape, setFullscreenMode, setIsFullscreen]);
|
|
|
|
const enterNativeFullscreen = useCallback(async () => {
|
|
if (!containerRef.current) return;
|
|
|
|
if (fullscreenMode === 'window') {
|
|
exitWindowFullscreen();
|
|
}
|
|
|
|
try {
|
|
if (containerRef.current.requestFullscreen) {
|
|
await containerRef.current.requestFullscreen();
|
|
} else if ((containerRef.current as any).webkitRequestFullscreen) {
|
|
await (containerRef.current as any).webkitRequestFullscreen();
|
|
} else if ((containerRef.current as any).mozRequestFullScreen) {
|
|
await (containerRef.current as any).mozRequestFullScreen();
|
|
} else if ((containerRef.current as any).msRequestFullscreen) {
|
|
await (containerRef.current as any).msRequestFullscreen();
|
|
} else if (videoRef.current && (videoRef.current as any).webkitEnterFullscreen) {
|
|
(videoRef.current as any).webkitEnterFullscreen();
|
|
}
|
|
|
|
setFullscreenMode('native');
|
|
setIsFullscreen(true);
|
|
await lockLandscape();
|
|
} catch (error) {
|
|
console.warn('Fullscreen request failed, trying fallback:', error);
|
|
if (videoRef.current && (videoRef.current as any).webkitEnterFullscreen) {
|
|
try {
|
|
(videoRef.current as any).webkitEnterFullscreen();
|
|
setFullscreenMode('native');
|
|
setIsFullscreen(true);
|
|
} catch (fallbackError) {
|
|
console.error('Final fullscreen fallback failed:', fallbackError);
|
|
}
|
|
}
|
|
}
|
|
}, [
|
|
containerRef,
|
|
exitWindowFullscreen,
|
|
fullscreenMode,
|
|
lockLandscape,
|
|
setFullscreenMode,
|
|
setIsFullscreen,
|
|
videoRef,
|
|
]);
|
|
|
|
const toggleWindowFullscreen = useCallback(async () => {
|
|
if (fullscreenMode === 'window') {
|
|
exitWindowFullscreen();
|
|
return;
|
|
}
|
|
|
|
await enterWindowFullscreen();
|
|
}, [enterWindowFullscreen, exitWindowFullscreen, fullscreenMode]);
|
|
|
|
const toggleNativeFullscreen = useCallback(async () => {
|
|
if (fullscreenMode === 'native') {
|
|
await exitNativeFullscreen();
|
|
return;
|
|
}
|
|
|
|
await enterNativeFullscreen();
|
|
}, [enterNativeFullscreen, exitNativeFullscreen, fullscreenMode]);
|
|
|
|
const toggleFullscreen = useCallback(async () => {
|
|
if (fullscreenMode === 'window') {
|
|
exitWindowFullscreen();
|
|
return;
|
|
}
|
|
|
|
if (fullscreenMode === 'native') {
|
|
await exitNativeFullscreen();
|
|
return;
|
|
}
|
|
|
|
if (fullscreenType === 'window') {
|
|
await enterWindowFullscreen();
|
|
return;
|
|
}
|
|
|
|
await enterNativeFullscreen();
|
|
}, [
|
|
enterNativeFullscreen,
|
|
enterWindowFullscreen,
|
|
exitNativeFullscreen,
|
|
exitWindowFullscreen,
|
|
fullscreenMode,
|
|
fullscreenType,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const handleFullscreenChange = () => {
|
|
const nativeFullscreenElement = getNativeFullscreenElement();
|
|
|
|
if (nativeFullscreenElement) {
|
|
setIsFullscreen(true);
|
|
setFullscreenMode('native');
|
|
lockLandscape().catch(() => { });
|
|
return;
|
|
}
|
|
|
|
if (fullscreenMode === 'native') {
|
|
unlockOrientation();
|
|
setIsFullscreen(false);
|
|
setFullscreenMode('none');
|
|
}
|
|
};
|
|
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
|
|
|
return () => {
|
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
|
|
document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
|
|
};
|
|
}, [fullscreenMode, getNativeFullscreenElement, lockLandscape, setFullscreenMode, setIsFullscreen, unlockOrientation]);
|
|
|
|
useEffect(() => {
|
|
if (fullscreenMode !== 'window') return;
|
|
|
|
const previousOverflow = document.body.style.overflow;
|
|
const previousOverscroll = document.body.style.overscrollBehavior;
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.overscrollBehavior = 'contain';
|
|
|
|
return () => {
|
|
document.body.style.overflow = previousOverflow;
|
|
document.body.style.overscrollBehavior = previousOverscroll;
|
|
};
|
|
}, [fullscreenMode]);
|
|
|
|
useEffect(() => {
|
|
if (fullscreenMode !== 'window') return;
|
|
|
|
const handleEsc = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
exitWindowFullscreen();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleEsc);
|
|
return () => window.removeEventListener('keydown', handleEsc);
|
|
}, [exitWindowFullscreen, fullscreenMode]);
|
|
|
|
const togglePictureInPicture = useCallback(async () => {
|
|
if (!videoRef.current || !isPiPSupported) return;
|
|
const video = videoRef.current as any;
|
|
try {
|
|
if (document.pictureInPictureElement) {
|
|
await document.exitPictureInPicture();
|
|
} else if (video.webkitPresentationMode === 'picture-in-picture') {
|
|
video.webkitSetPresentationMode('inline');
|
|
} else if (video.requestPictureInPicture) {
|
|
await video.requestPictureInPicture();
|
|
} else if (video.webkitSupportsPresentationMode && video.webkitSupportsPresentationMode('picture-in-picture')) {
|
|
video.webkitSetPresentationMode('picture-in-picture');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to toggle Picture-in-Picture:', error);
|
|
}
|
|
}, [videoRef, isPiPSupported]);
|
|
|
|
const showAirPlayMenu = useCallback(() => {
|
|
if (!videoRef.current || !isAirPlaySupported) return;
|
|
const video = videoRef.current as any;
|
|
if (video.webkitShowPlaybackTargetPicker) {
|
|
video.webkitShowPlaybackTargetPicker();
|
|
}
|
|
}, [videoRef, isAirPlaySupported]);
|
|
|
|
const fullscreenActions = useMemo(() => ({
|
|
toggleFullscreen,
|
|
toggleNativeFullscreen,
|
|
toggleWindowFullscreen,
|
|
togglePictureInPicture,
|
|
showAirPlayMenu
|
|
}), [
|
|
toggleFullscreen,
|
|
toggleNativeFullscreen,
|
|
toggleWindowFullscreen,
|
|
togglePictureInPicture,
|
|
showAirPlayMenu
|
|
]);
|
|
|
|
return fullscreenActions;
|
|
}
|