@@ -22,6 +22,36 @@ type WebFullscreenSize = 'full' | 'large' | 'focused';
2222
2323const WEB_FULLSCREEN_SIZE_KEY = 'kvideo-web-fullscreen-size' ;
2424const 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
2656interface 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 }
0 commit comments