Skip to content

Commit c8b7544

Browse files
committed
fix: stabilize iOS web fullscreen across auto-next
1 parent 773f0f3 commit c8b7544

2 files changed

Lines changed: 103 additions & 45 deletions

File tree

components/player/DesktopVideoPlayer.tsx

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,36 @@ type WebFullscreenSize = 'full' | 'large' | 'focused';
2222

2323
const WEB_FULLSCREEN_SIZE_KEY = 'kvideo-web-fullscreen-size';
2424
const WEB_FULLSCREEN_SIZE_ORDER: WebFullscreenSize[] = ['full', 'large', 'focused'];
25+
const WEB_FULLSCREEN_SCALE: Record<WebFullscreenSize, number> = {
26+
full: 1,
27+
large: 0.92,
28+
focused: 0.84,
29+
};
30+
31+
interface ViewportMetrics {
32+
width: number;
33+
height: number;
34+
}
35+
36+
type LegacyInlineVideoProps = React.VideoHTMLAttributes<HTMLVideoElement> & {
37+
'webkit-playsinline'?: 'true';
38+
};
39+
40+
const LEGACY_INLINE_VIDEO_PROPS: LegacyInlineVideoProps = {
41+
'webkit-playsinline': 'true',
42+
};
43+
44+
function readViewportMetrics(): ViewportMetrics {
45+
if (typeof window === 'undefined') {
46+
return { width: 0, height: 0 };
47+
}
48+
49+
const viewport = window.visualViewport;
50+
return {
51+
width: Math.round(viewport?.width ?? window.innerWidth ?? 0),
52+
height: Math.round(viewport?.height ?? window.innerHeight ?? 0),
53+
};
54+
}
2555

2656
interface DesktopVideoPlayerProps {
2757
src: string;
@@ -63,6 +93,7 @@ export function DesktopVideoPlayer({
6393
const { fullscreenType: settingsFullscreenType } = usePlayerSettings(isPremium);
6494
const isIOS = useIsIOS();
6595
const isMobile = useIsMobile();
96+
const [viewportMetrics, setViewportMetrics] = React.useState<ViewportMetrics>(() => readViewportMetrics());
6697
const [seekStepSeconds, setSeekStepSeconds] = React.useState(DEFAULT_SEEK_STEP_SECONDS);
6798
const [webFullscreenSize, setWebFullscreenSize] = React.useState<WebFullscreenSize>(() => {
6899
if (typeof window === 'undefined') return 'full';
@@ -88,25 +119,30 @@ export function DesktopVideoPlayer({
88119
episodeIndex: currentEpisodeIndex,
89120
});
90121

91-
// State to track if device is in landscape mode
92-
const [isLandscape, setIsLandscape] = React.useState(true);
122+
const updateViewportMetrics = React.useCallback(() => {
123+
setViewportMetrics((current) => {
124+
const next = readViewportMetrics();
125+
if (current.width === next.width && current.height === next.height) {
126+
return current;
127+
}
128+
return next;
129+
});
130+
}, []);
93131

94132
React.useEffect(() => {
95-
const checkOrientation = () => {
96-
// Check if width > height
97-
if (typeof window !== 'undefined') {
98-
setIsLandscape(window.innerWidth > window.innerHeight);
99-
}
100-
};
133+
updateViewportMetrics();
134+
135+
const visualViewport = window.visualViewport;
136+
window.addEventListener('resize', updateViewportMetrics);
137+
window.addEventListener('orientationchange', updateViewportMetrics);
138+
visualViewport?.addEventListener('resize', updateViewportMetrics);
101139

102-
checkOrientation();
103-
window.addEventListener('resize', checkOrientation);
104-
window.addEventListener('orientationchange', checkOrientation);
105140
return () => {
106-
window.removeEventListener('resize', checkOrientation);
107-
window.removeEventListener('orientationchange', checkOrientation);
141+
window.removeEventListener('resize', updateViewportMetrics);
142+
window.removeEventListener('orientationchange', updateViewportMetrics);
143+
visualViewport?.removeEventListener('resize', updateViewportMetrics);
108144
};
109-
}, []);
145+
}, [updateViewportMetrics]);
110146

111147
// Use user preference for fullscreen type, resolving 'auto' to device default
112148
// Auto Rules:
@@ -116,9 +152,25 @@ export function DesktopVideoPlayer({
116152
? (isIOS ? 'window' : isMobile ? 'window' : 'native') // Treat all mobile as window for consistency if auto
117153
: settingsFullscreenType;
118154

155+
const isLandscape = viewportMetrics.width > viewportMetrics.height;
156+
119157
// Check if we need to force landscape (iOS + Fullscreen + Portrait)
120158
const shouldForceLandscape = data.fullscreenMode === 'window' && isIOS && !isLandscape;
121159

160+
React.useEffect(() => {
161+
updateViewportMetrics();
162+
163+
if (data.fullscreenMode !== 'window') return;
164+
165+
const rafId = window.requestAnimationFrame(updateViewportMetrics);
166+
const timeoutId = window.setTimeout(updateViewportMetrics, 250);
167+
168+
return () => {
169+
window.cancelAnimationFrame(rafId);
170+
window.clearTimeout(timeoutId);
171+
};
172+
}, [data.fullscreenMode, src, updateViewportMetrics]);
173+
122174
React.useEffect(() => {
123175
localStorage.setItem(WEB_FULLSCREEN_SIZE_KEY, webFullscreenSize);
124176
}, [webFullscreenSize]);
@@ -167,6 +219,7 @@ export function DesktopVideoPlayer({
167219
const {
168220
videoRef,
169221
containerRef,
222+
moreMenuTimeoutRef,
170223
} = refs;
171224

172225
const {
@@ -243,8 +296,24 @@ export function DesktopVideoPlayer({
243296
});
244297
}, []);
245298

299+
const webFullscreenStyle = React.useMemo<React.CSSProperties | undefined>(() => {
300+
if (data.fullscreenMode !== 'window') return undefined;
301+
if (viewportMetrics.width <= 0 || viewportMetrics.height <= 0) return undefined;
302+
303+
const stageWidth = shouldForceLandscape ? viewportMetrics.height : viewportMetrics.width;
304+
const stageHeight = shouldForceLandscape ? viewportMetrics.width : viewportMetrics.height;
305+
306+
return {
307+
['--kvideo-viewport-width' as string]: `${viewportMetrics.width}px`,
308+
['--kvideo-viewport-height' as string]: `${viewportMetrics.height}px`,
309+
['--kvideo-stage-viewport-width' as string]: `${stageWidth}px`,
310+
['--kvideo-stage-viewport-height' as string]: `${stageHeight}px`,
311+
['--kvideo-web-scale' as string]: WEB_FULLSCREEN_SCALE[webFullscreenSize].toString(),
312+
};
313+
}, [data.fullscreenMode, shouldForceLandscape, viewportMetrics, webFullscreenSize]);
314+
246315
const stageClassName = data.fullscreenMode === 'window'
247-
? `kvideo-stage kvideo-web-fullscreen-stage web-fullscreen-size-${webFullscreenSize}`
316+
? 'kvideo-stage kvideo-web-fullscreen-stage'
248317
: 'kvideo-stage absolute inset-0';
249318

250319
// Mobile double-tap gesture for skip forward/backward
@@ -274,6 +343,7 @@ export function DesktopVideoPlayer({
274343
ref={containerRef}
275344
className={`kvideo-container relative aspect-video bg-black rounded-[var(--radius-2xl)] group ${data.fullscreenMode === 'window' ? 'is-web-fullscreen' : ''
276345
} ${shouldForceLandscape ? 'force-landscape' : ''}`}
346+
style={webFullscreenStyle}
277347
onMouseMove={() => { handleMouseMove(); }}
278348
onMouseLeave={() => isPlaying && setShowControls(false)}
279349
>
@@ -302,7 +372,7 @@ export function DesktopVideoPlayer({
302372
togglePlay();
303373
} : undefined}
304374
onTouchStart={isMobile ? handleTap : undefined}
305-
{...({ 'webkit-playsinline': 'true' } as any)} // Legacy iOS support
375+
{...LEGACY_INLINE_VIDEO_PROPS} // Legacy iOS support
306376
/>
307377

308378
{/* Danmaku Canvas */}
@@ -341,18 +411,18 @@ export function DesktopVideoPlayer({
341411
isProxied={src.includes('/api/proxy')}
342412
onToggleMoreMenu={() => actions.setShowMoreMenu(!data.showMoreMenu)}
343413
onMoreMenuMouseEnter={() => {
344-
if (refs.moreMenuTimeoutRef.current) {
345-
clearTimeout(refs.moreMenuTimeoutRef.current);
346-
refs.moreMenuTimeoutRef.current = null;
414+
if (moreMenuTimeoutRef.current) {
415+
clearTimeout(moreMenuTimeoutRef.current);
416+
moreMenuTimeoutRef.current = null;
347417
}
348418
}}
349419
onMoreMenuMouseLeave={() => {
350-
if (refs.moreMenuTimeoutRef.current) {
351-
clearTimeout(refs.moreMenuTimeoutRef.current);
420+
if (moreMenuTimeoutRef.current) {
421+
clearTimeout(moreMenuTimeoutRef.current);
352422
}
353-
refs.moreMenuTimeoutRef.current = setTimeout(() => {
423+
moreMenuTimeoutRef.current = setTimeout(() => {
354424
actions.setShowMoreMenu(false);
355-
refs.moreMenuTimeoutRef.current = null;
425+
moreMenuTimeoutRef.current = null;
356426
}, 800); // Increased timeout for better stability
357427
}}
358428
onCopyLink={logic.handleCopyLink}

components/player/web-fullscreen.css

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
position: fixed !important;
44
top: 0 !important;
55
left: 0 !important;
6-
width: 100vw !important;
7-
height: 100vh !important;
8-
height: 100dvh !important;
6+
width: var(--kvideo-viewport-width, 100vw) !important;
7+
height: var(--kvideo-viewport-height, 100vh) !important;
8+
height: var(--kvideo-viewport-height, 100dvh) !important;
99
/* Fallback for browsers supporting dvh */
1010
z-index: 2147483647 !important;
1111
background: black !important;
@@ -18,22 +18,10 @@
1818

1919
.kvideo-container.is-web-fullscreen .kvideo-web-fullscreen-stage {
2020
position: relative !important;
21-
width: min(calc(100vw * var(--kvideo-web-scale)), calc(100vh * var(--kvideo-web-scale) * 16 / 9)) !important;
22-
height: min(calc(100vh * var(--kvideo-web-scale)), calc(100vw * var(--kvideo-web-scale) * 9 / 16)) !important;
23-
max-width: 100vw !important;
24-
max-height: 100vh !important;
25-
}
26-
27-
.kvideo-container.is-web-fullscreen .web-fullscreen-size-full {
28-
--kvideo-web-scale: 1;
29-
}
30-
31-
.kvideo-container.is-web-fullscreen .web-fullscreen-size-large {
32-
--kvideo-web-scale: 0.92;
33-
}
34-
35-
.kvideo-container.is-web-fullscreen .web-fullscreen-size-focused {
36-
--kvideo-web-scale: 0.84;
21+
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;
22+
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;
23+
max-width: var(--kvideo-stage-viewport-width, var(--kvideo-viewport-width, 100vw)) !important;
24+
max-height: var(--kvideo-stage-viewport-height, var(--kvideo-viewport-height, 100vh)) !important;
3725
}
3826

3927
/* Ensure video takes up full space in web fullscreen */
@@ -45,9 +33,9 @@
4533

4634
/* Force Landscape Mode for Mobile (iOS mainly) */
4735
.kvideo-container.is-web-fullscreen.force-landscape {
48-
width: 100vh !important;
49-
width: 100dvh !important;
50-
height: 100vw !important;
36+
width: var(--kvideo-viewport-height, 100vh) !important;
37+
width: var(--kvideo-viewport-height, 100dvh) !important;
38+
height: var(--kvideo-viewport-width, 100vw) !important;
5139
top: 50% !important;
5240
left: 50% !important;
5341
transform: translate(-50%, -50%) rotate(90deg) !important;

0 commit comments

Comments
 (0)