@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,374 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ type RefObject,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react"
12
+ import { Animated, Easing, useWindowDimensions } from "react-native"
13
+
14
+ import type { VideoPlayer } from "expo-video"
15
+
16
+ import type { MoviiePlaybackData } from "@moviie/player-sdk"
17
+
18
+ import {
19
+ MOVIIE_SKIN_CONTROLS_FADE_MS,
20
+ MOVIIE_SKIN_SEEK_JUMP_SECONDS,
21
+ } from "../../constants"
22
+ import {
23
+ buildMoviieSkinLayoutMetrics,
24
+ type MoviieSkinLayoutMetrics,
25
+ } from "../../lib/build-moviie-skin-layout-metrics"
26
+ import { computeMoviiePlaybackEnded } from "../../lib/compute-moviie-playback-ended"
27
+ import { computePlaybackBuffering } from "../../lib/compute-playback-buffering"
28
+
29
+ /** Subconjunto do VideoView nativo usado pela skin (tipagens expo-video × React). */
30
+ export type MoviieVideoSurfaceRef = {
31
+ enterFullscreen?: () => Promise<void>
32
+ exitFullscreen?: () => Promise<void>
33
+ startPictureInPicture?: () => Promise<void>
34
+ stopPictureInPicture?: () => Promise<void>
35
+ }
36
+
37
+ export function usePlaybackUiSync(player: VideoPlayer): {
38
+ bufferedPosition: number
39
+ currentTime: number
40
+ duration: number
41
+ playing: boolean
42
+ status: string
43
+ } {
44
+ const [snapshot, setSnapshot] = useState({
45
+ bufferedPosition: player.bufferedPosition,
46
+ currentTime: player.currentTime,
47
+ duration: player.duration,
48
+ playing: player.playing,
49
+ status: player.status,
50
+ })
51
+
52
+ useEffect(() => {
53
+ const subscriptions = [
54
+ player.addListener("timeUpdate", ({ currentTime }) => {
55
+ setSnapshot((previous) => ({
56
+ ...previous,
57
+ bufferedPosition: player.bufferedPosition,
58
+ currentTime,
59
+ duration: player.duration,
60
+ status: player.status,
61
+ }))
62
+ }),
63
+ player.addListener("playingChange", ({ isPlaying }) => {
64
+ setSnapshot((previous) => ({
65
+ ...previous,
66
+ playing: isPlaying,
67
+ bufferedPosition: player.bufferedPosition,
68
+ status: player.status,
69
+ }))
70
+ }),
71
+ player.addListener("sourceChange", () => {
72
+ setSnapshot({
73
+ bufferedPosition: player.bufferedPosition,
74
+ currentTime: player.currentTime,
75
+ duration: player.duration,
76
+ playing: player.playing,
77
+ status: player.status,
78
+ })
79
+ }),
80
+ player.addListener("statusChange", ({ status }) => {
81
+ setSnapshot((previous) => ({
82
+ ...previous,
83
+ bufferedPosition: player.bufferedPosition,
84
+ status,
85
+ }))
86
+ }),
87
+ player.addListener("playToEnd", () => {
88
+ setSnapshot((previous) => ({
89
+ ...previous,
90
+ playing: false,
91
+ bufferedPosition: player.bufferedPosition,
92
+ currentTime: player.currentTime,
93
+ duration: player.duration,
94
+ status: player.status,
95
+ }))
96
+ }),
97
+ ]
98
+
99
+ return () => {
100
+ for (const sub of subscriptions) {
101
+ sub.remove()
102
+ }
103
+ }
104
+ }, [player])
105
+
106
+ return snapshot
107
+ }
108
+
109
+ export type { MoviieSkinLayoutMetrics }
110
+
111
+ export interface MoviieSkinChromeContextValue {
112
+ playback: MoviiePlaybackData
113
+ player: VideoPlayer
114
+ videoViewRef: RefObject<MoviieVideoSurfaceRef | null>
115
+ chromeVisible: boolean
116
+ chromeOpacity: Animated.Value
117
+ /** Posição (s) reflectida no relógio durante scrub; `null` = usar tempo do player. */
118
+ timelineScrubClockSeconds: number | null
119
+ setTimelineScrubClockSeconds: (seconds: number | null) => void
120
+ bumpChromeInteraction: () => void
121
+ dismissChrome: () => void
122
+ ui: ReturnType<typeof usePlaybackUiSync>
123
+ isBuffering: boolean
124
+ layout: MoviieSkinLayoutMetrics
125
+ togglePlay: () => void
126
+ seekBack: () => void
127
+ seekForward: () => void
128
+ /** Fullscreen da skin Moviie em JS (modal): alterna entrar/sair; não usa `VideoView.enterFullscreen`. */
129
+ toggleFullscreen: () => void
130
+ isCustomFullscreen: boolean
131
+ pictureInPictureControlEnabled: boolean
132
+ pictureInPictureActive: boolean
133
+ togglePictureInPicture: () => void
134
+ castControlEnabled: boolean
135
+ castConnected: boolean
136
+ openCastPicker: () => void
137
+ }
138
+
139
+ const MoviieSkinChromeContext = createContext<MoviieSkinChromeContextValue | null>(
140
+ null
141
+ )
142
+
143
+ export function useMoviieSkinChrome(): MoviieSkinChromeContextValue {
144
+ const ctx = useContext(MoviieSkinChromeContext)
145
+ if (ctx == null) {
146
+ throw new Error("useMoviieSkinChrome must be used within MoviieSkinChromeProvider")
147
+ }
148
+ return ctx
149
+ }
150
+
151
+ export interface MoviieSkinChromeProviderProps {
152
+ children: ReactNode
153
+ player: VideoPlayer
154
+ playback: MoviiePlaybackData
155
+ videoViewRef: RefObject<MoviieVideoSurfaceRef | null>
156
+ /** Quando definido com {@link onRequestExitCustomFullscreenAnimated}, o chrome usa fullscreen JS em vez de `VideoView.enterFullscreen`. */
157
+ customFullscreen?: boolean
158
+ /**
159
+ * Largura lógica para {@link buildMoviieSkinLayoutMetrics} (ex.: lado maior do ecrã no fullscreen rotacionado).
160
+ */
161
+ skinLayoutMetricsWidth?: number
162
+ onRequestEnterCustomFullscreen?: () => void
163
+ onRequestExitCustomFullscreenAnimated?: () => void
164
+ /**
165
+ * Ignorado: o chrome só oculta ao toque fora / comando (play, seek).
166
+ * Mantido por compatibilidade com chamadas existentes.
167
+ */
168
+ autoHideMs?: number
169
+ visibleByDefault?: boolean
170
+ pictureInPictureControlEnabled?: boolean
171
+ pictureInPictureActive?: boolean
172
+ togglePictureInPicture?: () => void
173
+ castControlEnabled?: boolean
174
+ castConnected?: boolean
175
+ openCastPicker?: () => void
176
+ }
177
+
178
+ export function MoviieSkinChromeProvider({
179
+ children,
180
+ player,
181
+ playback,
182
+ videoViewRef,
183
+ customFullscreen = false,
184
+ skinLayoutMetricsWidth,
185
+ onRequestEnterCustomFullscreen,
186
+ onRequestExitCustomFullscreenAnimated,
187
+ autoHideMs: _unusedAutoHideMs,
188
+ visibleByDefault = true,
189
+ pictureInPictureControlEnabled = false,
190
+ pictureInPictureActive = false,
191
+ togglePictureInPicture: togglePictureInPictureProp,
192
+ castControlEnabled = false,
193
+ castConnected = false,
194
+ openCastPicker: openCastPickerProp,
195
+ }: MoviieSkinChromeProviderProps) {
196
+ void _unusedAutoHideMs
197
+
198
+ const noopPictureInPicture = useCallback(() => {}, [])
199
+
200
+ const togglePictureInPicture =
201
+ togglePictureInPictureProp ?? noopPictureInPicture
202
+
203
+ const noopOpenCastPicker = useCallback(() => {}, [])
204
+
205
+ const openCastPicker = openCastPickerProp ?? noopOpenCastPicker
206
+
207
+ const { width: windowWidth } = useWindowDimensions()
208
+ const layoutBasisWidth = skinLayoutMetricsWidth ?? windowWidth
209
+ const layout = useMemo(
210
+ () =>
211
+ buildMoviieSkinLayoutMetrics(layoutBasisWidth, {
212
+ fullscreenHudProtection: customFullscreen,
213
+ }),
214
+ [customFullscreen, layoutBasisWidth]
215
+ )
216
+
217
+ const ui = usePlaybackUiSync(player)
218
+
219
+ const [chromeVisible, setChromeVisible] = useState(visibleByDefault)
220
+ const [timelineScrubClockSeconds, setTimelineScrubClockSeconds] =
221
+ useState<number | null>(null)
222
+
223
+ const playbackEndedForChrome = useMemo(
224
+ () =>
225
+ computeMoviiePlaybackEnded({
226
+ playing: ui.playing,
227
+ currentTime: ui.currentTime,
228
+ duration: ui.duration,
229
+ isLive: player.isLive,
230
+ loop: player.loop,
231
+ }),
232
+ [ui.playing, ui.currentTime, ui.duration, player.isLive, player.loop]
233
+ )
234
+
235
+ useEffect(() => {
236
+ if (playbackEndedForChrome) {
237
+ setChromeVisible(true)
238
+ }
239
+ }, [playbackEndedForChrome])
240
+
241
+ const chromeOpacity = useRef(new Animated.Value(visibleByDefault ? 1 : 0)).current
242
+
243
+ useEffect(() => {
244
+ Animated.timing(chromeOpacity, {
245
+ duration: MOVIIE_SKIN_CONTROLS_FADE_MS,
246
+ easing: Easing.out(Easing.quad),
247
+ toValue: chromeVisible ? 1 : 0,
248
+ useNativeDriver: true,
249
+ }).start()
250
+ }, [chromeOpacity, chromeVisible])
251
+
252
+ const bumpChromeInteraction = useCallback(() => {
253
+ setChromeVisible(true)
254
+ }, [])
255
+
256
+ const dismissChrome = useCallback(() => {
257
+ setChromeVisible(false)
258
+ }, [])
259
+
260
+ const togglePlay = useCallback(() => {
261
+ if (player.playing) {
262
+ player.pause()
263
+ return
264
+ }
265
+ if (
266
+ computeMoviiePlaybackEnded({
267
+ playing: false,
268
+ currentTime: player.currentTime,
269
+ duration: player.duration,
270
+ isLive: player.isLive,
271
+ loop: player.loop,
272
+ })
273
+ ) {
274
+ player.currentTime = 0
275
+ }
276
+ player.play()
277
+ setChromeVisible(false)
278
+ }, [player])
279
+
280
+ const seekBack = useCallback(() => {
281
+ void player.seekBy(-MOVIIE_SKIN_SEEK_JUMP_SECONDS)
282
+ setChromeVisible(false)
283
+ }, [player])
284
+
285
+ const seekForward = useCallback(() => {
286
+ void player.seekBy(MOVIIE_SKIN_SEEK_JUMP_SECONDS)
287
+ setChromeVisible(false)
288
+ }, [player])
289
+
290
+ const toggleFullscreen = useCallback(() => {
291
+ if (customFullscreen) {
292
+ onRequestExitCustomFullscreenAnimated?.()
293
+ return
294
+ }
295
+ setChromeVisible(false)
296
+ if (onRequestEnterCustomFullscreen != null) {
297
+ onRequestEnterCustomFullscreen()
298
+ return
299
+ }
300
+ void videoViewRef.current?.enterFullscreen?.().catch(() => undefined)
301
+ }, [
302
+ customFullscreen,
303
+ onRequestEnterCustomFullscreen,
304
+ onRequestExitCustomFullscreenAnimated,
305
+ videoViewRef,
306
+ ])
307
+
308
+ const isBuffering = useMemo(
309
+ () =>
310
+ computePlaybackBuffering({
311
+ ...ui,
312
+ isLive: player.isLive,
313
+ loop: player.loop,
314
+ }),
315
+ [ui, player.isLive, player.loop]
316
+ )
317
+
318
+ const value = useMemo(
319
+ (): MoviieSkinChromeContextValue => ({
320
+ playback,
321
+ player,
322
+ videoViewRef,
323
+ chromeVisible,
324
+ chromeOpacity,
325
+ timelineScrubClockSeconds,
326
+ setTimelineScrubClockSeconds,
327
+ bumpChromeInteraction,
328
+ dismissChrome,
329
+ ui,
330
+ isBuffering,
331
+ layout,
332
+ togglePlay,
333
+ seekBack,
334
+ seekForward,
335
+ toggleFullscreen,
336
+ isCustomFullscreen: customFullscreen,
337
+ pictureInPictureActive,
338
+ pictureInPictureControlEnabled,
339
+ togglePictureInPicture,
340
+ castControlEnabled,
341
+ castConnected,
342
+ openCastPicker,
343
+ }),
344
+ [
345
+ playback,
346
+ player,
347
+ videoViewRef,
348
+ chromeVisible,
349
+ chromeOpacity,
350
+ timelineScrubClockSeconds,
351
+ setTimelineScrubClockSeconds,
352
+ bumpChromeInteraction,
353
+ dismissChrome,
354
+ ui,
355
+ isBuffering,
356
+ layout,
357
+ togglePlay,
358
+ seekBack,
359
+ seekForward,
360
+ toggleFullscreen,
361
+ customFullscreen,
362
+ pictureInPictureActive,
363
+ pictureInPictureControlEnabled,
364
+ togglePictureInPicture,
365
+ castControlEnabled,
366
+ castConnected,
367
+ openCastPicker,
368
+ ]
369
+ )
370
+
371
+ return (
372
+ <MoviieSkinChromeContext.Provider value={value}>{children}</MoviieSkinChromeContext.Provider>
373
+ )
374
+ }
@@ -0,0 +1,157 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react"
2
+ import { Animated, StyleSheet, View } from "react-native"
3
+
4
+ import {
5
+ MOVIIE_SMART_PROGRESS_BAR_HEIGHT_DP,
6
+ MOVIIE_SMART_PROGRESS_FILL_TRANSITION_MS,
7
+ MOVIIE_SMART_PROGRESS_Z_INDEX,
8
+ } from "../../constants"
9
+ import { computePlaybackProgressRatio } from "../../lib/compute-playback-progress-ratio"
10
+ import { mapSmartProgressRawRatio } from "../../lib/map-smart-progress-display-ratio"
11
+ import { resolveSkinAccentColor } from "../../lib/resolve-skin-accent-color"
12
+
13
+ import { useMoviieSkinChrome } from "./moviie-skin-chrome-context"
14
+
15
+ const styles = StyleSheet.create({
16
+ container: {
17
+ bottom: 0,
18
+ height: MOVIIE_SMART_PROGRESS_BAR_HEIGHT_DP,
19
+ left: 0,
20
+ overflow: "hidden",
21
+ position: "absolute",
22
+ right: 0,
23
+ zIndex: MOVIIE_SMART_PROGRESS_Z_INDEX,
24
+ },
25
+ fill: {
26
+ bottom: 0,
27
+ height: "100%",
28
+ left: 0,
29
+ position: "absolute",
30
+ top: 0,
31
+ },
32
+ })
33
+
34
+ export function MoviieSkinSmartProgress() {
35
+ const { playback, player, timelineScrubClockSeconds, ui } = useMoviieSkinChrome()
36
+
37
+ const accent = resolveSkinAccentColor(playback.branding.primaryColor)
38
+
39
+ const durationOk =
40
+ playback.smartProgressEnabled &&
41
+ Number.isFinite(ui.duration) &&
42
+ ui.duration > 0 &&
43
+ !player.isLive
44
+
45
+ const [barVisible, setBarVisible] = useState(() => !player.muted)
46
+ const hasUserInteractedRef = useRef(!player.muted)
47
+ const lastDisplayedRef = useRef(0)
48
+ const progressAnim = useRef(new Animated.Value(0)).current
49
+
50
+ const syncMutedVisibility = useCallback(() => {
51
+ const muted = player.muted
52
+ if (!muted) {
53
+ hasUserInteractedRef.current = true
54
+ setBarVisible(true)
55
+ return
56
+ }
57
+ if (!hasUserInteractedRef.current) {
58
+ setBarVisible(false)
59
+ }
60
+ }, [player])
61
+
62
+ useEffect(() => {
63
+ syncMutedVisibility()
64
+ const subMuted = player.addListener("mutedChange", syncMutedVisibility)
65
+ const subVolume = player.addListener("volumeChange", syncMutedVisibility)
66
+ return () => {
67
+ subMuted.remove()
68
+ subVolume.remove()
69
+ }
70
+ }, [player, syncMutedVisibility])
71
+
72
+ useEffect(() => {
73
+ lastDisplayedRef.current = 0
74
+ progressAnim.setValue(0)
75
+ }, [playback.embedId, progressAnim])
76
+
77
+ useEffect(() => {
78
+ if (!durationOk || !barVisible) {
79
+ return
80
+ }
81
+
82
+ const scrubbing = timelineScrubClockSeconds != null
83
+ const clockSeconds = scrubbing ? timelineScrubClockSeconds : ui.currentTime
84
+
85
+ const realRatio = computePlaybackProgressRatio({
86
+ currentTime: clockSeconds,
87
+ duration: ui.duration,
88
+ durationOk: true,
89
+ playing: ui.playing,
90
+ })
91
+
92
+ const mapped = mapSmartProgressRawRatio(realRatio)
93
+ const next = scrubbing
94
+ ? mapped
95
+ : Math.max(mapped, lastDisplayedRef.current)
96
+
97
+ lastDisplayedRef.current = next
98
+
99
+ Animated.timing(progressAnim, {
100
+ duration: MOVIIE_SMART_PROGRESS_FILL_TRANSITION_MS,
101
+ toValue: next,
102
+ useNativeDriver: false,
103
+ }).start()
104
+ }, [
105
+ barVisible,
106
+ durationOk,
107
+ progressAnim,
108
+ timelineScrubClockSeconds,
109
+ ui.currentTime,
110
+ ui.duration,
111
+ ui.playing,
112
+ ])
113
+
114
+ useEffect(() => {
115
+ if (!durationOk || !barVisible) {
116
+ return
117
+ }
118
+
119
+ const sub = player.addListener("playToEnd", () => {
120
+ lastDisplayedRef.current = 1
121
+ Animated.timing(progressAnim, {
122
+ duration: MOVIIE_SMART_PROGRESS_FILL_TRANSITION_MS,
123
+ toValue: 1,
124
+ useNativeDriver: false,
125
+ }).start()
126
+ })
127
+ return () => sub.remove()
128
+ }, [barVisible, durationOk, player, progressAnim])
129
+
130
+ if (!playback.smartProgressEnabled || !durationOk) {
131
+ return null
132
+ }
133
+
134
+ if (!barVisible) {
135
+ return null
136
+ }
137
+
138
+ const widthInterpolated = progressAnim.interpolate({
139
+ extrapolate: "clamp",
140
+ inputRange: [0, 1],
141
+ outputRange: ["0%", "100%"],
142
+ })
143
+
144
+ return (
145
+ <View pointerEvents="none" style={styles.container}>
146
+ <Animated.View
147
+ style={[
148
+ styles.fill,
149
+ {
150
+ backgroundColor: accent,
151
+ width: widthInterpolated,
152
+ },
153
+ ]}
154
+ />
155
+ </View>
156
+ )
157
+ }