mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
fix: stabilize iOS web fullscreen across auto-next
This commit is contained in:
@@ -22,6 +22,36 @@ type WebFullscreenSize = 'full' | 'large' | 'focused';
|
||||
|
||||
const WEB_FULLSCREEN_SIZE_KEY = 'kvideo-web-fullscreen-size';
|
||||
const WEB_FULLSCREEN_SIZE_ORDER: WebFullscreenSize[] = ['full', 'large', 'focused'];
|
||||
const WEB_FULLSCREEN_SCALE: Record<WebFullscreenSize, number> = {
|
||||
full: 1,
|
||||
large: 0.92,
|
||||
focused: 0.84,
|
||||
};
|
||||
|
||||
interface ViewportMetrics {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type LegacyInlineVideoProps = React.VideoHTMLAttributes<HTMLVideoElement> & {
|
||||
'webkit-playsinline'?: 'true';
|
||||
};
|
||||
|
||||
const LEGACY_INLINE_VIDEO_PROPS: LegacyInlineVideoProps = {
|
||||
'webkit-playsinline': 'true',
|
||||
};
|
||||
|
||||
function readViewportMetrics(): ViewportMetrics {
|
||||
if (typeof window === 'undefined') {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
const viewport = window.visualViewport;
|
||||
return {
|
||||
width: Math.round(viewport?.width ?? window.innerWidth ?? 0),
|
||||
height: Math.round(viewport?.height ?? window.innerHeight ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
interface DesktopVideoPlayerProps {
|
||||
src: string;
|
||||
@@ -63,6 +93,7 @@ export function DesktopVideoPlayer({
|
||||
const { fullscreenType: settingsFullscreenType } = usePlayerSettings(isPremium);
|
||||
const isIOS = useIsIOS();
|
||||
const isMobile = useIsMobile();
|
||||
const [viewportMetrics, setViewportMetrics] = React.useState<ViewportMetrics>(() => readViewportMetrics());
|
||||
const [seekStepSeconds, setSeekStepSeconds] = React.useState(DEFAULT_SEEK_STEP_SECONDS);
|
||||
const [webFullscreenSize, setWebFullscreenSize] = React.useState<WebFullscreenSize>(() => {
|
||||
if (typeof window === 'undefined') return 'full';
|
||||
@@ -88,25 +119,30 @@ export function DesktopVideoPlayer({
|
||||
episodeIndex: currentEpisodeIndex,
|
||||
});
|
||||
|
||||
// State to track if device is in landscape mode
|
||||
const [isLandscape, setIsLandscape] = React.useState(true);
|
||||
const updateViewportMetrics = React.useCallback(() => {
|
||||
setViewportMetrics((current) => {
|
||||
const next = readViewportMetrics();
|
||||
if (current.width === next.width && current.height === next.height) {
|
||||
return current;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkOrientation = () => {
|
||||
// Check if width > height
|
||||
if (typeof window !== 'undefined') {
|
||||
setIsLandscape(window.innerWidth > window.innerHeight);
|
||||
}
|
||||
};
|
||||
updateViewportMetrics();
|
||||
|
||||
const visualViewport = window.visualViewport;
|
||||
window.addEventListener('resize', updateViewportMetrics);
|
||||
window.addEventListener('orientationchange', updateViewportMetrics);
|
||||
visualViewport?.addEventListener('resize', updateViewportMetrics);
|
||||
|
||||
checkOrientation();
|
||||
window.addEventListener('resize', checkOrientation);
|
||||
window.addEventListener('orientationchange', checkOrientation);
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkOrientation);
|
||||
window.removeEventListener('orientationchange', checkOrientation);
|
||||
window.removeEventListener('resize', updateViewportMetrics);
|
||||
window.removeEventListener('orientationchange', updateViewportMetrics);
|
||||
visualViewport?.removeEventListener('resize', updateViewportMetrics);
|
||||
};
|
||||
}, []);
|
||||
}, [updateViewportMetrics]);
|
||||
|
||||
// Use user preference for fullscreen type, resolving 'auto' to device default
|
||||
// Auto Rules:
|
||||
@@ -116,9 +152,25 @@ export function DesktopVideoPlayer({
|
||||
? (isIOS ? 'window' : isMobile ? 'window' : 'native') // Treat all mobile as window for consistency if auto
|
||||
: settingsFullscreenType;
|
||||
|
||||
const isLandscape = viewportMetrics.width > viewportMetrics.height;
|
||||
|
||||
// Check if we need to force landscape (iOS + Fullscreen + Portrait)
|
||||
const shouldForceLandscape = data.fullscreenMode === 'window' && isIOS && !isLandscape;
|
||||
|
||||
React.useEffect(() => {
|
||||
updateViewportMetrics();
|
||||
|
||||
if (data.fullscreenMode !== 'window') return;
|
||||
|
||||
const rafId = window.requestAnimationFrame(updateViewportMetrics);
|
||||
const timeoutId = window.setTimeout(updateViewportMetrics, 250);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [data.fullscreenMode, src, updateViewportMetrics]);
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem(WEB_FULLSCREEN_SIZE_KEY, webFullscreenSize);
|
||||
}, [webFullscreenSize]);
|
||||
@@ -167,6 +219,7 @@ export function DesktopVideoPlayer({
|
||||
const {
|
||||
videoRef,
|
||||
containerRef,
|
||||
moreMenuTimeoutRef,
|
||||
} = refs;
|
||||
|
||||
const {
|
||||
@@ -243,8 +296,24 @@ export function DesktopVideoPlayer({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const webFullscreenStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
||||
if (data.fullscreenMode !== 'window') return undefined;
|
||||
if (viewportMetrics.width <= 0 || viewportMetrics.height <= 0) return undefined;
|
||||
|
||||
const stageWidth = shouldForceLandscape ? viewportMetrics.height : viewportMetrics.width;
|
||||
const stageHeight = shouldForceLandscape ? viewportMetrics.width : viewportMetrics.height;
|
||||
|
||||
return {
|
||||
['--kvideo-viewport-width' as string]: `${viewportMetrics.width}px`,
|
||||
['--kvideo-viewport-height' as string]: `${viewportMetrics.height}px`,
|
||||
['--kvideo-stage-viewport-width' as string]: `${stageWidth}px`,
|
||||
['--kvideo-stage-viewport-height' as string]: `${stageHeight}px`,
|
||||
['--kvideo-web-scale' as string]: WEB_FULLSCREEN_SCALE[webFullscreenSize].toString(),
|
||||
};
|
||||
}, [data.fullscreenMode, shouldForceLandscape, viewportMetrics, webFullscreenSize]);
|
||||
|
||||
const stageClassName = data.fullscreenMode === 'window'
|
||||
? `kvideo-stage kvideo-web-fullscreen-stage web-fullscreen-size-${webFullscreenSize}`
|
||||
? 'kvideo-stage kvideo-web-fullscreen-stage'
|
||||
: 'kvideo-stage absolute inset-0';
|
||||
|
||||
// Mobile double-tap gesture for skip forward/backward
|
||||
@@ -274,6 +343,7 @@ export function DesktopVideoPlayer({
|
||||
ref={containerRef}
|
||||
className={`kvideo-container relative aspect-video bg-black rounded-[var(--radius-2xl)] group ${data.fullscreenMode === 'window' ? 'is-web-fullscreen' : ''
|
||||
} ${shouldForceLandscape ? 'force-landscape' : ''}`}
|
||||
style={webFullscreenStyle}
|
||||
onMouseMove={() => { handleMouseMove(); }}
|
||||
onMouseLeave={() => isPlaying && setShowControls(false)}
|
||||
>
|
||||
@@ -302,7 +372,7 @@ export function DesktopVideoPlayer({
|
||||
togglePlay();
|
||||
} : undefined}
|
||||
onTouchStart={isMobile ? handleTap : undefined}
|
||||
{...({ 'webkit-playsinline': 'true' } as any)} // Legacy iOS support
|
||||
{...LEGACY_INLINE_VIDEO_PROPS} // Legacy iOS support
|
||||
/>
|
||||
|
||||
{/* Danmaku Canvas */}
|
||||
@@ -341,18 +411,18 @@ export function DesktopVideoPlayer({
|
||||
isProxied={src.includes('/api/proxy')}
|
||||
onToggleMoreMenu={() => actions.setShowMoreMenu(!data.showMoreMenu)}
|
||||
onMoreMenuMouseEnter={() => {
|
||||
if (refs.moreMenuTimeoutRef.current) {
|
||||
clearTimeout(refs.moreMenuTimeoutRef.current);
|
||||
refs.moreMenuTimeoutRef.current = null;
|
||||
if (moreMenuTimeoutRef.current) {
|
||||
clearTimeout(moreMenuTimeoutRef.current);
|
||||
moreMenuTimeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
onMoreMenuMouseLeave={() => {
|
||||
if (refs.moreMenuTimeoutRef.current) {
|
||||
clearTimeout(refs.moreMenuTimeoutRef.current);
|
||||
if (moreMenuTimeoutRef.current) {
|
||||
clearTimeout(moreMenuTimeoutRef.current);
|
||||
}
|
||||
refs.moreMenuTimeoutRef.current = setTimeout(() => {
|
||||
moreMenuTimeoutRef.current = setTimeout(() => {
|
||||
actions.setShowMoreMenu(false);
|
||||
refs.moreMenuTimeoutRef.current = null;
|
||||
moreMenuTimeoutRef.current = null;
|
||||
}, 800); // Increased timeout for better stability
|
||||
}}
|
||||
onCopyLink={logic.handleCopyLink}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
width: var(--kvideo-viewport-width, 100vw) !important;
|
||||
height: var(--kvideo-viewport-height, 100vh) !important;
|
||||
height: var(--kvideo-viewport-height, 100dvh) !important;
|
||||
/* Fallback for browsers supporting dvh */
|
||||
z-index: 2147483647 !important;
|
||||
background: black !important;
|
||||
@@ -18,22 +18,10 @@
|
||||
|
||||
.kvideo-container.is-web-fullscreen .kvideo-web-fullscreen-stage {
|
||||
position: relative !important;
|
||||
width: min(calc(100vw * var(--kvideo-web-scale)), calc(100vh * var(--kvideo-web-scale) * 16 / 9)) !important;
|
||||
height: min(calc(100vh * var(--kvideo-web-scale)), calc(100vw * var(--kvideo-web-scale) * 9 / 16)) !important;
|
||||
max-width: 100vw !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
|
||||
.kvideo-container.is-web-fullscreen .web-fullscreen-size-full {
|
||||
--kvideo-web-scale: 1;
|
||||
}
|
||||
|
||||
.kvideo-container.is-web-fullscreen .web-fullscreen-size-large {
|
||||
--kvideo-web-scale: 0.92;
|
||||
}
|
||||
|
||||
.kvideo-container.is-web-fullscreen .web-fullscreen-size-focused {
|
||||
--kvideo-web-scale: 0.84;
|
||||
width: min(calc(var(--kvideo-stage-viewport-width, var(--kvideo-viewport-width, 100vw)) * var(--kvideo-web-scale)), calc(var(--kvideo-stage-viewport-height, var(--kvideo-viewport-height, 100vh)) * var(--kvideo-web-scale) * 16 / 9)) !important;
|
||||
height: min(calc(var(--kvideo-stage-viewport-height, var(--kvideo-viewport-height, 100vh)) * var(--kvideo-web-scale)), calc(var(--kvideo-stage-viewport-width, var(--kvideo-viewport-width, 100vw)) * var(--kvideo-web-scale) * 9 / 16)) !important;
|
||||
max-width: var(--kvideo-stage-viewport-width, var(--kvideo-viewport-width, 100vw)) !important;
|
||||
max-height: var(--kvideo-stage-viewport-height, var(--kvideo-viewport-height, 100vh)) !important;
|
||||
}
|
||||
|
||||
/* Ensure video takes up full space in web fullscreen */
|
||||
@@ -45,9 +33,9 @@
|
||||
|
||||
/* Force Landscape Mode for Mobile (iOS mainly) */
|
||||
.kvideo-container.is-web-fullscreen.force-landscape {
|
||||
width: 100vh !important;
|
||||
width: 100dvh !important;
|
||||
height: 100vw !important;
|
||||
width: var(--kvideo-viewport-height, 100vh) !important;
|
||||
width: var(--kvideo-viewport-height, 100dvh) !important;
|
||||
height: var(--kvideo-viewport-width, 100vw) !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) rotate(90deg) !important;
|
||||
|
||||
Reference in New Issue
Block a user