@moviie/player-expo 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/app.plugin.cjs +3 -0
  4. package/dist/cast.cjs +100 -0
  5. package/dist/cast.cjs.map +1 -0
  6. package/dist/cast.d.cts +15 -0
  7. package/dist/cast.d.ts +15 -0
  8. package/dist/cast.mjs +92 -0
  9. package/dist/cast.mjs.map +1 -0
  10. package/dist/chunk-67DJ7NOB.mjs +294 -0
  11. package/dist/chunk-67DJ7NOB.mjs.map +1 -0
  12. package/dist/chunk-7U2LKIGU.mjs +12 -0
  13. package/dist/chunk-7U2LKIGU.mjs.map +1 -0
  14. package/dist/chunk-BJTO5JO5.mjs +10 -0
  15. package/dist/chunk-BJTO5JO5.mjs.map +1 -0
  16. package/dist/index.cjs +3934 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +450 -0
  19. package/dist/index.d.ts +450 -0
  20. package/dist/index.mjs +3571 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/layout.cjs +217 -0
  23. package/dist/layout.cjs.map +1 -0
  24. package/dist/layout.d.cts +20 -0
  25. package/dist/layout.d.ts +20 -0
  26. package/dist/layout.mjs +4 -0
  27. package/dist/layout.mjs.map +1 -0
  28. package/dist/moviie-cast-adapter-DmSU2u3j.d.cts +53 -0
  29. package/dist/moviie-cast-adapter-DmSU2u3j.d.ts +53 -0
  30. package/dist/plugin/with-moviie.cjs +88 -0
  31. package/dist/plugin/with-moviie.cjs.map +1 -0
  32. package/dist/plugin/with-moviie.d.cts +52 -0
  33. package/dist/plugin/with-moviie.d.ts +52 -0
  34. package/dist/plugin/with-moviie.mjs +76 -0
  35. package/dist/plugin/with-moviie.mjs.map +1 -0
  36. package/package.json +134 -0
  37. package/plugin/validate-options.ts +31 -0
  38. package/plugin/with-moviie-types.ts +21 -0
  39. package/plugin/with-moviie.ts +84 -0
  40. package/src/apply-expo-moviie-endpoints.ts +47 -0
  41. package/src/cast/google-cast-adapter.ts +111 -0
  42. package/src/cast/index.ts +12 -0
  43. package/src/components/controls/moviie-bottom-timeline.tsx +477 -0
  44. package/src/components/controls/moviie-cast-buttons.tsx +96 -0
  45. package/src/components/controls/moviie-chrome-edge-gradients.tsx +162 -0
  46. package/src/components/controls/moviie-controls.tsx +585 -0
  47. package/src/components/controls/moviie-skin-chrome-context.tsx +374 -0
  48. package/src/components/controls/moviie-skin-smart-progress.tsx +157 -0
  49. package/src/components/icons/embed-media-icons.tsx +282 -0
  50. package/src/components/icons/moviie-embed-brand-mark.tsx +58 -0
  51. package/src/components/moviie-error-boundary.tsx +80 -0
  52. package/src/components/moviie-player-error-shell.tsx +143 -0
  53. package/src/components/moviie-player-loading-shell.tsx +59 -0
  54. package/src/components/moviie-skin-custom-fullscreen-modal.tsx +232 -0
  55. package/src/components/moviie-video-props.ts +134 -0
  56. package/src/components/moviie-video.tsx +568 -0
  57. package/src/components/moviie-video.web.tsx +167 -0
  58. package/src/components/overlays/moviie-watermark.tsx +53 -0
  59. package/src/constants.ts +374 -0
  60. package/src/hooks/use-moviie-event.ts +103 -0
  61. package/src/hooks/use-moviie-playback-ended.ts +14 -0
  62. package/src/hooks/use-moviie-playback-resume-persistence.ts +76 -0
  63. package/src/hooks/use-moviie-playback.ts +99 -0
  64. package/src/hooks/use-moviie-player-types.ts +55 -0
  65. package/src/hooks/use-moviie-player.ts +236 -0
  66. package/src/hooks/use-moviie-player.web.ts +57 -0
  67. package/src/hooks/use-moviie-telemetry.ts +133 -0
  68. package/src/index.ts +90 -0
  69. package/src/layout.ts +4 -0
  70. package/src/lib/add-moviie-playback-ended-listener.ts +16 -0
  71. package/src/lib/build-moviie-branding-marketing-url.ts +14 -0
  72. package/src/lib/build-moviie-embed-brand-home-url.ts +16 -0
  73. package/src/lib/build-moviie-skin-layout-metrics.ts +166 -0
  74. package/src/lib/build-moviie-video-presentation.ts +42 -0
  75. package/src/lib/build-moviie-watch-embed-url.ts +30 -0
  76. package/src/lib/cast-adapter-registry.ts +13 -0
  77. package/src/lib/clamp-unit-interval.ts +3 -0
  78. package/src/lib/compute-custom-fullscreen-stage-dimensions.ts +39 -0
  79. package/src/lib/compute-moviie-playback-ended.ts +31 -0
  80. package/src/lib/compute-playback-buffering.ts +41 -0
  81. package/src/lib/compute-playback-progress-ratio.ts +33 -0
  82. package/src/lib/compute-timeline-scrub-commit-time.ts +40 -0
  83. package/src/lib/custom-fullscreen-native-orientation.ts +88 -0
  84. package/src/lib/format-playback-clock.ts +27 -0
  85. package/src/lib/jsx-native-bridge.ts +45 -0
  86. package/src/lib/map-smart-progress-display-ratio.ts +43 -0
  87. package/src/lib/moviie-cast-adapter.ts +53 -0
  88. package/src/lib/moviie-error-display.ts +149 -0
  89. package/src/lib/moviie-shell-tokens.ts +27 -0
  90. package/src/lib/moviie-telemetry-callbacks.ts +76 -0
  91. package/src/lib/optional-status-bar.ts +48 -0
  92. package/src/lib/partition-moviie-video-host-style.ts +54 -0
  93. package/src/lib/remember-playback-position-storage.ts +98 -0
  94. package/src/lib/resolve-moviie-skin-layout-scale.ts +17 -0
  95. package/src/lib/resolve-orientation-lock-for-rotate-z-deg.ts +21 -0
  96. package/src/lib/resolve-skin-accent-color.ts +10 -0
  97. package/src/lib/resolve-web-embed-iframe-src.ts +17 -0
  98. package/src/lib/warn-once.ts +23 -0
  99. package/src/platform/native-application-id.ts +10 -0
  100. package/src/platform/platform-client-info.ts +31 -0
  101. package/src/playback/fetch-playback-fresh.ts +27 -0
  102. package/src/playback/playback-memory-cache.ts +52 -0
  103. package/src/provider/moviie-provider.tsx +99 -0
  104. package/src/secure-store-viewer-token-store.ts +80 -0
@@ -0,0 +1,568 @@
1
+ import {
2
+ createElement,
3
+ forwardRef,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ComponentType,
10
+ type ForwardedRef,
11
+ } from 'react'
12
+ import { View, StyleSheet, useWindowDimensions } from 'react-native'
13
+ import { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
14
+ import { ReanimatedView } from '../lib/jsx-native-bridge'
15
+ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
16
+
17
+ import type { VideoContentFit, VideoViewProps } from 'expo-video'
18
+ import { VideoView, isPictureInPictureSupported } from 'expo-video'
19
+
20
+ import type { MoviieVideoProps } from './moviie-video-props'
21
+ import { safeInvokeMoviieTelemetry } from '../lib/moviie-telemetry-callbacks'
22
+
23
+ import {
24
+ MOVIIE_PLAYBACK_PROFILE,
25
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_BACKGROUND_HEX,
26
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_OVERSCAN_PX,
27
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_VIDEO_CONTENT_FIT,
28
+ MOVIIE_VIDEO_DEFAULT_CONTENT_FIT,
29
+ } from '../constants'
30
+ import {
31
+ MOVIIE_SHELL_CROSSFADE_DURATION_MS,
32
+ MOVIIE_SHELL_DEFAULT_ASPECT_RATIO,
33
+ } from '../lib/moviie-shell-tokens'
34
+ import { partitionMoviieVideoHostStyle } from '../lib/partition-moviie-video-host-style'
35
+ import { computeCustomFullscreenStageDimensions } from '../lib/compute-custom-fullscreen-stage-dimensions'
36
+ import { buildMoviieVideoPresentationProps } from '../lib/build-moviie-video-presentation'
37
+ import type { MoviieCastState } from '../lib/moviie-cast-adapter'
38
+ import { getRegisteredCastAdapter } from '../lib/cast-adapter-registry'
39
+ import { warnOnce } from '../lib/warn-once'
40
+
41
+ import {
42
+ MoviieSkinChromeProvider,
43
+ MoviieSkinVideoOverlay,
44
+ type MoviieVideoSurfaceRef,
45
+ } from './controls/moviie-controls'
46
+ import {
47
+ MoviieSkinCustomFullscreenModal,
48
+ type MoviieSkinCustomFullscreenModalHandle,
49
+ } from './moviie-skin-custom-fullscreen-modal'
50
+ import { MoviiePlayerErrorShell } from './moviie-player-error-shell'
51
+ import { MoviiePlayerLoadingShell } from './moviie-player-loading-shell'
52
+
53
+ function assignForwardedRef<T>(forwarded: ForwardedRef<T>, instance: T | null): void {
54
+ if (typeof forwarded === 'function') {
55
+ forwarded(instance)
56
+ return
57
+ }
58
+ if (forwarded) {
59
+ forwarded.current = instance
60
+ }
61
+ }
62
+
63
+ export type { MoviieVideoProps } from './moviie-video-props'
64
+
65
+ type VideoHostProps = VideoViewProps & {
66
+ ref?: (instance: MoviieVideoSurfaceRef | null) => void
67
+ }
68
+
69
+ const HostVideoView = VideoView as unknown as ComponentType<VideoHostProps>
70
+
71
+ type ShellState = 'loading' | 'error' | 'ready'
72
+
73
+ const CROSSFADE_TIMING = {
74
+ duration: MOVIIE_SHELL_CROSSFADE_DURATION_MS,
75
+ easing: Easing.out(Easing.quad),
76
+ } as const
77
+
78
+ export const MoviieVideo = forwardRef<MoviieVideoSurfaceRef, MoviieVideoProps>(
79
+ function MoviieVideo(props, ref) {
80
+ return (
81
+ <SafeAreaProvider>
82
+ <MoviieVideoImpl ref={ref} {...props} />
83
+ </SafeAreaProvider>
84
+ )
85
+ }
86
+ )
87
+
88
+ const MoviieVideoImpl = forwardRef<MoviieVideoSurfaceRef, MoviieVideoProps>(
89
+ function MoviieVideoImpl(props, forwardedRef) {
90
+ const {
91
+ player,
92
+ nativeControls,
93
+ native = false,
94
+ playback = null,
95
+ controlsAutoHideMs,
96
+ controlsVisibleByDefault,
97
+ pictureInPicture,
98
+ pictureInPictureAutoStart,
99
+ allowsPictureInPicture: passthroughAllowsPictureInPicture,
100
+ startsPictureInPictureAutomatically: passthroughStartsPictureInPictureAutomatically,
101
+ requiresLinearPlayback: passthroughRequiresLinearPlayback,
102
+ onPictureInPictureStart: userOnPictureInPictureStart,
103
+ onPictureInPictureStop: userOnPictureInPictureStop,
104
+ children,
105
+ style,
106
+ contentFit,
107
+ error = null,
108
+ isLoading = false,
109
+ aspectRatio,
110
+ onRetry,
111
+ retry,
112
+ castAdapter: castAdapterProp,
113
+ cast,
114
+ onTelemetryError,
115
+ onTelemetryEvent,
116
+ ...videoRest
117
+ } = props
118
+
119
+ // Auto-detect: use explicit prop first, then fall back to side-effect registry
120
+ // (populated when user imports "@moviie/player-expo/cast" in their app entry).
121
+ const castAdapter = castAdapterProp ?? getRegisteredCastAdapter()
122
+
123
+ // Telemetria opt-in: dispara `onTelemetryError` quando o fetch de playback
124
+ // falha. Ref garante one-shot por instância de erro (evita duplicar em
125
+ // re-renders) e `safeInvokeMoviieTelemetry` engole exceções do consumer.
126
+ const lastReportedErrorRef = useRef<Error | null>(null)
127
+ useEffect(() => {
128
+ if (error == null) {
129
+ lastReportedErrorRef.current = null
130
+ return
131
+ }
132
+ if (lastReportedErrorRef.current === error) {
133
+ return
134
+ }
135
+ lastReportedErrorRef.current = error
136
+ safeInvokeMoviieTelemetry(onTelemetryError, error, {
137
+ phase: 'playback_fetch',
138
+ })
139
+ }, [error, onTelemetryError])
140
+
141
+ // Stub para futuras integrações: o callback de eventos não é disparado
142
+ // ainda neste ciclo, mas o handle está reservado para wiring posterior
143
+ // (play/pause/seek/ended). Mantém a API estável.
144
+ void onTelemetryEvent
145
+
146
+ const useMoviieChrome = !native
147
+
148
+ const shellState: ShellState = playback != null ? 'ready' : error != null ? 'error' : 'loading'
149
+
150
+ const loadingOpacity = useSharedValue(shellState === 'loading' ? 1 : 0)
151
+ const errorOpacity = useSharedValue(shellState === 'error' ? 1 : 0)
152
+ const playerOpacity = useSharedValue(shellState === 'ready' ? 1 : 0)
153
+
154
+ useEffect(() => {
155
+ loadingOpacity.value = withTiming(shellState === 'loading' ? 1 : 0, CROSSFADE_TIMING)
156
+ errorOpacity.value = withTiming(shellState === 'error' ? 1 : 0, CROSSFADE_TIMING)
157
+ playerOpacity.value = withTiming(shellState === 'ready' ? 1 : 0, CROSSFADE_TIMING)
158
+ }, [shellState, loadingOpacity, errorOpacity, playerOpacity])
159
+
160
+ const loadingAnimatedStyle = useAnimatedStyle(() => ({
161
+ opacity: loadingOpacity.value,
162
+ }))
163
+ const errorAnimatedStyle = useAnimatedStyle(() => ({
164
+ opacity: errorOpacity.value,
165
+ }))
166
+ const playerAnimatedStyle = useAnimatedStyle(() => ({
167
+ opacity: playerOpacity.value,
168
+ }))
169
+
170
+ const effectiveOnRetry = useMemo(() => {
171
+ if (onRetry) return onRetry
172
+ if (retry) return retry
173
+ return undefined
174
+ }, [onRetry, retry])
175
+
176
+ const nativePresentationVideoProps = useMemo(() => {
177
+ if (playback != null) {
178
+ const embedBaseline = buildMoviieVideoPresentationProps({
179
+ pictureInPicture,
180
+ pictureInPictureAutoStart,
181
+ profile: playback.profile,
182
+ showPip: playback.controls.showPip,
183
+ })
184
+ return {
185
+ allowsPictureInPicture:
186
+ passthroughAllowsPictureInPicture ?? embedBaseline.allowsPictureInPicture,
187
+ startsPictureInPictureAutomatically:
188
+ passthroughStartsPictureInPictureAutomatically ??
189
+ embedBaseline.startsPictureInPictureAutomatically,
190
+ requiresLinearPlayback:
191
+ passthroughRequiresLinearPlayback ?? embedBaseline.requiresLinearPlayback,
192
+ }
193
+ }
194
+ return {
195
+ allowsPictureInPicture: passthroughAllowsPictureInPicture ?? false,
196
+ startsPictureInPictureAutomatically:
197
+ passthroughStartsPictureInPictureAutomatically ?? false,
198
+ requiresLinearPlayback: passthroughRequiresLinearPlayback ?? false,
199
+ }
200
+ }, [
201
+ playback,
202
+ pictureInPicture,
203
+ pictureInPictureAutoStart,
204
+ passthroughAllowsPictureInPicture,
205
+ passthroughStartsPictureInPictureAutomatically,
206
+ passthroughRequiresLinearPlayback,
207
+ ])
208
+
209
+ const pipControlEnabled = useMemo(() => {
210
+ if (!playback || !useMoviieChrome) {
211
+ return false
212
+ }
213
+ return isPictureInPictureSupported() && nativePresentationVideoProps.allowsPictureInPicture
214
+ }, [playback, useMoviieChrome, nativePresentationVideoProps.allowsPictureInPicture])
215
+
216
+ const innerRef = useRef<MoviieVideoSurfaceRef | null>(null)
217
+ const fullscreenModalRef = useRef<MoviieSkinCustomFullscreenModalHandle | null>(null)
218
+ const [customFullscreen, setCustomFullscreen] = useState(false)
219
+ const [pipActive, setPipActive] = useState(false)
220
+ const [castState, setCastState] = useState<MoviieCastState>(() =>
221
+ castAdapter?.isAvailable() ? castAdapter.getState() : 'unavailable'
222
+ )
223
+
224
+ const castDesired = useMemo(() => {
225
+ if (!playback || !useMoviieChrome) return false
226
+ if (cast === false) return false
227
+ if (cast === true) return true
228
+ return playback.controls.chromecast === true
229
+ }, [cast, playback, useMoviieChrome])
230
+
231
+ const castControlEnabled = useMemo(() => {
232
+ if (!castDesired) return false
233
+ return castAdapter?.isAvailable() === true
234
+ }, [castAdapter, castDesired])
235
+
236
+ const castConnected = castState === 'connected'
237
+
238
+ const openCastPicker = useCallback(() => {
239
+ void castAdapter?.showDevicePicker().catch(() => {
240
+ /* native picker may be unavailable; UX falls back to no-op */
241
+ })
242
+ }, [castAdapter])
243
+
244
+ // Subscribe to cast state changes when adapter is provided.
245
+ useEffect(() => {
246
+ if (!castAdapter?.isAvailable()) {
247
+ setCastState('unavailable')
248
+ return
249
+ }
250
+ setCastState(castAdapter.getState())
251
+ const unsubscribe = castAdapter.subscribe((next) => {
252
+ setCastState(next)
253
+ })
254
+ return () => {
255
+ unsubscribe()
256
+ }
257
+ }, [castAdapter])
258
+
259
+ // Warn once when embed asks for cast but no adapter is wired.
260
+ useEffect(() => {
261
+ if (castDesired && (!castAdapter || !castAdapter.isAvailable())) {
262
+ warnOnce(
263
+ 'moviie-cast-missing-adapter',
264
+ '[@moviie/player-expo] O embed habilita Chromecast mas a lib não está disponível. Instale `react-native-google-cast` e adicione `import "@moviie/player-expo/cast"` no seu _layout.tsx (ver docs/player-expo/cast). Para desligar explicitamente, passe `cast={false}` em <MoviieVideo />.'
265
+ )
266
+ }
267
+ }, [castDesired, castAdapter])
268
+
269
+ // Hand off media to receiver when connected; pause local playback.
270
+ useEffect(() => {
271
+ if (!castAdapter?.isAvailable() || !playback || !player) return
272
+ if (castState !== 'connected') return
273
+ const startSeconds = player.currentTime ?? 0
274
+ void castAdapter
275
+ .loadMedia({
276
+ uri: playback.playback.uri,
277
+ title: playback.title,
278
+ posterUrl: playback.posterUrl,
279
+ durationSeconds: playback.durationSeconds ?? null,
280
+ startSeconds,
281
+ })
282
+ .then(() => {
283
+ try {
284
+ player.pause()
285
+ } catch {
286
+ /* player may be in an unrecoverable state */
287
+ }
288
+ })
289
+ .catch(() => {
290
+ /* loadMedia failure — keep local player running */
291
+ })
292
+ }, [castAdapter, castState, playback, player])
293
+
294
+ const { width: winW, height: winH } = useWindowDimensions()
295
+ const safeAreaInsets = useSafeAreaInsets()
296
+
297
+ const fullscreenStageLayout = useMemo(
298
+ () =>
299
+ computeCustomFullscreenStageDimensions({
300
+ windowHeight: winH,
301
+ safeInsets: safeAreaInsets,
302
+ stageOverscanPx: MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_OVERSCAN_PX,
303
+ windowWidth: winW,
304
+ }),
305
+ [
306
+ winW,
307
+ winH,
308
+ safeAreaInsets.bottom,
309
+ safeAreaInsets.left,
310
+ safeAreaInsets.right,
311
+ safeAreaInsets.top,
312
+ ]
313
+ )
314
+
315
+ useEffect(() => {
316
+ setPipActive(false)
317
+ }, [playback?.embedId])
318
+
319
+ const handleNativePictureInPictureStart = useCallback(() => {
320
+ if (useMoviieChrome && playback != null) {
321
+ setPipActive(true)
322
+ }
323
+ userOnPictureInPictureStart?.()
324
+ }, [playback, useMoviieChrome, userOnPictureInPictureStart])
325
+
326
+ const handleNativePictureInPictureStop = useCallback(() => {
327
+ if (useMoviieChrome && playback != null) {
328
+ setPipActive(false)
329
+ }
330
+ userOnPictureInPictureStop?.()
331
+ }, [playback, useMoviieChrome, userOnPictureInPictureStop])
332
+
333
+ const togglePictureInPicture = useCallback(async () => {
334
+ const surface = innerRef.current
335
+ if (!surface?.startPictureInPicture) {
336
+ return
337
+ }
338
+ try {
339
+ if (pipActive) {
340
+ await surface.stopPictureInPicture?.()
341
+ } else {
342
+ await surface.startPictureInPicture()
343
+ }
344
+ } catch {
345
+ /* PiP may be unavailable on device or misconfigured */
346
+ }
347
+ }, [pipActive])
348
+
349
+ const requestExitCustomFullscreenAnimated = useCallback(() => {
350
+ fullscreenModalRef.current?.animateExit()
351
+ }, [])
352
+
353
+ const setRefs = useCallback(
354
+ (node: MoviieVideoSurfaceRef | null) => {
355
+ innerRef.current = node
356
+ assignForwardedRef(forwardedRef, node)
357
+ },
358
+ [forwardedRef]
359
+ )
360
+
361
+ const resolvedNativeControlsDefault = native
362
+ ? playback?.profile === MOVIIE_PLAYBACK_PROFILE.VSL
363
+ ? false
364
+ : true
365
+ : false
366
+
367
+ const effectiveNativeControls = nativeControls ?? resolvedNativeControlsDefault
368
+
369
+ const intrinsicAspectRatio =
370
+ playback?.videoWidthPx != null &&
371
+ playback?.videoHeightPx != null &&
372
+ playback.videoWidthPx > 0 &&
373
+ playback.videoHeightPx > 0
374
+ ? playback.videoWidthPx / playback.videoHeightPx
375
+ : null
376
+
377
+ const effectiveAspectRatio =
378
+ intrinsicAspectRatio ?? aspectRatio ?? MOVIIE_SHELL_DEFAULT_ASPECT_RATIO
379
+
380
+ const effectiveContentFit: VideoContentFit | undefined =
381
+ contentFit ??
382
+ (playback != null
383
+ ? playback.isVertical
384
+ ? MOVIIE_VIDEO_DEFAULT_CONTENT_FIT.VERTICAL
385
+ : MOVIIE_VIDEO_DEFAULT_CONTENT_FIT.HORIZONTAL
386
+ : undefined)
387
+
388
+ const { columnStyle, surfaceStyle } = partitionMoviieVideoHostStyle(StyleSheet.flatten(style))
389
+
390
+ const stageContainerStyle = [
391
+ styles.videoStage,
392
+ {
393
+ width: '100%' as const,
394
+ aspectRatio: effectiveAspectRatio,
395
+ alignSelf: 'stretch' as const,
396
+ flex: 0 as const,
397
+ },
398
+ surfaceStyle,
399
+ useMoviieChrome && playback ? styles.videoStageClipNone : null,
400
+ ]
401
+
402
+ const inlineVideoAndOverlay =
403
+ player != null ? (
404
+ useMoviieChrome && playback ? (
405
+ <>
406
+ {createElement(HostVideoView, {
407
+ ...videoRest,
408
+ ...nativePresentationVideoProps,
409
+ ref: setRefs,
410
+ player,
411
+ nativeControls: effectiveNativeControls,
412
+ onPictureInPictureStart: handleNativePictureInPictureStart,
413
+ onPictureInPictureStop: handleNativePictureInPictureStop,
414
+ ...(effectiveContentFit != null ? { contentFit: effectiveContentFit } : {}),
415
+ style: styles.videoFill,
416
+ })}
417
+ <MoviieSkinVideoOverlay />
418
+ </>
419
+ ) : (
420
+ createElement(HostVideoView, {
421
+ ...videoRest,
422
+ ...nativePresentationVideoProps,
423
+ ref: setRefs,
424
+ player,
425
+ nativeControls: effectiveNativeControls,
426
+ onPictureInPictureStart: handleNativePictureInPictureStart,
427
+ onPictureInPictureStop: handleNativePictureInPictureStop,
428
+ ...(effectiveContentFit != null ? { contentFit: effectiveContentFit } : {}),
429
+ style: styles.videoFill,
430
+ })
431
+ )
432
+ ) : null
433
+
434
+ const renderInlineStage = (active: boolean) => {
435
+ if (!playback || !useMoviieChrome) {
436
+ return inlineVideoAndOverlay
437
+ }
438
+ if (active && !customFullscreen) {
439
+ return inlineVideoAndOverlay
440
+ }
441
+ return (
442
+ <View
443
+ style={[
444
+ styles.videoFill,
445
+ {
446
+ backgroundColor: MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_BACKGROUND_HEX,
447
+ },
448
+ ]}
449
+ />
450
+ )
451
+ }
452
+
453
+ const stageContent = (
454
+ <View style={stageContainerStyle}>
455
+ <ReanimatedView
456
+ style={[StyleSheet.absoluteFillObject, loadingAnimatedStyle, styles.shellLayer]}
457
+ pointerEvents={shellState === 'loading' ? 'auto' : 'none'}
458
+ >
459
+ {shellState === 'loading' || loadingOpacity.value > 0 ? (
460
+ <MoviiePlayerLoadingShell
461
+ posterUrl={playback?.posterUrl ?? null}
462
+ primaryColor={playback?.branding.primaryColor ?? null}
463
+ />
464
+ ) : null}
465
+ </ReanimatedView>
466
+ <ReanimatedView
467
+ style={[StyleSheet.absoluteFillObject, errorAnimatedStyle, styles.shellLayer]}
468
+ pointerEvents={shellState === 'error' ? 'auto' : 'none'}
469
+ >
470
+ {error ? <MoviiePlayerErrorShell error={error} onRetry={effectiveOnRetry} /> : null}
471
+ </ReanimatedView>
472
+ <ReanimatedView
473
+ style={[StyleSheet.absoluteFillObject, playerAnimatedStyle]}
474
+ pointerEvents={shellState === 'ready' ? 'auto' : 'none'}
475
+ >
476
+ {renderInlineStage(true)}
477
+ </ReanimatedView>
478
+ </View>
479
+ )
480
+
481
+ if (useMoviieChrome && playback && player) {
482
+ return (
483
+ <MoviieSkinChromeProvider
484
+ autoHideMs={controlsAutoHideMs}
485
+ customFullscreen={customFullscreen}
486
+ pictureInPictureActive={pipActive}
487
+ pictureInPictureControlEnabled={pipControlEnabled}
488
+ togglePictureInPicture={togglePictureInPicture}
489
+ castControlEnabled={castControlEnabled}
490
+ castConnected={castConnected}
491
+ openCastPicker={openCastPicker}
492
+ onRequestEnterCustomFullscreen={() => setCustomFullscreen(true)}
493
+ onRequestExitCustomFullscreenAnimated={requestExitCustomFullscreenAnimated}
494
+ playback={playback}
495
+ player={player}
496
+ skinLayoutMetricsWidth={
497
+ customFullscreen ? fullscreenStageLayout.layoutMetricsBasisWidth : undefined
498
+ }
499
+ videoViewRef={innerRef}
500
+ visibleByDefault={controlsVisibleByDefault}
501
+ >
502
+ <>
503
+ <View style={[styles.column, columnStyle]}>
504
+ {stageContent}
505
+ {children}
506
+ </View>
507
+ <MoviieSkinCustomFullscreenModal
508
+ ref={fullscreenModalRef}
509
+ visible={customFullscreen}
510
+ onExitComplete={() => setCustomFullscreen(false)}
511
+ >
512
+ <View style={styles.fullscreenStage}>
513
+ {createElement(HostVideoView, {
514
+ ...videoRest,
515
+ ...nativePresentationVideoProps,
516
+ ref: setRefs,
517
+ player,
518
+ nativeControls: effectiveNativeControls,
519
+ onPictureInPictureStart: handleNativePictureInPictureStart,
520
+ onPictureInPictureStop: handleNativePictureInPictureStop,
521
+ contentFit: MOVIIE_SKIN_CUSTOM_FULLSCREEN_VIDEO_CONTENT_FIT,
522
+ style: StyleSheet.absoluteFillObject,
523
+ })}
524
+ <MoviieSkinVideoOverlay />
525
+ </View>
526
+ </MoviieSkinCustomFullscreenModal>
527
+ </>
528
+ </MoviieSkinChromeProvider>
529
+ )
530
+ }
531
+
532
+ return (
533
+ <View style={[styles.column, columnStyle]}>
534
+ {stageContent}
535
+ {children}
536
+ {castAdapter?.renderAndroidProxy?.()}
537
+ </View>
538
+ )
539
+ }
540
+ )
541
+
542
+ const styles = StyleSheet.create({
543
+ column: {
544
+ alignSelf: 'stretch',
545
+ width: '100%',
546
+ },
547
+ videoStage: {
548
+ overflow: 'hidden',
549
+ position: 'relative',
550
+ width: '100%',
551
+ },
552
+ videoStageClipNone: {
553
+ overflow: 'visible',
554
+ },
555
+ videoFill: {
556
+ flex: 1,
557
+ width: '100%',
558
+ alignSelf: 'stretch',
559
+ },
560
+ fullscreenStage: {
561
+ flex: 1,
562
+ position: 'relative',
563
+ width: '100%',
564
+ },
565
+ shellLayer: {
566
+ zIndex: 1,
567
+ },
568
+ })