@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,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
|
+
}
|