@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,14 @@
1
+ import type { VideoPlayer } from "expo-video"
2
+ import { PLAYER_API_EVENTS } from "@moviie/player-types"
3
+
4
+ import { useMoviieEvent } from "./use-moviie-event"
5
+
6
+ /**
7
+ * Natural playback end from expo-video (`playToEnd`), aligned with {@link PLAYER_API_EVENTS.ENDED}.
8
+ */
9
+ export function useMoviiePlaybackEnded(
10
+ player: VideoPlayer | null,
11
+ onEnded: () => void
12
+ ): void {
13
+ useMoviieEvent(player, PLAYER_API_EVENTS.ENDED, onEnded)
14
+ }
@@ -0,0 +1,76 @@
1
+ import { useEffect, useRef } from "react"
2
+
3
+ import type { VideoPlayer } from "expo-video"
4
+
5
+ import { MOVIIE_REMEMBER_POSITION_DEBOUNCE_MS } from "../constants"
6
+ import {
7
+ clampResumeTimeSeconds,
8
+ clearStoredResumeSeconds,
9
+ writeStoredResumeSeconds,
10
+ } from "../lib/remember-playback-position-storage"
11
+
12
+ export function useMoviiePlaybackResumePersistence(options: {
13
+ player: VideoPlayer
14
+ embedId: string | undefined
15
+ durationSeconds: number | null | undefined
16
+ enabled: boolean
17
+ }): void {
18
+ const { player, embedId, durationSeconds, enabled } = options
19
+
20
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
21
+
22
+ useEffect(() => {
23
+ if (!enabled || embedId == null || embedId === "") {
24
+ return
25
+ }
26
+
27
+ const clearDebounce = () => {
28
+ if (debounceTimerRef.current != null) {
29
+ clearTimeout(debounceTimerRef.current)
30
+ debounceTimerRef.current = null
31
+ }
32
+ }
33
+
34
+ const persistNow = (seconds: number) => {
35
+ const clamped = clampResumeTimeSeconds(seconds, durationSeconds)
36
+ if (clamped == null) {
37
+ void clearStoredResumeSeconds(embedId)
38
+ return
39
+ }
40
+ void writeStoredResumeSeconds(embedId, clamped)
41
+ }
42
+
43
+ const schedulePersist = (seconds: number) => {
44
+ clearDebounce()
45
+ debounceTimerRef.current = setTimeout(() => {
46
+ debounceTimerRef.current = null
47
+ persistNow(seconds)
48
+ }, MOVIIE_REMEMBER_POSITION_DEBOUNCE_MS)
49
+ }
50
+
51
+ const subTime = player.addListener("timeUpdate", ({ currentTime }) => {
52
+ if (player.playing) {
53
+ schedulePersist(currentTime)
54
+ }
55
+ })
56
+
57
+ const subPlaying = player.addListener("playingChange", ({ isPlaying }) => {
58
+ if (!isPlaying) {
59
+ clearDebounce()
60
+ persistNow(player.currentTime)
61
+ }
62
+ })
63
+
64
+ const subEnd = player.addListener("playToEnd", () => {
65
+ clearDebounce()
66
+ void clearStoredResumeSeconds(embedId)
67
+ })
68
+
69
+ return () => {
70
+ clearDebounce()
71
+ subTime.remove()
72
+ subPlaying.remove()
73
+ subEnd.remove()
74
+ }
75
+ }, [durationSeconds, embedId, enabled, player])
76
+ }
@@ -0,0 +1,99 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+
3
+ import type { MoviiePlaybackData } from "@moviie/player-sdk"
4
+
5
+ import { fetchPlaybackFreshWriteCache } from "../playback/fetch-playback-fresh"
6
+ import { readPlaybackCache } from "../playback/playback-memory-cache"
7
+ import { useMoviieContext } from "../provider/moviie-provider"
8
+
9
+ export interface UseMoviiePlaybackArgs {
10
+ /** Public embed UUID — client SDK never calls admin APIs or private keys. */
11
+ embedId?: string
12
+ }
13
+
14
+ export interface UseMoviiePlaybackResult {
15
+ data: MoviiePlaybackData | null
16
+ error: Error | null
17
+ isLoading: boolean
18
+ retry: () => void
19
+ }
20
+
21
+ export function useMoviiePlayback(
22
+ args: UseMoviiePlaybackArgs
23
+ ): UseMoviiePlaybackResult {
24
+ const trimmedEmbedId = args.embedId?.trim()
25
+ const missingEmbedId = !trimmedEmbedId
26
+ const { client, publishableKey } = useMoviieContext()
27
+
28
+ const [data, setData] = useState<MoviiePlaybackData | null>(null)
29
+ const [fetchError, setFetchError] = useState<Error | null>(null)
30
+ const [isLoading, setIsLoading] = useState(!missingEmbedId)
31
+ const [retryNonce, setRetryNonce] = useState(0)
32
+
33
+ const retry = useCallback(() => {
34
+ setRetryNonce((n) => n + 1)
35
+ }, [])
36
+
37
+ useEffect(() => {
38
+ if (!trimmedEmbedId) {
39
+ return
40
+ }
41
+
42
+ const embedKey = trimmedEmbedId
43
+
44
+ const controller = new AbortController()
45
+ let cancelled = false
46
+
47
+ async function load(): Promise<void> {
48
+ setIsLoading(true)
49
+ setFetchError(null)
50
+
51
+ try {
52
+ const cached = readPlaybackCache(publishableKey, embedKey)
53
+ if (cached && !controller.signal.aborted) {
54
+ setData(cached)
55
+ }
56
+
57
+ const outcome = await fetchPlaybackFreshWriteCache(
58
+ publishableKey,
59
+ embedKey,
60
+ controller.signal,
61
+ (id, sig) => client.getPlayback(id, sig),
62
+ )
63
+
64
+ if (cancelled || controller.signal.aborted) {
65
+ return
66
+ }
67
+
68
+ if (outcome.ok) {
69
+ setData(outcome.data)
70
+ } else {
71
+ setFetchError(outcome.error)
72
+ setData(readPlaybackCache(publishableKey, embedKey))
73
+ }
74
+ } finally {
75
+ if (!cancelled && !controller.signal.aborted) {
76
+ setIsLoading(false)
77
+ }
78
+ }
79
+ }
80
+
81
+ void load()
82
+
83
+ return () => {
84
+ cancelled = true
85
+ controller.abort()
86
+ }
87
+ }, [trimmedEmbedId, client, publishableKey, retryNonce])
88
+
89
+ const error = missingEmbedId
90
+ ? new Error("Informe um embedId válido (UUID público do embed).")
91
+ : fetchError
92
+
93
+ return {
94
+ data: missingEmbedId ? null : data,
95
+ error,
96
+ isLoading: missingEmbedId ? false : isLoading,
97
+ retry,
98
+ }
99
+ }
@@ -0,0 +1,55 @@
1
+ import type { VideoPlayer } from "expo-video"
2
+
3
+ import type { MoviiePlaybackData } from "@moviie/player-sdk"
4
+
5
+ import type { MoviieVideoPresentationProps } from "../lib/build-moviie-video-presentation"
6
+
7
+ export interface UseMoviiePlayerOptions {
8
+ embedId?: string
9
+ /**
10
+ * When set, overrides {@link MoviiePlaybackData.controls.autoplay}. When omitted, embed wins.
11
+ */
12
+ autoplay?: boolean
13
+ /**
14
+ * When set, overrides PiP capability vs embed `showPip` (passed into presentation props).
15
+ * When omitted, embed wins.
16
+ */
17
+ pictureInPicture?: boolean
18
+ /**
19
+ * When set, overrides auto-start PiP. When omitted, defaults to `false` in presentation props.
20
+ */
21
+ pictureInPictureAutoStart?: boolean
22
+ /**
23
+ * Native-only: keep playback active when the app backgrounds (not part of embed controls).
24
+ * @default false
25
+ */
26
+ backgroundPlayback?: boolean
27
+ /**
28
+ * Native-only: show Now Playing / lock-screen controls (not part of embed controls).
29
+ * @default false
30
+ */
31
+ lockScreenNowPlaying?: boolean
32
+ /**
33
+ * When set, overrides {@link MoviiePlaybackData.controls.rememberPosition}.
34
+ * When omitted, embed wins.
35
+ */
36
+ rememberPosition?: boolean
37
+ }
38
+
39
+ export interface UseMoviiePlayerResult {
40
+ /**
41
+ * Native: `expo-video` player instance. Web (iframe): always `null` — use embed JS API / `onEmbedIframeRef`.
42
+ */
43
+ player: VideoPlayer | null
44
+ playback: MoviiePlaybackData | null
45
+ error: Error | null
46
+ isLoading: boolean
47
+ /**
48
+ * Presentation props for `MoviieVideo` / `VideoView`: embed defaults, plus explicit hook overrides.
49
+ */
50
+ videoPresentation: MoviieVideoPresentationProps
51
+ /**
52
+ * Re-runs the playback fetch. Used by the built-in error shell for transient errors (network/rate_limit).
53
+ */
54
+ retry: () => void
55
+ }
@@ -0,0 +1,236 @@
1
+ import { useEffect, useMemo, useRef } from "react"
2
+
3
+ import { useVideoPlayer } from "expo-video"
4
+ import type { VideoPlayer } from "expo-video"
5
+ import type { VideoSource } from "expo-video"
6
+
7
+ import {
8
+ deriveTelemetryEventsBaseUrlFromBootstrapUrl,
9
+ type MoviiePlaybackData,
10
+ } from "@moviie/player-sdk"
11
+
12
+ import { useMoviiePlayback } from "./use-moviie-playback"
13
+ import { useMoviieTelemetry } from "./use-moviie-telemetry"
14
+ import { useMoviiePlaybackResumePersistence } from "./use-moviie-playback-resume-persistence"
15
+ import { useMoviieContext } from "../provider/moviie-provider"
16
+ import {
17
+ buildMoviieVideoPresentationProps,
18
+ type MoviieVideoPresentationProps,
19
+ } from "../lib/build-moviie-video-presentation"
20
+ import {
21
+ clampResumeTimeSeconds,
22
+ readStoredResumeSeconds,
23
+ } from "../lib/remember-playback-position-storage"
24
+
25
+ import type {
26
+ UseMoviiePlayerOptions,
27
+ UseMoviiePlayerResult,
28
+ } from "./use-moviie-player-types"
29
+
30
+ export type {
31
+ UseMoviiePlayerOptions,
32
+ UseMoviiePlayerResult,
33
+ } from "./use-moviie-player-types"
34
+
35
+ const PLAYBACK_REQUEST_HEADER_ALLOWLIST = new Set([
36
+ "authorization",
37
+ "referer",
38
+ "user-agent",
39
+ "x-moviie-playback-token",
40
+ "x-moviie-signature",
41
+ "x-playback-token",
42
+ ])
43
+
44
+ function filterPlaybackRequestHeaders(
45
+ raw: Record<string, string> | undefined
46
+ ): Record<string, string> | undefined {
47
+ if (!raw) return undefined
48
+ const safe: Record<string, string> = {}
49
+ for (const [name, value] of Object.entries(raw)) {
50
+ if (typeof name !== "string" || typeof value !== "string") continue
51
+ if (!PLAYBACK_REQUEST_HEADER_ALLOWLIST.has(name.toLowerCase())) continue
52
+ if (/[\r\n]/.test(value)) continue
53
+ safe[name] = value
54
+ }
55
+ return Object.keys(safe).length > 0 ? safe : undefined
56
+ }
57
+
58
+ function buildVideoSource(data: MoviiePlaybackData): VideoSource {
59
+ const headers = filterPlaybackRequestHeaders(data.playback.requestHeaders)
60
+ const base: VideoSource = {
61
+ uri: data.playback.uri,
62
+ metadata: {
63
+ title: data.title,
64
+ artist: data.branding.organizationName ?? undefined,
65
+ artwork: data.posterUrl ?? undefined,
66
+ },
67
+ }
68
+ if (headers) {
69
+ return { ...base, headers }
70
+ }
71
+ return base
72
+ }
73
+
74
+ function configurePlaybackDefaults(
75
+ player: VideoPlayer,
76
+ data: MoviiePlaybackData
77
+ ): void {
78
+ const c = data.controls
79
+ player.loop = c.loop
80
+ player.muted = c.muted
81
+ player.timeUpdateEventInterval = 1
82
+ }
83
+
84
+ function applyNativePlaybackPresentation(
85
+ player: VideoPlayer,
86
+ opts: Pick<UseMoviiePlayerOptions, "backgroundPlayback" | "lockScreenNowPlaying">
87
+ ): void {
88
+ player.staysActiveInBackground = opts.backgroundPlayback ?? false
89
+ player.showNowPlayingNotification = opts.lockScreenNowPlaying ?? false
90
+ }
91
+
92
+ function syncPlaybackIntent(player: VideoPlayer, shouldPlay: boolean): void {
93
+ if (shouldPlay) {
94
+ player.play()
95
+ } else {
96
+ player.pause()
97
+ }
98
+ }
99
+
100
+ const EMPTY_PRESENTATION = buildMoviieVideoPresentationProps({
101
+ profile: "",
102
+ showPip: false,
103
+ })
104
+
105
+ export function useMoviiePlayer(
106
+ options: UseMoviiePlayerOptions
107
+ ): UseMoviiePlayerResult {
108
+ const ctx = useMoviieContext()
109
+
110
+ const { data, error, isLoading, retry } = useMoviiePlayback({
111
+ embedId: options.embedId,
112
+ })
113
+
114
+ const rememberEnabled = useMemo(() => {
115
+ if (options.rememberPosition === false) {
116
+ return false
117
+ }
118
+ if (options.rememberPosition === true) {
119
+ return true
120
+ }
121
+ return data?.controls.rememberPosition === true
122
+ }, [data?.controls.rememberPosition, options.rememberPosition])
123
+
124
+ const player = useVideoPlayer(null, (initialPlayer) => {
125
+ initialPlayer.timeUpdateEventInterval = 1
126
+ })
127
+
128
+ useMoviiePlaybackResumePersistence({
129
+ durationSeconds: data?.durationSeconds,
130
+ embedId: data?.embedId,
131
+ enabled: rememberEnabled && data != null,
132
+ player,
133
+ })
134
+
135
+ const resumeHandledRef = useRef(false)
136
+
137
+ useEffect(() => {
138
+ resumeHandledRef.current = false
139
+ }, [data?.embedId, data?.playback.uri])
140
+
141
+ useEffect(() => {
142
+ if (!data) {
143
+ return
144
+ }
145
+
146
+ const src = buildVideoSource(data)
147
+ const shouldPlay =
148
+ options.autoplay !== undefined
149
+ ? options.autoplay
150
+ : data.controls.autoplay
151
+
152
+ player.replace(src)
153
+ configurePlaybackDefaults(player, data)
154
+ applyNativePlaybackPresentation(player, {
155
+ backgroundPlayback: options.backgroundPlayback,
156
+ lockScreenNowPlaying: options.lockScreenNowPlaying,
157
+ })
158
+
159
+ const restoreAndSync = async () => {
160
+ if (player.status !== "readyToPlay") {
161
+ return
162
+ }
163
+
164
+ if (!resumeHandledRef.current) {
165
+ resumeHandledRef.current = true
166
+ if (rememberEnabled) {
167
+ const stored = await readStoredResumeSeconds(data.embedId)
168
+ const clamped = clampResumeTimeSeconds(stored, data.durationSeconds)
169
+ if (clamped != null) {
170
+ player.currentTime = clamped
171
+ }
172
+ }
173
+ }
174
+
175
+ syncPlaybackIntent(player, shouldPlay)
176
+ }
177
+
178
+ const sub = player.addListener("statusChange", (payload) => {
179
+ if (payload.status === "readyToPlay") {
180
+ void restoreAndSync()
181
+ }
182
+ })
183
+
184
+ void restoreAndSync()
185
+
186
+ return () => {
187
+ sub.remove()
188
+ }
189
+ }, [
190
+ data,
191
+ player,
192
+ options.autoplay,
193
+ options.backgroundPlayback,
194
+ options.lockScreenNowPlaying,
195
+ rememberEnabled,
196
+ ])
197
+
198
+ const videoPresentation = useMemo((): MoviieVideoPresentationProps => {
199
+ if (!data) {
200
+ return EMPTY_PRESENTATION
201
+ }
202
+ return buildMoviieVideoPresentationProps({
203
+ pictureInPicture: options.pictureInPicture,
204
+ pictureInPictureAutoStart: options.pictureInPictureAutoStart,
205
+ profile: data.profile,
206
+ showPip: data.controls.showPip,
207
+ })
208
+ }, [
209
+ data,
210
+ options.pictureInPicture,
211
+ options.pictureInPictureAutoStart,
212
+ ])
213
+
214
+ const eventsBaseUrl =
215
+ data?.telemetry.bootstrapUrl != null
216
+ ? deriveTelemetryEventsBaseUrlFromBootstrapUrl(data.telemetry.bootstrapUrl) ??
217
+ ctx.client.getEventsBaseUrl()
218
+ : ctx.client.getEventsBaseUrl()
219
+
220
+ useMoviieTelemetry(
221
+ player,
222
+ data,
223
+ eventsBaseUrl,
224
+ ctx.telemetryHeaders,
225
+ ctx.viewerTokenStore
226
+ )
227
+
228
+ return {
229
+ player,
230
+ playback: data,
231
+ error,
232
+ isLoading,
233
+ videoPresentation,
234
+ retry,
235
+ }
236
+ }
@@ -0,0 +1,57 @@
1
+ import { useMemo } from "react"
2
+
3
+ import type { MoviiePlaybackData } from "@moviie/player-sdk"
4
+
5
+ import {
6
+ buildMoviieVideoPresentationProps,
7
+ type MoviieVideoPresentationProps,
8
+ } from "../lib/build-moviie-video-presentation"
9
+ import { useMoviiePlayback } from "./use-moviie-playback"
10
+ import type {
11
+ UseMoviiePlayerOptions,
12
+ UseMoviiePlayerResult,
13
+ } from "./use-moviie-player-types"
14
+
15
+ const EMPTY_PRESENTATION = buildMoviieVideoPresentationProps({
16
+ profile: "",
17
+ showPip: false,
18
+ })
19
+
20
+ /**
21
+ * Expo Web: não usa `expo-video`; obtém playback via SDK e deixa o vídeo ao iframe embed + `embed.js`.
22
+ * Opções só nativas (`backgroundPlayback`, `lockScreenNowPlaying`, telemetria ligada ao player nativo, resume SecureStore) são ignoradas aqui.
23
+ */
24
+ export function useMoviiePlayer(
25
+ options: UseMoviiePlayerOptions
26
+ ): UseMoviiePlayerResult {
27
+ const { data, error, isLoading, retry } = useMoviiePlayback({
28
+ embedId: options.embedId,
29
+ })
30
+
31
+ const videoPresentation = useMemo((): MoviieVideoPresentationProps => {
32
+ if (!data) {
33
+ return EMPTY_PRESENTATION
34
+ }
35
+ return buildMoviieVideoPresentationProps({
36
+ pictureInPicture: options.pictureInPicture,
37
+ pictureInPictureAutoStart: options.pictureInPictureAutoStart,
38
+ profile: data.profile,
39
+ showPip: data.controls.showPip,
40
+ })
41
+ }, [
42
+ data,
43
+ options.pictureInPicture,
44
+ options.pictureInPictureAutoStart,
45
+ ])
46
+
47
+ const playback: MoviiePlaybackData | null = data
48
+
49
+ return {
50
+ player: null,
51
+ playback,
52
+ error,
53
+ isLoading,
54
+ videoPresentation,
55
+ retry,
56
+ }
57
+ }
@@ -0,0 +1,133 @@
1
+ import { useEffect, useRef } from "react"
2
+
3
+ import { AppState, type AppStateStatus } from "react-native"
4
+ import type { VideoPlayer } from "expo-video"
5
+ import {
6
+ PLAYBACK_EVENT_TYPE,
7
+ TelemetryClient,
8
+ type MoviiePlaybackData,
9
+ type ViewerTokenStore,
10
+ } from "@moviie/player-sdk"
11
+
12
+ export function useMoviieTelemetry(
13
+ player: VideoPlayer,
14
+ playback: MoviiePlaybackData | null,
15
+ eventsBaseUrl: string,
16
+ defaultHeaders: Record<string, string>,
17
+ viewerTokenStore: ViewerTokenStore
18
+ ): void {
19
+ const hasStartedPlaybackRef = useRef(false)
20
+
21
+ useEffect(() => {
22
+ if (!playback?.telemetry.bootstrapUrl) {
23
+ return
24
+ }
25
+
26
+ const telemetry = new TelemetryClient({
27
+ eventsBaseUrl,
28
+ defaultHeaders,
29
+ viewerTokenStore,
30
+ getPositionSeconds: () => player.currentTime,
31
+ })
32
+
33
+ let disposed = false
34
+ const subs: Array<{ remove: () => void }> = []
35
+
36
+ void (async () => {
37
+ try {
38
+ await telemetry.bootstrap({
39
+ bootstrapUrl: playback.telemetry.bootstrapUrl,
40
+ })
41
+ if (disposed) {
42
+ return
43
+ }
44
+ await telemetry.recordEvent({
45
+ eventType: PLAYBACK_EVENT_TYPE.SESSION_START,
46
+ positionSeconds: 0,
47
+ })
48
+ } catch {
49
+ /* falha de telemetria não bloqueia playback */
50
+ }
51
+ })()
52
+
53
+ subs.push(
54
+ player.addListener("playingChange", ({ isPlaying }) => {
55
+ telemetry.updatePosition(Math.max(0, player.currentTime))
56
+ if (isPlaying) {
57
+ void telemetry.recordEvent({
58
+ eventType: hasStartedPlaybackRef.current
59
+ ? PLAYBACK_EVENT_TYPE.RESUME
60
+ : PLAYBACK_EVENT_TYPE.PLAY_START,
61
+ positionSeconds: Math.floor(Math.max(0, player.currentTime)),
62
+ })
63
+ hasStartedPlaybackRef.current = true
64
+ telemetry.startHeartbeats()
65
+ } else {
66
+ void telemetry.recordEvent({
67
+ eventType: PLAYBACK_EVENT_TYPE.PAUSE,
68
+ positionSeconds: Math.floor(Math.max(0, player.currentTime)),
69
+ })
70
+ telemetry.stopHeartbeats()
71
+ }
72
+ })
73
+ )
74
+
75
+ subs.push(
76
+ player.addListener("playToEnd", () => {
77
+ telemetry.updatePosition(Math.max(0, player.currentTime))
78
+ void telemetry.recordEvent({
79
+ eventType: PLAYBACK_EVENT_TYPE.ENDED,
80
+ positionSeconds: Math.floor(Math.max(0, player.duration)),
81
+ })
82
+ telemetry.stopHeartbeats()
83
+ })
84
+ )
85
+
86
+ subs.push(
87
+ player.addListener("statusChange", ({ status }) => {
88
+ if (status === "error") {
89
+ void telemetry.recordEvent({
90
+ eventType: PLAYBACK_EVENT_TYPE.ERROR,
91
+ positionSeconds: Math.floor(Math.max(0, player.currentTime)),
92
+ errorCode: "player_status_error",
93
+ })
94
+ }
95
+ })
96
+ )
97
+
98
+ subs.push(
99
+ player.addListener("timeUpdate", ({ currentTime }) => {
100
+ telemetry.updatePosition(Math.max(0, currentTime))
101
+ })
102
+ )
103
+
104
+ const appStateSub = AppState.addEventListener(
105
+ "change",
106
+ (next: AppStateStatus) => {
107
+ if (next === "active") {
108
+ telemetry.resumeHeartbeats()
109
+ } else {
110
+ telemetry.pauseHeartbeats()
111
+ void telemetry.flush()
112
+ }
113
+ }
114
+ )
115
+
116
+ return () => {
117
+ disposed = true
118
+ hasStartedPlaybackRef.current = false
119
+ subs.forEach((s) => {
120
+ s.remove()
121
+ })
122
+ appStateSub.remove()
123
+ void telemetry.destroy()
124
+ }
125
+ }, [
126
+ player,
127
+ playback?.embedId,
128
+ playback?.telemetry.bootstrapUrl,
129
+ eventsBaseUrl,
130
+ defaultHeaders,
131
+ viewerTokenStore,
132
+ ])
133
+ }