11import { useEffect } from 'react' ;
2- import { BackHandler , StatusBar , StyleSheet , View } from 'react-native' ;
2+ import {
3+ BackHandler ,
4+ Modal ,
5+ Platform ,
6+ StatusBar ,
7+ StyleSheet ,
8+ View ,
9+ } from 'react-native' ;
310import { FULLSCREEN_SURFACE_ID } from '../core/VideoManager' ;
411import { usePlayback } from '../hooks/usePlayback' ;
512import { useVideoManager } from '../provider/VideoContext' ;
13+ import type { OrientationLock } from '../types/video' ;
614import { VideoControls } from './VideoControls' ;
715import { VideoSurface } from './VideoSurface' ;
816
17+ type ModalOrientation =
18+ | 'portrait'
19+ | 'portrait-upside-down'
20+ | 'landscape'
21+ | 'landscape-left'
22+ | 'landscape-right' ;
23+
24+ const ALL_ORIENTATIONS : ModalOrientation [ ] = [
25+ 'portrait' ,
26+ 'portrait-upside-down' ,
27+ 'landscape' ,
28+ 'landscape-left' ,
29+ 'landscape-right' ,
30+ ] ;
31+
32+ /** Which orientations the iOS Modal may present — this is what locks it. */
33+ function modalOrientations ( lock : OrientationLock ) : ModalOrientation [ ] {
34+ switch ( lock ) {
35+ case 'portrait' :
36+ case 'inverted-portrait' :
37+ return [ 'portrait' , 'portrait-upside-down' ] ;
38+ case 'landscape' :
39+ case 'inverted-landscape' :
40+ return [ 'landscape' , 'landscape-left' , 'landscape-right' ] ;
41+ default :
42+ return ALL_ORIENTATIONS ; // 'auto' → follow the sensor
43+ }
44+ }
45+
946/**
10- * Built-in fullscreen host, rendered by VideoProvider. While visible,
11- * rotation is unlocked natively (all orientations allowed); on exit the
12- * previous orientation lock is restored and the player re-attaches to the
13- * surface it came from.
14- *
15- * Implemented as an in-window absolute overlay (not a Modal). A Modal is a
16- * separate Android window, and re-parenting the player's TextureView into it
17- * — then rotating — drops the video surface (black screen, audio only) for
18- * live streams. The floating host already proves in-window re-parenting is
19- * reliable, so fullscreen uses the same approach.
47+ * Built-in fullscreen host, rendered by VideoProvider.
2048 *
21- * Rendered automatically — you normally never mount this yourself. Apps
22- * that want fullscreen "inside" a navigation screen can instead render a
23- * plain <VideoSurface> there and call attach().
49+ * Platform split:
50+ * - iOS uses a `Modal` whose `supportedOrientations` are derived from the
51+ * locked fullscreen orientation. That's how iOS rotates a fullscreen video
52+ * (and locks out the portrait sensor) WITHOUT app-wide landscape config.
53+ * - Android uses an in-window absolute overlay (a Modal is a separate window,
54+ * and re-parenting the player's TextureView into it drops the video surface
55+ * — black screen, audio only — for live streams). Rotation is driven by the
56+ * native `requestedOrientation` lock.
2457 */
2558export function FullscreenPlayer ( ) {
2659 const manager = useVideoManager ( ) ;
2760 const fullscreen = usePlayback ( ( s ) => s . fullscreen ) ;
61+ const fullscreenLock = usePlayback ( ( s ) => s . fullscreenLock ) ;
2862
29- // Android hardware back exits fullscreen (Modal used to do this for us ).
63+ // Android hardware back exits fullscreen (the iOS Modal handles its own ).
3064 useEffect ( ( ) => {
31- if ( ! fullscreen ) {
65+ if ( ! fullscreen || Platform . OS !== 'android' ) {
3266 return ;
3367 }
3468 const sub = BackHandler . addEventListener ( 'hardwareBackPress' , ( ) => {
@@ -42,20 +76,42 @@ export function FullscreenPlayer() {
4276 return null ;
4377 }
4478
45- return (
46- < View style = { styles . overlay } >
79+ const content = (
80+ < >
4781 < StatusBar hidden />
4882 < VideoSurface
4983 surfaceId = { FULLSCREEN_SURFACE_ID }
5084 autoAttach
5185 style = { styles . surface }
5286 />
5387 < VideoControls onClose = { ( ) => manager . exitFullscreen ( ) } />
54- </ View >
88+ </ >
5589 ) ;
90+
91+ if ( Platform . OS === 'ios' ) {
92+ return (
93+ < Modal
94+ visible
95+ transparent = { false }
96+ animationType = "fade"
97+ presentationStyle = "fullScreen"
98+ statusBarTranslucent
99+ supportedOrientations = { modalOrientations ( fullscreenLock ) }
100+ onRequestClose = { ( ) => manager . exitFullscreen ( ) }
101+ >
102+ < View style = { styles . container } > { content } </ View >
103+ </ Modal >
104+ ) ;
105+ }
106+
107+ return < View style = { styles . overlay } > { content } </ View > ;
56108}
57109
58110const styles = StyleSheet . create ( {
111+ container : {
112+ flex : 1 ,
113+ backgroundColor : '#000' ,
114+ } ,
59115 overlay : {
60116 position : 'absolute' ,
61117 top : 0 ,
0 commit comments