Files
KVideo/components/player/hooks/desktop/useFullscreenControls.ts
T

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;
}