@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,232 @@
1
+ import {
2
+ forwardRef,
3
+ type ReactNode,
4
+ useCallback,
5
+ useEffect,
6
+ useImperativeHandle,
7
+ useMemo,
8
+ useRef,
9
+ } from 'react'
10
+ import { Modal, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
11
+ import Animated, {
12
+ runOnJS,
13
+ useAnimatedStyle,
14
+ useSharedValue,
15
+ withTiming,
16
+ } from 'react-native-reanimated'
17
+ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
18
+ import { moviieSetStatusBarHidden } from '../lib/optional-status-bar'
19
+
20
+ import {
21
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_ENTER_SCALE_START,
22
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_ROTATE_Z_DEGREES,
23
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_BACKGROUND_HEX,
24
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_OVERSCAN_PX,
25
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_TRANSITION_MS,
26
+ } from '../constants'
27
+ import { computeCustomFullscreenStageDimensions } from '../lib/compute-custom-fullscreen-stage-dimensions'
28
+ import { subscribeCustomFullscreenNativeOrientation } from '../lib/custom-fullscreen-native-orientation'
29
+ import { ReanimatedView } from '../lib/jsx-native-bridge'
30
+ import { resolveOrientationLockValueForRotateZDeg } from '../lib/resolve-orientation-lock-for-rotate-z-deg'
31
+
32
+ const CUSTOM_FULLSCREEN_TIMING = {
33
+ duration: MOVIIE_SKIN_CUSTOM_FULLSCREEN_TRANSITION_MS,
34
+ } as const
35
+
36
+ /**
37
+ * Em iOS/Android o fullscreen usa `expo-screen-orientation`: o referencial da janela passa a landscape.
38
+ * Rotação Z adicional somaria com a orientação nativa e invertia o resultado.
39
+ * Na web mantemos rotação só em JS (sem lock nativo fiável no nosso fluxo).
40
+ */
41
+ const CUSTOM_FULLSCREEN_USE_JS_STAGE_ROTATION = Platform.OS === 'web'
42
+
43
+ export type MoviieSkinCustomFullscreenModalHandle = {
44
+ animateExit: () => void
45
+ }
46
+
47
+ export type MoviieSkinCustomFullscreenModalProps = {
48
+ visible: boolean
49
+ onExitComplete: () => void
50
+ children: ReactNode
51
+ }
52
+
53
+ const MoviieSkinCustomFullscreenModalInner = forwardRef<
54
+ MoviieSkinCustomFullscreenModalHandle,
55
+ MoviieSkinCustomFullscreenModalProps
56
+ >(function MoviieSkinCustomFullscreenModalInner(props, ref) {
57
+ const orientationFinishRef = useRef<(() => Promise<void>) | null>(null)
58
+
59
+ const insets = useSafeAreaInsets()
60
+ const { height: winH, width: winW } = useWindowDimensions()
61
+
62
+ const { longSide, shortSide } = useMemo(
63
+ () =>
64
+ computeCustomFullscreenStageDimensions({
65
+ windowHeight: winH,
66
+ safeInsets: insets,
67
+ stageOverscanPx: MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_OVERSCAN_PX,
68
+ windowWidth: winW,
69
+ }),
70
+ [winW, winH, insets.bottom, insets.left, insets.right, insets.top]
71
+ )
72
+
73
+ const rotateZDeg = useSharedValue(0)
74
+ const scale = useSharedValue(1)
75
+
76
+ const emitExitComplete = useCallback(() => {
77
+ props.onExitComplete()
78
+ }, [props.onExitComplete])
79
+
80
+ const completeExitAfterNativeOrientationRestore = useCallback(() => {
81
+ void (async () => {
82
+ await orientationFinishRef.current?.()
83
+ emitExitComplete()
84
+ })()
85
+ }, [emitExitComplete])
86
+
87
+ const animateExit = useCallback(() => {
88
+ scale.value = withTiming(
89
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_ENTER_SCALE_START,
90
+ CUSTOM_FULLSCREEN_TIMING,
91
+ (finishedScale) => {
92
+ if (!CUSTOM_FULLSCREEN_USE_JS_STAGE_ROTATION && finishedScale) {
93
+ runOnJS(completeExitAfterNativeOrientationRestore)()
94
+ }
95
+ }
96
+ )
97
+
98
+ if (CUSTOM_FULLSCREEN_USE_JS_STAGE_ROTATION) {
99
+ rotateZDeg.value = withTiming(0, CUSTOM_FULLSCREEN_TIMING, (finished) => {
100
+ if (finished) {
101
+ runOnJS(emitExitComplete)()
102
+ }
103
+ })
104
+ return
105
+ }
106
+
107
+ rotateZDeg.value = 0
108
+ }, [completeExitAfterNativeOrientationRestore, emitExitComplete, rotateZDeg, scale])
109
+
110
+ useImperativeHandle(ref, () => ({ animateExit }), [animateExit])
111
+
112
+ useEffect(() => {
113
+ if (!props.visible) {
114
+ return
115
+ }
116
+ rotateZDeg.value = 0
117
+ scale.value = MOVIIE_SKIN_CUSTOM_FULLSCREEN_ENTER_SCALE_START
118
+ if (CUSTOM_FULLSCREEN_USE_JS_STAGE_ROTATION) {
119
+ rotateZDeg.value = withTiming(
120
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_ROTATE_Z_DEGREES,
121
+ CUSTOM_FULLSCREEN_TIMING
122
+ )
123
+ }
124
+ scale.value = withTiming(1, CUSTOM_FULLSCREEN_TIMING)
125
+ }, [props.visible, rotateZDeg, scale])
126
+
127
+ useEffect(() => {
128
+ if (!props.visible) {
129
+ return
130
+ }
131
+ moviieSetStatusBarHidden(true, 'fade')
132
+ return () => {
133
+ moviieSetStatusBarHidden(false, 'fade')
134
+ }
135
+ }, [props.visible])
136
+
137
+ useEffect(() => {
138
+ if (!props.visible || CUSTOM_FULLSCREEN_USE_JS_STAGE_ROTATION) {
139
+ orientationFinishRef.current = null
140
+ return
141
+ }
142
+
143
+ const targetLock = resolveOrientationLockValueForRotateZDeg(
144
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_ROTATE_Z_DEGREES
145
+ )
146
+ const sub = subscribeCustomFullscreenNativeOrientation(targetLock)
147
+ orientationFinishRef.current = () => sub.finish()
148
+
149
+ return () => {
150
+ orientationFinishRef.current = null
151
+ void sub.finish()
152
+ }
153
+ }, [props.visible])
154
+
155
+ const animatedStyle = useAnimatedStyle(() => {
156
+ if (CUSTOM_FULLSCREEN_USE_JS_STAGE_ROTATION) {
157
+ return {
158
+ transform: [{ rotateZ: `${rotateZDeg.value}deg` }, { scale: scale.value }],
159
+ }
160
+ }
161
+
162
+ return {
163
+ transform: [{ scale: scale.value }],
164
+ }
165
+ })
166
+
167
+ const stageStaticStyle = useMemo(
168
+ () => ({
169
+ height: shortSide,
170
+ width: longSide,
171
+ }),
172
+ [longSide, shortSide]
173
+ )
174
+
175
+ return (
176
+ <View
177
+ style={[styles.root, { backgroundColor: MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_BACKGROUND_HEX }]}
178
+ >
179
+ <View style={styles.centerSheet}>
180
+ <ReanimatedView style={[styles.rotatingStage, stageStaticStyle, animatedStyle]}>
181
+ {props.children}
182
+ </ReanimatedView>
183
+ </View>
184
+ </View>
185
+ )
186
+ })
187
+
188
+ export const MoviieSkinCustomFullscreenModal = forwardRef<
189
+ MoviieSkinCustomFullscreenModalHandle,
190
+ MoviieSkinCustomFullscreenModalProps
191
+ >(function MoviieSkinCustomFullscreenModal(props, ref) {
192
+ return (
193
+ <Modal
194
+ animationType="none"
195
+ hardwareAccelerated={Platform.OS === 'android'}
196
+ onRequestClose={() => {
197
+ if (ref != null && typeof ref !== 'function') {
198
+ ref.current?.animateExit()
199
+ }
200
+ }}
201
+ presentationStyle="fullScreen"
202
+ statusBarTranslucent={Platform.OS === 'android'}
203
+ supportedOrientations={[
204
+ 'portrait',
205
+ 'portrait-upside-down',
206
+ 'landscape',
207
+ 'landscape-left',
208
+ 'landscape-right',
209
+ ]}
210
+ visible={props.visible}
211
+ >
212
+ <SafeAreaProvider>
213
+ <MoviieSkinCustomFullscreenModalInner ref={ref} {...props} />
214
+ </SafeAreaProvider>
215
+ </Modal>
216
+ )
217
+ })
218
+
219
+ const styles = StyleSheet.create({
220
+ root: {
221
+ flex: 1,
222
+ },
223
+ centerSheet: {
224
+ alignItems: 'center',
225
+ flex: 1,
226
+ justifyContent: 'center',
227
+ overflow: 'visible',
228
+ },
229
+ rotatingStage: {
230
+ position: 'relative',
231
+ },
232
+ })
@@ -0,0 +1,134 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import type { VideoPlayer, VideoViewProps } from 'expo-video'
4
+
5
+ import type { MoviiePlaybackData } from '@moviie/player-sdk'
6
+
7
+ import type { MoviieCastAdapter } from '../lib/moviie-cast-adapter'
8
+ import type {
9
+ MoviieTelemetryOnError,
10
+ MoviieTelemetryOnEvent,
11
+ } from '../lib/moviie-telemetry-callbacks'
12
+
13
+ /**
14
+ * Subconjunto do elemento iframe usado na integração web (embed watch).
15
+ * Evita acoplar o pacote à lib DOM completa no tsconfig.
16
+ */
17
+ export type MoviieWatchEmbedIframeHandle = {
18
+ readonly contentWindow: {
19
+ postMessage(message: unknown, targetOrigin: string): void
20
+ } | null
21
+ }
22
+
23
+ export type MoviieWatchEmbedIframeRefCallback = (
24
+ iframe: MoviieWatchEmbedIframeHandle | null
25
+ ) => void
26
+
27
+ export type MoviieVideoProps = Omit<VideoViewProps, 'player'> & {
28
+ /**
29
+ * Instância `VideoPlayer` do expo-video (iOS / Android).
30
+ * Na web, `useMoviiePlayer` devolve `null` e `MoviieVideo` usa iframe do embed.
31
+ */
32
+ player: VideoPlayer | null
33
+ nativeControls?: boolean
34
+ /**
35
+ * When `true`, uses the platform `VideoView` chrome. When `false` or omitted, uses the Moviie custom chrome (default).
36
+ */
37
+ native?: boolean
38
+ playback?: MoviiePlaybackData | null
39
+ /**
40
+ * Optional override: when set, replaces embed `showPip` for PiP capability (with {@link pictureInPictureAutoStart}).
41
+ * Raw `VideoView` props (`allowsPictureInPicture`, …) still win when passed explicitly.
42
+ */
43
+ pictureInPicture?: boolean
44
+ /**
45
+ * Optional override for auto-start PiP. Embed has no field for this; omit for `false` unless set here or on the hook.
46
+ */
47
+ pictureInPictureAutoStart?: boolean
48
+ /**
49
+ * Ignorado quando `native` é omitido ou `false`: o chrome Moviie não usa temporizador para ocultar.
50
+ */
51
+ controlsAutoHideMs?: number
52
+ controlsVisibleByDefault?: boolean
53
+ children?: ReactNode
54
+ /**
55
+ * Web: callback com referência ao iframe após montagem, para `Moviie.getPlayer(iframe)` (`embed.js`).
56
+ */
57
+ onEmbedIframeRef?: MoviieWatchEmbedIframeRefCallback
58
+ /**
59
+ * Web: injeta `<script src="{watchOrigin}/embed.js">` uma vez. Ignorado em nativo. Default `true`.
60
+ */
61
+ loadEmbedScript?: boolean
62
+ /**
63
+ * Erro vindo de `useMoviiePlayer`/`useMoviiePlayback`. Quando preenchido (e sem `playback`),
64
+ * a shell built-in mostra feedback de erro com CTA mapeado por código.
65
+ */
66
+ error?: Error | null
67
+ /**
68
+ * Indica que o fetch de playback está em curso. Quando `true` (e sem `playback`), a shell
69
+ * built-in mostra estado de carregamento.
70
+ */
71
+ isLoading?: boolean
72
+ /**
73
+ * Aspect ratio do container reservado pelo player enquanto não há dimensões intrínsecas do
74
+ * vídeo. Default `16/9`. Após `playback` chegar, o container passa a usar `videoWidthPx/videoHeightPx`.
75
+ */
76
+ aspectRatio?: number
77
+ /**
78
+ * Callback opcional para erros transientes (network/rate_limit). Override do retry built-in.
79
+ * Tem precedência sobre {@link retry}.
80
+ */
81
+ onRetry?: () => void
82
+ /**
83
+ * Função de retry built-in fornecida por `useMoviiePlayer().retry`. Aceita o spread do hook
84
+ * sem boilerplate. Quando `onRetry` é fornecido, este é ignorado.
85
+ */
86
+ retry?: () => void
87
+ /**
88
+ * Adapter opcional para cast (Chromecast). Crie via factory do subpath
89
+ * `@moviie/player-expo/cast` (`createGoogleCastAdapter()`) e passe aqui.
90
+ *
91
+ * Quando omitido, o botão de cast não aparece. Se o embed pede cast
92
+ * (`playback.controls.chromecast === true`) e o adapter não foi fornecido,
93
+ * dispara um `console.warn` único com instruções.
94
+ */
95
+ castAdapter?: MoviieCastAdapter | null
96
+ /**
97
+ * Override explícito do flag `playback.controls.chromecast`. Quando definido,
98
+ * vence o painel:
99
+ * - `true`: força exibir botão (se adapter estiver disponível);
100
+ * - `false`: força esconder e silencia warning;
101
+ * - omitido: usa o flag do painel.
102
+ */
103
+ cast?: boolean
104
+ /**
105
+ * Callback opt-in invocado quando um erro relevante ocorre — para integração
106
+ * com Sentry/PostHog/Datadog do consumer.
107
+ *
108
+ * Não substitui nem expande a telemetria interna enviada ao backend Moviie.
109
+ * Exceções lançadas dentro do callback são engolidas e logged via warn.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * <MoviieVideo
114
+ * {...moviie}
115
+ * onTelemetryError={(error, ctx) => Sentry.captureException(error, { tags: ctx })}
116
+ * />
117
+ * ```
118
+ */
119
+ onTelemetryError?: MoviieTelemetryOnError
120
+ /**
121
+ * Callback opt-in invocado em eventos de ciclo de vida do player
122
+ * (play, pause, seek, fullscreen, cast). Útil para correlacionar UX com erros
123
+ * em ferramentas de observability do consumer.
124
+ *
125
+ * @example
126
+ * ```tsx
127
+ * <MoviieVideo
128
+ * {...moviie}
129
+ * onTelemetryEvent={(e) => posthog.capture(`player.${e.name}`, e)}
130
+ * />
131
+ * ```
132
+ */
133
+ onTelemetryEvent?: MoviieTelemetryOnEvent
134
+ }