@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.
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/app.plugin.cjs +3 -0
- package/dist/cast.cjs +100 -0
- package/dist/cast.cjs.map +1 -0
- package/dist/cast.d.cts +15 -0
- package/dist/cast.d.ts +15 -0
- package/dist/cast.mjs +92 -0
- package/dist/cast.mjs.map +1 -0
- package/dist/chunk-67DJ7NOB.mjs +294 -0
- package/dist/chunk-67DJ7NOB.mjs.map +1 -0
- package/dist/chunk-7U2LKIGU.mjs +12 -0
- package/dist/chunk-7U2LKIGU.mjs.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +10 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/index.cjs +3934 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +450 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.mjs +3571 -0
- package/dist/index.mjs.map +1 -0
- package/dist/layout.cjs +217 -0
- package/dist/layout.cjs.map +1 -0
- package/dist/layout.d.cts +20 -0
- package/dist/layout.d.ts +20 -0
- package/dist/layout.mjs +4 -0
- package/dist/layout.mjs.map +1 -0
- package/dist/moviie-cast-adapter-DmSU2u3j.d.cts +53 -0
- package/dist/moviie-cast-adapter-DmSU2u3j.d.ts +53 -0
- package/dist/plugin/with-moviie.cjs +88 -0
- package/dist/plugin/with-moviie.cjs.map +1 -0
- package/dist/plugin/with-moviie.d.cts +52 -0
- package/dist/plugin/with-moviie.d.ts +52 -0
- package/dist/plugin/with-moviie.mjs +76 -0
- package/dist/plugin/with-moviie.mjs.map +1 -0
- package/package.json +134 -0
- package/plugin/validate-options.ts +31 -0
- package/plugin/with-moviie-types.ts +21 -0
- package/plugin/with-moviie.ts +84 -0
- package/src/apply-expo-moviie-endpoints.ts +47 -0
- package/src/cast/google-cast-adapter.ts +111 -0
- package/src/cast/index.ts +12 -0
- package/src/components/controls/moviie-bottom-timeline.tsx +477 -0
- package/src/components/controls/moviie-cast-buttons.tsx +96 -0
- package/src/components/controls/moviie-chrome-edge-gradients.tsx +162 -0
- package/src/components/controls/moviie-controls.tsx +585 -0
- package/src/components/controls/moviie-skin-chrome-context.tsx +374 -0
- package/src/components/controls/moviie-skin-smart-progress.tsx +157 -0
- package/src/components/icons/embed-media-icons.tsx +282 -0
- package/src/components/icons/moviie-embed-brand-mark.tsx +58 -0
- package/src/components/moviie-error-boundary.tsx +80 -0
- package/src/components/moviie-player-error-shell.tsx +143 -0
- package/src/components/moviie-player-loading-shell.tsx +59 -0
- package/src/components/moviie-skin-custom-fullscreen-modal.tsx +232 -0
- package/src/components/moviie-video-props.ts +134 -0
- package/src/components/moviie-video.tsx +568 -0
- package/src/components/moviie-video.web.tsx +167 -0
- package/src/components/overlays/moviie-watermark.tsx +53 -0
- package/src/constants.ts +374 -0
- package/src/hooks/use-moviie-event.ts +103 -0
- package/src/hooks/use-moviie-playback-ended.ts +14 -0
- package/src/hooks/use-moviie-playback-resume-persistence.ts +76 -0
- package/src/hooks/use-moviie-playback.ts +99 -0
- package/src/hooks/use-moviie-player-types.ts +55 -0
- package/src/hooks/use-moviie-player.ts +236 -0
- package/src/hooks/use-moviie-player.web.ts +57 -0
- package/src/hooks/use-moviie-telemetry.ts +133 -0
- package/src/index.ts +90 -0
- package/src/layout.ts +4 -0
- package/src/lib/add-moviie-playback-ended-listener.ts +16 -0
- package/src/lib/build-moviie-branding-marketing-url.ts +14 -0
- package/src/lib/build-moviie-embed-brand-home-url.ts +16 -0
- package/src/lib/build-moviie-skin-layout-metrics.ts +166 -0
- package/src/lib/build-moviie-video-presentation.ts +42 -0
- package/src/lib/build-moviie-watch-embed-url.ts +30 -0
- package/src/lib/cast-adapter-registry.ts +13 -0
- package/src/lib/clamp-unit-interval.ts +3 -0
- package/src/lib/compute-custom-fullscreen-stage-dimensions.ts +39 -0
- package/src/lib/compute-moviie-playback-ended.ts +31 -0
- package/src/lib/compute-playback-buffering.ts +41 -0
- package/src/lib/compute-playback-progress-ratio.ts +33 -0
- package/src/lib/compute-timeline-scrub-commit-time.ts +40 -0
- package/src/lib/custom-fullscreen-native-orientation.ts +88 -0
- package/src/lib/format-playback-clock.ts +27 -0
- package/src/lib/jsx-native-bridge.ts +45 -0
- package/src/lib/map-smart-progress-display-ratio.ts +43 -0
- package/src/lib/moviie-cast-adapter.ts +53 -0
- package/src/lib/moviie-error-display.ts +149 -0
- package/src/lib/moviie-shell-tokens.ts +27 -0
- package/src/lib/moviie-telemetry-callbacks.ts +76 -0
- package/src/lib/optional-status-bar.ts +48 -0
- package/src/lib/partition-moviie-video-host-style.ts +54 -0
- package/src/lib/remember-playback-position-storage.ts +98 -0
- package/src/lib/resolve-moviie-skin-layout-scale.ts +17 -0
- package/src/lib/resolve-orientation-lock-for-rotate-z-deg.ts +21 -0
- package/src/lib/resolve-skin-accent-color.ts +10 -0
- package/src/lib/resolve-web-embed-iframe-src.ts +17 -0
- package/src/lib/warn-once.ts +23 -0
- package/src/platform/native-application-id.ts +10 -0
- package/src/platform/platform-client-info.ts +31 -0
- package/src/playback/fetch-playback-fresh.ts +27 -0
- package/src/playback/playback-memory-cache.ts +52 -0
- package/src/provider/moviie-provider.tsx +99 -0
- 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
|
+
})
|