@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,585 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useCallback, useMemo } from 'react'
3
+ import type { PressableProps } from 'react-native'
4
+ import {
5
+ ActivityIndicator,
6
+ Animated,
7
+ Pressable,
8
+ StyleSheet,
9
+ Text,
10
+ View,
11
+ type ViewStyle,
12
+ } from 'react-native'
13
+
14
+ import {
15
+ MOVIIE_SKIN_CHROME_CONTROL_PRESSED_OPACITY,
16
+ MOVIIE_SKIN_CHROME_CONTROL_REST_OPACITY,
17
+ MOVIIE_SKIN_CHROME_FOREGROUND_HEX,
18
+ MOVIIE_SKIN_CHROME_SCRIM_BLACK_RGBA,
19
+ MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX,
20
+ MOVIIE_SKIN_TOP_ICON_FULLSCREEN_VISUAL_SCALE,
21
+ MOVIIE_SKIN_TOP_ICON_PIP_VISUAL_SCALE,
22
+ } from '../../constants'
23
+ import type { MoviieSkinLayoutMetrics } from '../../lib/build-moviie-skin-layout-metrics'
24
+ import { computeMoviiePlaybackEnded } from '../../lib/compute-moviie-playback-ended'
25
+ import { formatPlaybackClock } from '../../lib/format-playback-clock'
26
+ import { AnimatedView } from '../../lib/jsx-native-bridge'
27
+ import {
28
+ EmbedMediaIconFullscreen,
29
+ EmbedMediaIconFullscreenExit,
30
+ EmbedMediaIconPause,
31
+ EmbedMediaIconPictureInPicture,
32
+ EmbedMediaIconPlay,
33
+ EmbedMediaIconReplay,
34
+ EmbedMediaIconSeekBackward10,
35
+ EmbedMediaIconSeekForward10,
36
+ } from '../icons/embed-media-icons'
37
+ import { MoviieEmbedBrandMark } from '../icons/moviie-embed-brand-mark'
38
+
39
+ import { MoviieSkinCastCircleButton, MoviieSkinCastTopBareButton } from './moviie-cast-buttons'
40
+ import { MoviieSkinChromeEdgeGradients } from './moviie-chrome-edge-gradients'
41
+ import { MoviieSkinChromeProvider, useMoviieSkinChrome } from './moviie-skin-chrome-context'
42
+ import { MoviieSkinBottomTimeline } from './moviie-bottom-timeline'
43
+ import { MoviieSkinSmartProgress } from './moviie-skin-smart-progress'
44
+
45
+ export type { MoviieVideoSurfaceRef } from './moviie-skin-chrome-context'
46
+
47
+ export { MoviieSkinChromeProvider }
48
+
49
+ export { useMoviieSkinChrome } from './moviie-skin-chrome-context'
50
+
51
+ function resolveChromeControlPressOpacityStyle(pressed: boolean): Pick<ViewStyle, 'opacity'> {
52
+ return {
53
+ opacity: pressed
54
+ ? MOVIIE_SKIN_CHROME_CONTROL_PRESSED_OPACITY
55
+ : MOVIIE_SKIN_CHROME_CONTROL_REST_OPACITY,
56
+ }
57
+ }
58
+
59
+ function createMoviieSkinOverlayStyles(layout: MoviieSkinLayoutMetrics) {
60
+ return StyleSheet.create({
61
+ chromeScrimBase: {
62
+ backgroundColor: MOVIIE_SKIN_CHROME_SCRIM_BLACK_RGBA,
63
+ },
64
+ floatingHud: {
65
+ bottom: 0,
66
+ left: 0,
67
+ pointerEvents: 'box-none',
68
+ position: 'absolute',
69
+ right: 0,
70
+ },
71
+ floatingHudInner: {
72
+ alignItems: 'center',
73
+ flexDirection: 'row',
74
+ justifyContent: 'space-between',
75
+ paddingBottom: layout.floatingHudAboveTimelineGapPx,
76
+ paddingHorizontal: layout.hudEdgeInsetPx,
77
+ },
78
+ floatingLeftSpacer: {
79
+ flex: 1,
80
+ },
81
+ floatingRight: {
82
+ alignItems: 'center',
83
+ columnGap: layout.floatingHudButtonGapPx,
84
+ flexDirection: 'row',
85
+ flexShrink: 0,
86
+ },
87
+ clockScrim: {
88
+ borderRadius: layout.hudClockCornerRadiusPx,
89
+ overflow: 'hidden',
90
+ paddingHorizontal: layout.hudEdgeInsetPx,
91
+ paddingVertical: layout.hudEdgeInsetPx,
92
+ },
93
+ clockText: {
94
+ color: MOVIIE_SKIN_CHROME_FOREGROUND_HEX,
95
+ fontSize: layout.clockTextFontSizePx,
96
+ fontVariant: ['tabular-nums'],
97
+ },
98
+ centerCluster: {
99
+ ...StyleSheet.absoluteFillObject,
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ pointerEvents: 'box-none',
103
+ },
104
+ centerClusterRow: {
105
+ alignItems: 'center',
106
+ columnGap: layout.centerClusterColumnGapPx,
107
+ flexDirection: 'row',
108
+ justifyContent: 'center',
109
+ },
110
+ seekCircularScrim: {
111
+ alignItems: 'center',
112
+ borderRadius: layout.seekScrimOuterDiameterPx / 2,
113
+ height: layout.seekScrimOuterDiameterPx,
114
+ justifyContent: 'center',
115
+ overflow: 'hidden',
116
+ width: layout.seekScrimOuterDiameterPx,
117
+ },
118
+ playScrim: {
119
+ alignItems: 'center',
120
+ borderRadius: layout.playScrimOuterDiameterPx / 2,
121
+ height: layout.playScrimOuterDiameterPx,
122
+ justifyContent: 'center',
123
+ overflow: 'hidden',
124
+ width: layout.playScrimOuterDiameterPx,
125
+ },
126
+ playPlaceholder: {
127
+ height: layout.playScrimOuterDiameterPx,
128
+ width: layout.playScrimOuterDiameterPx,
129
+ },
130
+ fullscreenHudCircleScrim: {
131
+ alignItems: 'center',
132
+ borderRadius: layout.floatingHudFullscreenScrimDiameterPx / 2,
133
+ height: layout.floatingHudFullscreenScrimDiameterPx,
134
+ justifyContent: 'center',
135
+ overflow: 'hidden',
136
+ width: layout.floatingHudFullscreenScrimDiameterPx,
137
+ },
138
+ topFullscreenCorner: {
139
+ alignItems: 'center',
140
+ columnGap: layout.floatingHudButtonGapPx,
141
+ flexDirection: 'row',
142
+ pointerEvents: 'box-none',
143
+ position: 'absolute',
144
+ right: layout.hudEdgeInsetPx,
145
+ top: layout.hudEdgeInsetPx,
146
+ zIndex: 8,
147
+ },
148
+ })
149
+ }
150
+
151
+ function ChromeScrimPad(props: {
152
+ accessibilityLabel?: string
153
+ accessibilityRole?: 'button' | 'link'
154
+ children: ReactNode
155
+ chromeScrimBaseStyle: object
156
+ hitSlop?: PressableProps['hitSlop']
157
+ onPress?: () => void
158
+ style?: object
159
+ }) {
160
+ const padStyle = [props.chromeScrimBaseStyle, props.style]
161
+ if (props.onPress != null) {
162
+ return (
163
+ <Pressable
164
+ accessibilityLabel={props.accessibilityLabel}
165
+ accessibilityRole={props.accessibilityRole ?? 'button'}
166
+ hitSlop={props.hitSlop}
167
+ onPress={props.onPress}
168
+ style={(state) => [
169
+ props.chromeScrimBaseStyle,
170
+ props.style,
171
+ resolveChromeControlPressOpacityStyle(state.pressed),
172
+ ]}
173
+ >
174
+ {props.children}
175
+ </Pressable>
176
+ )
177
+ }
178
+ return <View style={padStyle}>{props.children}</View>
179
+ }
180
+
181
+ function MoviieSkinPictureInPictureCircleButton(props: {
182
+ layout: MoviieSkinLayoutMetrics
183
+ onPress: () => void
184
+ pictureInPictureActive: boolean
185
+ styles: ReturnType<typeof createMoviieSkinOverlayStyles>
186
+ }) {
187
+ return (
188
+ <ChromeScrimPad
189
+ accessibilityLabel={
190
+ props.pictureInPictureActive ? 'Fechar imagem no imagem' : 'Imagem no imagem'
191
+ }
192
+ chromeScrimBaseStyle={props.styles.chromeScrimBase}
193
+ hitSlop={MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX}
194
+ onPress={props.onPress}
195
+ style={props.styles.fullscreenHudCircleScrim}
196
+ >
197
+ <EmbedMediaIconPictureInPicture
198
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
199
+ size={props.layout.floatingHudFullscreenIconPx}
200
+ />
201
+ </ChromeScrimPad>
202
+ )
203
+ }
204
+
205
+ function MoviieSkinPictureInPictureTopBareButton(props: {
206
+ layout: MoviieSkinLayoutMetrics
207
+ onPress: () => void
208
+ pictureInPictureActive: boolean
209
+ }) {
210
+ return (
211
+ <Pressable
212
+ accessibilityLabel={
213
+ props.pictureInPictureActive ? 'Fechar imagem no imagem' : 'Imagem no imagem'
214
+ }
215
+ accessibilityRole="button"
216
+ hitSlop={MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX}
217
+ onPress={props.onPress}
218
+ style={(state) => resolveChromeControlPressOpacityStyle(state.pressed)}
219
+ >
220
+ <EmbedMediaIconPictureInPicture
221
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
222
+ size={props.layout.floatingHudFullscreenTopIconPx * MOVIIE_SKIN_TOP_ICON_PIP_VISUAL_SCALE}
223
+ />
224
+ </Pressable>
225
+ )
226
+ }
227
+
228
+ function MoviieSkinFullscreenCircleButton(props: {
229
+ layout: MoviieSkinLayoutMetrics
230
+ onPress: () => void
231
+ styles: ReturnType<typeof createMoviieSkinOverlayStyles>
232
+ isFullscreen: boolean
233
+ }) {
234
+ return (
235
+ <ChromeScrimPad
236
+ accessibilityLabel={props.isFullscreen ? 'Sair de ecrã inteiro' : 'Ecrã inteiro'}
237
+ chromeScrimBaseStyle={props.styles.chromeScrimBase}
238
+ hitSlop={MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX}
239
+ onPress={props.onPress}
240
+ style={props.styles.fullscreenHudCircleScrim}
241
+ >
242
+ {props.isFullscreen ? (
243
+ <EmbedMediaIconFullscreenExit
244
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
245
+ size={props.layout.floatingHudFullscreenIconPx}
246
+ />
247
+ ) : (
248
+ <EmbedMediaIconFullscreen
249
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
250
+ size={props.layout.floatingHudFullscreenIconPx}
251
+ />
252
+ )}
253
+ </ChromeScrimPad>
254
+ )
255
+ }
256
+
257
+ function MoviieSkinFullscreenTopBareButton(props: {
258
+ layout: MoviieSkinLayoutMetrics
259
+ onPress: () => void
260
+ isFullscreen: boolean
261
+ }) {
262
+ const size =
263
+ props.layout.floatingHudFullscreenTopIconPx * MOVIIE_SKIN_TOP_ICON_FULLSCREEN_VISUAL_SCALE
264
+ return (
265
+ <Pressable
266
+ accessibilityLabel={props.isFullscreen ? 'Sair de ecrã inteiro' : 'Ecrã inteiro'}
267
+ accessibilityRole="button"
268
+ hitSlop={MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX}
269
+ onPress={props.onPress}
270
+ style={(state) => resolveChromeControlPressOpacityStyle(state.pressed)}
271
+ >
272
+ {props.isFullscreen ? (
273
+ <EmbedMediaIconFullscreenExit color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX} size={size} />
274
+ ) : (
275
+ <EmbedMediaIconFullscreen color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX} size={size} />
276
+ )}
277
+ </Pressable>
278
+ )
279
+ }
280
+
281
+ function MoviieSkinBufferingIndicator() {
282
+ const { isBuffering, layout } = useMoviieSkinChrome()
283
+
284
+ const bufferingHudStyle = useMemo(
285
+ () => ({
286
+ alignItems: 'center' as const,
287
+ height: layout.bufferingIndicatorSlotPx,
288
+ justifyContent: 'center' as const,
289
+ left: layout.hudEdgeInsetPx,
290
+ position: 'absolute' as const,
291
+ top: layout.hudEdgeInsetPx,
292
+ width: layout.bufferingIndicatorSlotPx,
293
+ zIndex: 16,
294
+ }),
295
+ [layout]
296
+ )
297
+
298
+ if (!isBuffering) {
299
+ return null
300
+ }
301
+
302
+ return (
303
+ <View pointerEvents="none" style={bufferingHudStyle}>
304
+ <ActivityIndicator
305
+ accessibilityLabel="A carregar vídeo"
306
+ color={MOVIIE_SKIN_CHROME_FOREGROUND_HEX}
307
+ size="small"
308
+ />
309
+ </View>
310
+ )
311
+ }
312
+
313
+ export function MoviieSkinVideoOverlay() {
314
+ const {
315
+ chromeOpacity,
316
+ chromeVisible,
317
+ bumpChromeInteraction,
318
+ castConnected,
319
+ castControlEnabled,
320
+ dismissChrome,
321
+ isCustomFullscreen,
322
+ openCastPicker,
323
+ pictureInPictureActive,
324
+ pictureInPictureControlEnabled,
325
+ toggleFullscreen,
326
+ togglePictureInPicture,
327
+ layout,
328
+ playback,
329
+ player,
330
+ seekBack,
331
+ seekForward,
332
+ timelineScrubClockSeconds,
333
+ togglePlay,
334
+ ui,
335
+ } = useMoviieSkinChrome()
336
+
337
+ const handlePictureInPicturePress = useCallback(() => {
338
+ togglePictureInPicture()
339
+ dismissChrome()
340
+ }, [togglePictureInPicture, dismissChrome])
341
+
342
+ const handleCastPress = useCallback(() => {
343
+ // Dismiss chrome first so the overlay doesn't interfere with the native dialog presentation.
344
+ dismissChrome()
345
+ // Defer picker slightly to let the chrome animation settle before the native sheet presents.
346
+ setTimeout(() => openCastPicker(), 150)
347
+ }, [openCastPicker, dismissChrome])
348
+
349
+ const styles = useMemo(() => createMoviieSkinOverlayStyles(layout), [layout])
350
+
351
+ const { controls } = playback
352
+ const fg = MOVIIE_SKIN_CHROME_FOREGROUND_HEX
353
+
354
+ const clockDisplaySeconds = timelineScrubClockSeconds ?? ui.currentTime
355
+
356
+ const playbackEndedForUi = useMemo(
357
+ () =>
358
+ computeMoviiePlaybackEnded({
359
+ playing: ui.playing,
360
+ currentTime: clockDisplaySeconds,
361
+ duration: ui.duration,
362
+ isLive: player.isLive,
363
+ loop: player.loop,
364
+ }),
365
+ [ui.playing, clockDisplaySeconds, ui.duration, player.isLive, player.loop]
366
+ )
367
+
368
+ const { centerSeekBackwardEnabled, centerSeekForwardEnabled } = useMemo(() => {
369
+ const alignSeekButtonsWithPlay = controls.showPlay && !player.isLive
370
+ const hideSeekClusterAtEnd = playbackEndedForUi
371
+ return {
372
+ centerSeekBackwardEnabled:
373
+ !hideSeekClusterAtEnd && (controls.seekBackwardEnabled || alignSeekButtonsWithPlay),
374
+ centerSeekForwardEnabled:
375
+ !hideSeekClusterAtEnd && (controls.seekForwardEnabled || alignSeekButtonsWithPlay),
376
+ }
377
+ }, [
378
+ controls.seekBackwardEnabled,
379
+ controls.seekForwardEnabled,
380
+ controls.showPlay,
381
+ player.isLive,
382
+ playbackEndedForUi,
383
+ ])
384
+
385
+ const centerPlayShowsReplay = playbackEndedForUi
386
+
387
+ const centerPlayAccessibilityLabel = ui.playing
388
+ ? 'Pausar'
389
+ : centerPlayShowsReplay
390
+ ? 'Começar novamente'
391
+ : 'Reproduzir'
392
+
393
+ const clockLabel =
394
+ controls.showCurrentTime || controls.showDuration
395
+ ? formatPlaybackClock(clockDisplaySeconds, ui.duration)
396
+ : null
397
+
398
+ const showSeekCluster = centerSeekBackwardEnabled || centerSeekForwardEnabled || controls.showPlay
399
+
400
+ const seekSlot = layout.seekScrimOuterDiameterPx
401
+
402
+ const showPictureInPictureTrailing = pictureInPictureControlEnabled
403
+ const showCastTrailing = castControlEnabled
404
+
405
+ const hasFloatingHudTrailing =
406
+ playback.branding.showWatermark ||
407
+ (!playback.branding.showWatermark &&
408
+ (controls.showFullscreen || showPictureInPictureTrailing || showCastTrailing))
409
+
410
+ const showFloatingHud = clockLabel != null || hasFloatingHudTrailing
411
+
412
+ const showFullscreenBottomTrailing = controls.showFullscreen && !playback.branding.showWatermark
413
+
414
+ const showTopCornerChrome =
415
+ playback.branding.showWatermark &&
416
+ (controls.showFullscreen || showPictureInPictureTrailing || showCastTrailing)
417
+
418
+ return (
419
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFillObject}>
420
+ <Pressable
421
+ accessibilityLabel={
422
+ chromeVisible
423
+ ? 'Área do vídeo — toque para esconder os controles'
424
+ : 'Mostrar controles do vídeo'
425
+ }
426
+ accessibilityRole="button"
427
+ onPress={chromeVisible ? dismissChrome : bumpChromeInteraction}
428
+ style={[StyleSheet.absoluteFillObject, { zIndex: 0 }]}
429
+ />
430
+
431
+ <AnimatedView
432
+ pointerEvents={!chromeVisible ? 'none' : 'box-none'}
433
+ style={[
434
+ StyleSheet.absoluteFillObject,
435
+ {
436
+ opacity: chromeOpacity,
437
+ zIndex: 2,
438
+ },
439
+ ]}
440
+ >
441
+ <MoviieSkinChromeEdgeGradients layout={layout} showProgress={controls.showProgress} />
442
+
443
+ {showTopCornerChrome ? (
444
+ <View pointerEvents="box-none" style={styles.topFullscreenCorner}>
445
+ {showCastTrailing ? (
446
+ <MoviieSkinCastTopBareButton
447
+ castConnected={castConnected}
448
+ layout={layout}
449
+ onPress={handleCastPress}
450
+ />
451
+ ) : null}
452
+ {showPictureInPictureTrailing ? (
453
+ <MoviieSkinPictureInPictureTopBareButton
454
+ layout={layout}
455
+ pictureInPictureActive={pictureInPictureActive}
456
+ onPress={handlePictureInPicturePress}
457
+ />
458
+ ) : null}
459
+ {controls.showFullscreen ? (
460
+ <MoviieSkinFullscreenTopBareButton
461
+ isFullscreen={isCustomFullscreen}
462
+ layout={layout}
463
+ onPress={toggleFullscreen}
464
+ />
465
+ ) : null}
466
+ </View>
467
+ ) : null}
468
+
469
+ {showFloatingHud ? (
470
+ <View
471
+ pointerEvents="box-none"
472
+ style={[
473
+ styles.floatingHud,
474
+ controls.showProgress ? { bottom: layout.timelineStackHeightChromeVisiblePx } : null,
475
+ ]}
476
+ >
477
+ <View style={styles.floatingHudInner}>
478
+ {clockLabel ? (
479
+ <ChromeScrimPad
480
+ chromeScrimBaseStyle={styles.chromeScrimBase}
481
+ style={styles.clockScrim}
482
+ >
483
+ <Text accessibilityLabel={`Tempo ${clockLabel}`} style={styles.clockText}>
484
+ {clockLabel}
485
+ </Text>
486
+ </ChromeScrimPad>
487
+ ) : (
488
+ <View style={styles.floatingLeftSpacer} />
489
+ )}
490
+
491
+ <View style={styles.floatingRight}>
492
+ {playback.branding.showWatermark ? (
493
+ <MoviieEmbedBrandMark />
494
+ ) : (
495
+ <>
496
+ {showCastTrailing ? (
497
+ <MoviieSkinCastCircleButton
498
+ castConnected={castConnected}
499
+ chromeScrimBaseStyle={styles.chromeScrimBase}
500
+ circleScrimStyle={styles.fullscreenHudCircleScrim}
501
+ layout={layout}
502
+ onPress={handleCastPress}
503
+ />
504
+ ) : null}
505
+ {showPictureInPictureTrailing ? (
506
+ <MoviieSkinPictureInPictureCircleButton
507
+ layout={layout}
508
+ pictureInPictureActive={pictureInPictureActive}
509
+ onPress={handlePictureInPicturePress}
510
+ styles={styles}
511
+ />
512
+ ) : null}
513
+ {showFullscreenBottomTrailing ? (
514
+ <MoviieSkinFullscreenCircleButton
515
+ isFullscreen={isCustomFullscreen}
516
+ layout={layout}
517
+ onPress={toggleFullscreen}
518
+ styles={styles}
519
+ />
520
+ ) : null}
521
+ </>
522
+ )}
523
+ </View>
524
+ </View>
525
+ </View>
526
+ ) : null}
527
+
528
+ {showSeekCluster ? (
529
+ <View pointerEvents="box-none" style={styles.centerCluster}>
530
+ <View style={styles.centerClusterRow}>
531
+ {centerSeekBackwardEnabled ? (
532
+ <ChromeScrimPad
533
+ accessibilityLabel="Retroceder 10 segundos"
534
+ chromeScrimBaseStyle={styles.chromeScrimBase}
535
+ onPress={seekBack}
536
+ style={styles.seekCircularScrim}
537
+ >
538
+ <EmbedMediaIconSeekBackward10 color={fg} size={layout.seekIconPx} />
539
+ </ChromeScrimPad>
540
+ ) : (
541
+ <View style={{ height: seekSlot, width: seekSlot }} />
542
+ )}
543
+
544
+ {controls.showPlay ? (
545
+ <ChromeScrimPad
546
+ accessibilityLabel={centerPlayAccessibilityLabel}
547
+ chromeScrimBaseStyle={styles.chromeScrimBase}
548
+ onPress={togglePlay}
549
+ style={styles.playScrim}
550
+ >
551
+ {ui.playing ? (
552
+ <EmbedMediaIconPause color={fg} size={layout.centerPlayIconPx} />
553
+ ) : centerPlayShowsReplay ? (
554
+ <EmbedMediaIconReplay color={fg} size={layout.centerPlayIconPx} />
555
+ ) : (
556
+ <EmbedMediaIconPlay color={fg} size={layout.centerPlayIconPx} />
557
+ )}
558
+ </ChromeScrimPad>
559
+ ) : (
560
+ <View style={styles.playPlaceholder} />
561
+ )}
562
+
563
+ {centerSeekForwardEnabled ? (
564
+ <ChromeScrimPad
565
+ accessibilityLabel="Avançar 10 segundos"
566
+ chromeScrimBaseStyle={styles.chromeScrimBase}
567
+ onPress={seekForward}
568
+ style={styles.seekCircularScrim}
569
+ >
570
+ <EmbedMediaIconSeekForward10 color={fg} size={layout.seekIconPx} />
571
+ </ChromeScrimPad>
572
+ ) : (
573
+ <View style={{ height: seekSlot, width: seekSlot }} />
574
+ )}
575
+ </View>
576
+ </View>
577
+ ) : null}
578
+ </AnimatedView>
579
+
580
+ <MoviieSkinBufferingIndicator />
581
+ <MoviieSkinSmartProgress />
582
+ <MoviieSkinBottomTimeline />
583
+ </View>
584
+ )
585
+ }