@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,27 @@
|
|
|
1
|
+
function pad2(value: number): string {
|
|
2
|
+
return String(Math.floor(value)).padStart(2, "0")
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function formatSegment(totalSeconds: number): string {
|
|
6
|
+
if (!Number.isFinite(totalSeconds) || totalSeconds < 0) {
|
|
7
|
+
return "0:00"
|
|
8
|
+
}
|
|
9
|
+
const seconds = totalSeconds % 60
|
|
10
|
+
const minutes = Math.floor(totalSeconds / 60) % 60
|
|
11
|
+
const hours = Math.floor(totalSeconds / 3600)
|
|
12
|
+
if (hours > 0) {
|
|
13
|
+
return `${hours}:${pad2(minutes)}:${pad2(seconds)}`
|
|
14
|
+
}
|
|
15
|
+
return `${Math.floor(minutes)}:${pad2(seconds)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatPlaybackClock(
|
|
19
|
+
currentSeconds: number,
|
|
20
|
+
durationSeconds: number
|
|
21
|
+
): string {
|
|
22
|
+
const current = formatSegment(currentSeconds)
|
|
23
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
24
|
+
return current
|
|
25
|
+
}
|
|
26
|
+
return `${current} / ${formatSegment(durationSeconds)}`
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ComponentType, PropsWithChildren } from 'react'
|
|
2
|
+
import { Animated as RNAnimated, View, type ViewProps } from 'react-native'
|
|
3
|
+
import AnimatedReanimated from 'react-native-reanimated'
|
|
4
|
+
import type { AnimateProps } from 'react-native-reanimated'
|
|
5
|
+
import RNSvg, {
|
|
6
|
+
Defs as RNDefs,
|
|
7
|
+
LinearGradient as RNLinearGradient,
|
|
8
|
+
Path as RNPath,
|
|
9
|
+
Rect as RNRect,
|
|
10
|
+
Stop as RNStop,
|
|
11
|
+
Text as RNText,
|
|
12
|
+
} from 'react-native-svg'
|
|
13
|
+
import type {
|
|
14
|
+
LinearGradientProps,
|
|
15
|
+
PathProps,
|
|
16
|
+
RectProps,
|
|
17
|
+
StopProps,
|
|
18
|
+
SvgProps,
|
|
19
|
+
TextProps,
|
|
20
|
+
} from 'react-native-svg'
|
|
21
|
+
|
|
22
|
+
type RNCoreAnimatedViewProps = RNAnimated.AnimatedProps<ComponentPropsWithRef<typeof View>>
|
|
23
|
+
|
|
24
|
+
type ReanimatedViewBridgeProps = AnimateProps<ViewProps>
|
|
25
|
+
|
|
26
|
+
export const Svg = RNSvg as unknown as ComponentType<SvgProps>
|
|
27
|
+
|
|
28
|
+
export default Svg
|
|
29
|
+
|
|
30
|
+
export const Path = RNPath as unknown as ComponentType<PathProps>
|
|
31
|
+
|
|
32
|
+
export const Defs = RNDefs as unknown as ComponentType<PropsWithChildren>
|
|
33
|
+
|
|
34
|
+
export const LinearGradient = RNLinearGradient as unknown as ComponentType<LinearGradientProps>
|
|
35
|
+
|
|
36
|
+
export const Rect = RNRect as unknown as ComponentType<RectProps>
|
|
37
|
+
|
|
38
|
+
export const Stop = RNStop as unknown as ComponentType<StopProps>
|
|
39
|
+
|
|
40
|
+
export const SvgText = RNText as unknown as ComponentType<TextProps>
|
|
41
|
+
|
|
42
|
+
export const AnimatedView = RNAnimated.View as unknown as ComponentType<RNCoreAnimatedViewProps>
|
|
43
|
+
|
|
44
|
+
export const ReanimatedView =
|
|
45
|
+
AnimatedReanimated.View as unknown as ComponentType<ReanimatedViewBridgeProps>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MOVIIE_SMART_PROGRESS_CONFIG } from "../constants"
|
|
2
|
+
|
|
3
|
+
function easeOutQuad(t: number): number {
|
|
4
|
+
return t * (2 - t)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function easeInQuad(t: number): number {
|
|
8
|
+
return t * t
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps real playback progress [0–1] to Smart Progress display [0–P_MAX_BEFORE_END],
|
|
13
|
+
* mirroring the embed inline script (`mapProgress` in `templates/player.ts`).
|
|
14
|
+
*/
|
|
15
|
+
export function mapSmartProgressRawRatio(realUnitInterval: number): number {
|
|
16
|
+
const r = Math.min(1, Math.max(0, realUnitInterval))
|
|
17
|
+
const {
|
|
18
|
+
R1,
|
|
19
|
+
P1,
|
|
20
|
+
R2,
|
|
21
|
+
P2,
|
|
22
|
+
P_MAX_BEFORE_END: pMax,
|
|
23
|
+
} = MOVIIE_SMART_PROGRESS_CONFIG
|
|
24
|
+
|
|
25
|
+
if (r <= 0) {
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
if (r >= 1) {
|
|
29
|
+
return pMax
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (r <= R1) {
|
|
33
|
+
return P1 * easeOutQuad(r / R1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (r <= R2) {
|
|
37
|
+
const t = (r - R1) / (R2 - R1)
|
|
38
|
+
return P1 + (P2 - P1) * t
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const t = (r - R2) / (1 - R2)
|
|
42
|
+
return P2 + (pMax - P2) * easeInQuad(t)
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contrato genérico para integração de cast (Chromecast / outras tecnologias futuras).
|
|
3
|
+
*
|
|
4
|
+
* **Zero dependências nativas**. O package `@moviie/player-expo` nunca importa
|
|
5
|
+
* a lib subjacente diretamente — a implementação fica num subpath opcional
|
|
6
|
+
* (`@moviie/player-expo/cast`) que o consumidor importa só se quiser usar.
|
|
7
|
+
*
|
|
8
|
+
* Padrão recomendado: o consumidor cria um adapter via factory do subpath e
|
|
9
|
+
* passa via prop `castAdapter` em `MoviieVideo`. Se omitido, o player funciona
|
|
10
|
+
* normalmente sem botão de cast.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type React from 'react'
|
|
14
|
+
|
|
15
|
+
export type MoviieCastState = 'unavailable' | 'disconnected' | 'connecting' | 'connected'
|
|
16
|
+
|
|
17
|
+
export interface MoviieCastMediaPayload {
|
|
18
|
+
uri: string
|
|
19
|
+
title: string
|
|
20
|
+
posterUrl: string | null
|
|
21
|
+
durationSeconds: number | null
|
|
22
|
+
startSeconds: number
|
|
23
|
+
/** Content type MIME do stream. Default `application/x-mpegURL` (HLS). */
|
|
24
|
+
contentType?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MoviieCastAdapter {
|
|
28
|
+
/**
|
|
29
|
+
* True se a lib nativa está disponível (módulo carregado, plataforma suportada).
|
|
30
|
+
* Quando false, o botão de cast não aparece e o player funciona normalmente.
|
|
31
|
+
*/
|
|
32
|
+
isAvailable(): boolean
|
|
33
|
+
/** Estado atual de discovery / conexão. */
|
|
34
|
+
getState(): MoviieCastState
|
|
35
|
+
/**
|
|
36
|
+
* Inscreve-se em mudanças de estado. Retorna função de unsubscribe que deve
|
|
37
|
+
* ser chamada no cleanup do `useEffect`.
|
|
38
|
+
*/
|
|
39
|
+
subscribe(callback: (state: MoviieCastState) => void): () => void
|
|
40
|
+
/** Abre UI nativa de seleção de dispositivo. */
|
|
41
|
+
showDevicePicker(): Promise<void>
|
|
42
|
+
/** Carrega mídia no dispositivo conectado. No-op se desconectado. */
|
|
43
|
+
loadMedia(payload: MoviieCastMediaPayload): Promise<void>
|
|
44
|
+
/** Encerra sessão ativa de cast. */
|
|
45
|
+
endSession(): Promise<void>
|
|
46
|
+
/**
|
|
47
|
+
* Android only. Renderiza um proxy nativo invisível necessário para que
|
|
48
|
+
* `showDevicePicker()` funcione no Android (o SDK precisa de um
|
|
49
|
+
* `MediaRouteButton` montado na árvore de views).
|
|
50
|
+
* Retorna `null` em iOS / quando não necessário.
|
|
51
|
+
*/
|
|
52
|
+
renderAndroidProxy?(): React.ReactNode
|
|
53
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { MoviieErrorCode } from "@moviie/player-sdk"
|
|
2
|
+
import {
|
|
3
|
+
MoviieAuthError,
|
|
4
|
+
MoviieBundleBlockedError,
|
|
5
|
+
MoviieNetworkError,
|
|
6
|
+
MoviieNotFoundError,
|
|
7
|
+
MoviieRateLimitError,
|
|
8
|
+
MoviieReferrerBlockedError,
|
|
9
|
+
MoviieSubscriptionInactiveError,
|
|
10
|
+
} from "@moviie/player-sdk"
|
|
11
|
+
|
|
12
|
+
import { MOVIIE_DASHBOARD_URL } from "./moviie-shell-tokens"
|
|
13
|
+
|
|
14
|
+
export type MoviieShellCtaKind = "retry" | "external" | "none"
|
|
15
|
+
|
|
16
|
+
export interface MoviieShellErrorDisplay {
|
|
17
|
+
code: MoviieErrorCode
|
|
18
|
+
numericCode: string
|
|
19
|
+
heading: string
|
|
20
|
+
description: string
|
|
21
|
+
ctaLabel: string | null
|
|
22
|
+
ctaKind: MoviieShellCtaKind
|
|
23
|
+
externalUrl: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ERROR_NUMERIC_CODE: Record<MoviieErrorCode, string> = {
|
|
27
|
+
auth: "E1001",
|
|
28
|
+
not_found: "E1002",
|
|
29
|
+
bundle_blocked: "E1003",
|
|
30
|
+
referrer_blocked: "E1004",
|
|
31
|
+
subscription_inactive: "E1005",
|
|
32
|
+
network: "E2001",
|
|
33
|
+
rate_limit: "E2002",
|
|
34
|
+
unknown: "E9999",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveErrorCode(error: Error): MoviieErrorCode {
|
|
38
|
+
if (error instanceof MoviieAuthError) return "auth"
|
|
39
|
+
if (error instanceof MoviieNotFoundError) return "not_found"
|
|
40
|
+
if (error instanceof MoviieBundleBlockedError) return "bundle_blocked"
|
|
41
|
+
if (error instanceof MoviieReferrerBlockedError) return "referrer_blocked"
|
|
42
|
+
if (error instanceof MoviieSubscriptionInactiveError) return "subscription_inactive"
|
|
43
|
+
if (error instanceof MoviieNetworkError) return "network"
|
|
44
|
+
if (error instanceof MoviieRateLimitError) return "rate_limit"
|
|
45
|
+
const code = (error as { code?: unknown }).code
|
|
46
|
+
if (typeof code === "string") {
|
|
47
|
+
const known: MoviieErrorCode[] = [
|
|
48
|
+
"auth",
|
|
49
|
+
"not_found",
|
|
50
|
+
"bundle_blocked",
|
|
51
|
+
"referrer_blocked",
|
|
52
|
+
"subscription_inactive",
|
|
53
|
+
"network",
|
|
54
|
+
"rate_limit",
|
|
55
|
+
]
|
|
56
|
+
if ((known as string[]).includes(code)) {
|
|
57
|
+
return code as MoviieErrorCode
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return "unknown"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getErrorDisplay(error: Error): MoviieShellErrorDisplay {
|
|
64
|
+
const code = resolveErrorCode(error)
|
|
65
|
+
|
|
66
|
+
switch (code) {
|
|
67
|
+
case "auth":
|
|
68
|
+
return {
|
|
69
|
+
code,
|
|
70
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
71
|
+
heading: "Configuração inválida",
|
|
72
|
+
description: "Verifique a chave publicável do SDK.",
|
|
73
|
+
ctaLabel: "Acessar Moviie",
|
|
74
|
+
ctaKind: "external",
|
|
75
|
+
externalUrl: MOVIIE_DASHBOARD_URL,
|
|
76
|
+
}
|
|
77
|
+
case "not_found":
|
|
78
|
+
return {
|
|
79
|
+
code,
|
|
80
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
81
|
+
heading: "Reprodução indisponível",
|
|
82
|
+
description: "Não conseguimos encontrar este vídeo.",
|
|
83
|
+
ctaLabel: "Acessar Moviie",
|
|
84
|
+
ctaKind: "external",
|
|
85
|
+
externalUrl: MOVIIE_DASHBOARD_URL,
|
|
86
|
+
}
|
|
87
|
+
case "bundle_blocked":
|
|
88
|
+
return {
|
|
89
|
+
code,
|
|
90
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
91
|
+
heading: "Aplicativo não autorizado",
|
|
92
|
+
description: "Este aplicativo não está autorizado a reproduzir este vídeo.",
|
|
93
|
+
ctaLabel: "Ajustar permissões",
|
|
94
|
+
ctaKind: "external",
|
|
95
|
+
externalUrl: MOVIIE_DASHBOARD_URL,
|
|
96
|
+
}
|
|
97
|
+
case "referrer_blocked":
|
|
98
|
+
return {
|
|
99
|
+
code,
|
|
100
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
101
|
+
heading: "Origem não autorizada",
|
|
102
|
+
description: "Origem não autorizada para reprodução.",
|
|
103
|
+
ctaLabel: "Acessar Moviie",
|
|
104
|
+
ctaKind: "external",
|
|
105
|
+
externalUrl: MOVIIE_DASHBOARD_URL,
|
|
106
|
+
}
|
|
107
|
+
case "subscription_inactive":
|
|
108
|
+
return {
|
|
109
|
+
code,
|
|
110
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
111
|
+
heading: "Reprodução temporariamente indisponível",
|
|
112
|
+
description: "Acesse a Moviie para continuar assistindo.",
|
|
113
|
+
ctaLabel: "Ir para a Moviie",
|
|
114
|
+
ctaKind: "external",
|
|
115
|
+
externalUrl: MOVIIE_DASHBOARD_URL,
|
|
116
|
+
}
|
|
117
|
+
case "network":
|
|
118
|
+
return {
|
|
119
|
+
code,
|
|
120
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
121
|
+
heading: "Erro de conexão",
|
|
122
|
+
description: "Verifique sua conexão e tente novamente.",
|
|
123
|
+
ctaLabel: "Tentar de novo",
|
|
124
|
+
ctaKind: "retry",
|
|
125
|
+
externalUrl: null,
|
|
126
|
+
}
|
|
127
|
+
case "rate_limit":
|
|
128
|
+
return {
|
|
129
|
+
code,
|
|
130
|
+
numericCode: ERROR_NUMERIC_CODE[code],
|
|
131
|
+
heading: "Muitas solicitações",
|
|
132
|
+
description: "Aguarde alguns instantes e tente novamente.",
|
|
133
|
+
ctaLabel: "Tentar de novo",
|
|
134
|
+
ctaKind: "retry",
|
|
135
|
+
externalUrl: null,
|
|
136
|
+
}
|
|
137
|
+
case "unknown":
|
|
138
|
+
default:
|
|
139
|
+
return {
|
|
140
|
+
code: "unknown",
|
|
141
|
+
numericCode: ERROR_NUMERIC_CODE["unknown"],
|
|
142
|
+
heading: "Erro temporário",
|
|
143
|
+
description: "Tente novamente em instantes.",
|
|
144
|
+
ctaLabel: "Tentar de novo",
|
|
145
|
+
ctaKind: "retry",
|
|
146
|
+
externalUrl: null,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const MOVIIE_SHELL_BACKGROUND_HEX = "#0a0a0a" as const
|
|
2
|
+
export const MOVIIE_SHELL_RADIAL_INNER_HEX = "#1a1a1a" as const
|
|
3
|
+
export const MOVIIE_SHELL_RADIAL_OUTER_HEX = "#000000" as const
|
|
4
|
+
|
|
5
|
+
export const MOVIIE_SHELL_HEADING_HEX = "#f8fafc" as const
|
|
6
|
+
export const MOVIIE_SHELL_DESCRIPTION_HEX = "#e5e7eb" as const
|
|
7
|
+
export const MOVIIE_SHELL_ERROR_CODE_HEX = "#4b5563" as const
|
|
8
|
+
|
|
9
|
+
export const MOVIIE_SHELL_BUTTON_BG_HEX = "#0a0a0a" as const
|
|
10
|
+
export const MOVIIE_SHELL_BUTTON_BORDER_RGBA = "rgba(255,255,255,0.15)" as const
|
|
11
|
+
export const MOVIIE_SHELL_BUTTON_LABEL_HEX = "#f8fafc" as const
|
|
12
|
+
|
|
13
|
+
export const MOVIIE_SHELL_HEADING_FONT_SIZE_PX = 22 as const
|
|
14
|
+
export const MOVIIE_SHELL_DESCRIPTION_FONT_SIZE_PX = 14 as const
|
|
15
|
+
export const MOVIIE_SHELL_BUTTON_LABEL_FONT_SIZE_PX = 14 as const
|
|
16
|
+
|
|
17
|
+
export const MOVIIE_SHELL_CONTENT_MAX_WIDTH_PX = 280 as const
|
|
18
|
+
|
|
19
|
+
export const MOVIIE_SHELL_BUTTON_PADDING_H_PX = 22 as const
|
|
20
|
+
export const MOVIIE_SHELL_BUTTON_PADDING_V_PX = 12 as const
|
|
21
|
+
export const MOVIIE_SHELL_BUTTON_RADIUS_PX = 9999 as const
|
|
22
|
+
|
|
23
|
+
export const MOVIIE_SHELL_CROSSFADE_DURATION_MS = 240 as const
|
|
24
|
+
|
|
25
|
+
export const MOVIIE_SHELL_DEFAULT_ASPECT_RATIO = 16 / 9
|
|
26
|
+
|
|
27
|
+
export const MOVIIE_DASHBOARD_URL = "https://app.moviie.ai" as const
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks opt-in para telemetria do consumer (Sentry, PostHog, Datadog, etc.).
|
|
3
|
+
*
|
|
4
|
+
* `@moviie/player-expo` já envia telemetria interna para o backend Moviie via
|
|
5
|
+
* `TelemetryClient` (impressions, playback duration). Estes callbacks servem
|
|
6
|
+
* exclusivamente para o consumer também observar eventos em **sua própria**
|
|
7
|
+
* pipeline de monitoring — não substituem nem ampliam a telemetria interna.
|
|
8
|
+
*
|
|
9
|
+
* **Garantias**:
|
|
10
|
+
* - Callbacks são síncronos e não-bloqueantes: exceções neles são engolidas e
|
|
11
|
+
* loggadas via `console.warn` (não derrubam o player).
|
|
12
|
+
* - Sem PII: eventos só carregam `embedId` público e métricas técnicas.
|
|
13
|
+
* - Cada evento é entregue at-most-once por ciclo de vida do player.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type MoviieTelemetryErrorPhase =
|
|
17
|
+
/** Falha ao carregar metadata de playback (`useMoviiePlayback`). */
|
|
18
|
+
| 'playback_fetch'
|
|
19
|
+
/** Falha durante reprodução (network stall, decode error, DRM, etc.). */
|
|
20
|
+
| 'playback_runtime'
|
|
21
|
+
/** Falha capturada pelo `MoviieErrorBoundary` (render/effect throw). */
|
|
22
|
+
| 'render_boundary'
|
|
23
|
+
|
|
24
|
+
export interface MoviieTelemetryErrorContext {
|
|
25
|
+
/** UUID público do embed em causa, se disponível. */
|
|
26
|
+
embedId?: string
|
|
27
|
+
/** Categorização da fase onde o erro ocorreu. */
|
|
28
|
+
phase: MoviieTelemetryErrorPhase
|
|
29
|
+
/** Código de erro classificado pelo SDK (quando aplicável). */
|
|
30
|
+
code?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type MoviieTelemetryLifecycleEventName =
|
|
34
|
+
| 'playback_loaded'
|
|
35
|
+
| 'playback_started'
|
|
36
|
+
| 'playback_paused'
|
|
37
|
+
| 'playback_seeking'
|
|
38
|
+
| 'playback_ended'
|
|
39
|
+
| 'fullscreen_enter'
|
|
40
|
+
| 'fullscreen_exit'
|
|
41
|
+
| 'pip_enter'
|
|
42
|
+
| 'pip_exit'
|
|
43
|
+
| 'cast_session_started'
|
|
44
|
+
| 'cast_session_ended'
|
|
45
|
+
|
|
46
|
+
export interface MoviieTelemetryLifecycleEvent {
|
|
47
|
+
name: MoviieTelemetryLifecycleEventName
|
|
48
|
+
embedId?: string
|
|
49
|
+
/** Posição (segundos) no momento do evento, quando relevante. */
|
|
50
|
+
positionSeconds?: number
|
|
51
|
+
/** Duração total (segundos) quando conhecida. */
|
|
52
|
+
durationSeconds?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type MoviieTelemetryOnError = (error: Error, context: MoviieTelemetryErrorContext) => void
|
|
56
|
+
|
|
57
|
+
export type MoviieTelemetryOnEvent = (event: MoviieTelemetryLifecycleEvent) => void
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper para invocar callbacks de consumer sem deixar exceções escalar.
|
|
61
|
+
* Use em vez de chamar `onError` / `onEvent` diretamente do hook/efeito.
|
|
62
|
+
*/
|
|
63
|
+
export function safeInvokeMoviieTelemetry<TArgs extends unknown[]>(
|
|
64
|
+
callback: ((...args: TArgs) => void) | undefined,
|
|
65
|
+
...args: TArgs
|
|
66
|
+
): void {
|
|
67
|
+
if (callback == null) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
callback(...args)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.warn('[@moviie/player-expo] telemetry callback threw — ignorado', err)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper opcional para `expo-status-bar`. Quando o módulo não está instalado,
|
|
3
|
+
* `setStatusBarHidden` vira no-op e um warn é disparado uma única vez por sessão.
|
|
4
|
+
*
|
|
5
|
+
* Modo degradado: a status bar fica visível em fullscreen JS (overlay leve por
|
|
6
|
+
* cima do vídeo). Funcionalidade core do player não é afetada.
|
|
7
|
+
*/
|
|
8
|
+
import { warnOnce } from './warn-once'
|
|
9
|
+
|
|
10
|
+
type StatusBarAnimation = 'none' | 'fade' | 'slide'
|
|
11
|
+
|
|
12
|
+
type ExpoStatusBarModule = {
|
|
13
|
+
setStatusBarHidden: (hidden: boolean, animation?: StatusBarAnimation) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let cachedModule: ExpoStatusBarModule | null | undefined
|
|
17
|
+
|
|
18
|
+
function loadModule(): ExpoStatusBarModule | null {
|
|
19
|
+
if (cachedModule !== undefined) {
|
|
20
|
+
return cachedModule
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
cachedModule = require('expo-status-bar') as ExpoStatusBarModule
|
|
25
|
+
} catch {
|
|
26
|
+
cachedModule = null
|
|
27
|
+
warnOnce(
|
|
28
|
+
'moviie:expo-status-bar-missing',
|
|
29
|
+
'[@moviie/player-expo] `expo-status-bar` não está instalado. Status bar não será ocultada em fullscreen JS. ' +
|
|
30
|
+
'Instala com `pnpm add expo-status-bar` se quiser este comportamento.'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return cachedModule
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function moviieSetStatusBarHidden(
|
|
37
|
+
hidden: boolean,
|
|
38
|
+
animation: StatusBarAnimation = 'fade'
|
|
39
|
+
): void {
|
|
40
|
+
const mod = loadModule()
|
|
41
|
+
if (mod == null) return
|
|
42
|
+
try {
|
|
43
|
+
mod.setStatusBarHidden(hidden, animation)
|
|
44
|
+
} catch {
|
|
45
|
+
// Defensive: se o módulo está carregado mas o método explode, ignora —
|
|
46
|
+
// status bar não é critical path.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ViewStyle } from "react-native"
|
|
2
|
+
|
|
3
|
+
type LooseViewStyle = Record<string, unknown>
|
|
4
|
+
|
|
5
|
+
/** Props visuais da área do vídeo — não devem envolver a faixa de progresso abaixo do vídeo. */
|
|
6
|
+
const VIDEO_SURFACE_STYLE_KEYS = new Set([
|
|
7
|
+
"backgroundColor",
|
|
8
|
+
"borderRadius",
|
|
9
|
+
"borderTopLeftRadius",
|
|
10
|
+
"borderTopRightRadius",
|
|
11
|
+
"borderBottomLeftRadius",
|
|
12
|
+
"borderBottomRightRadius",
|
|
13
|
+
"borderCurve",
|
|
14
|
+
"borderWidth",
|
|
15
|
+
"borderColor",
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Espera `flatStyle` já planificado com `StyleSheet.flatten` no runtime RN
|
|
20
|
+
* (resolve IDs devolvidos por `StyleSheet.create`).
|
|
21
|
+
*/
|
|
22
|
+
export function partitionMoviieVideoHostStyle(
|
|
23
|
+
flatStyle: ViewStyle | undefined | null
|
|
24
|
+
): {
|
|
25
|
+
columnStyle: ViewStyle | undefined
|
|
26
|
+
surfaceStyle: ViewStyle | undefined
|
|
27
|
+
} {
|
|
28
|
+
if (flatStyle == null || Object.keys(flatStyle).length === 0) {
|
|
29
|
+
return { columnStyle: undefined, surfaceStyle: undefined }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const flat = flatStyle as LooseViewStyle
|
|
33
|
+
const column: LooseViewStyle = {}
|
|
34
|
+
const surface: LooseViewStyle = {}
|
|
35
|
+
|
|
36
|
+
for (const key of Object.keys(flat)) {
|
|
37
|
+
const value = flat[key]
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (VIDEO_SURFACE_STYLE_KEYS.has(key)) {
|
|
42
|
+
surface[key] = value
|
|
43
|
+
} else {
|
|
44
|
+
column[key] = value
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
columnStyle:
|
|
50
|
+
Object.keys(column).length > 0 ? (column as ViewStyle) : undefined,
|
|
51
|
+
surfaceStyle:
|
|
52
|
+
Object.keys(surface).length > 0 ? (surface as ViewStyle) : undefined,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { MOVIIE_REMEMBER_POSITION_STORAGE_PREFIX } from '../constants'
|
|
2
|
+
import { warnOnce } from './warn-once'
|
|
3
|
+
|
|
4
|
+
const RESUME_SECURE_STORE_MISSING_WARNING =
|
|
5
|
+
'[@moviie/player-expo] `expo-secure-store` não está instalado — resume position não será persistido. ' +
|
|
6
|
+
'Instala com `pnpm add expo-secure-store` se quiseres retomar onde o utilizador parou.'
|
|
7
|
+
|
|
8
|
+
const SECURE_STORE_KEY_SAFE_PATTERN = /^[A-Za-z0-9._-]+$/
|
|
9
|
+
|
|
10
|
+
function isSecureStoreSafeEmbedId(embedId: string): boolean {
|
|
11
|
+
return SECURE_STORE_KEY_SAFE_PATTERN.test(embedId)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function rememberPlaybackPositionStorageKey(embedId: string): string {
|
|
15
|
+
return `${MOVIIE_REMEMBER_POSITION_STORAGE_PREFIX}${embedId}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseStoredResumeSeconds(raw: string | null): number | null {
|
|
19
|
+
if (raw == null || raw === '') {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
const n = Number(raw)
|
|
23
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
return n
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clampResumeTimeSeconds(
|
|
30
|
+
seconds: number | null,
|
|
31
|
+
durationSeconds: number | null | undefined
|
|
32
|
+
): number | null {
|
|
33
|
+
if (seconds == null || !Number.isFinite(seconds) || seconds < 0.25) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
if (durationSeconds != null && Number.isFinite(durationSeconds) && durationSeconds > 0) {
|
|
37
|
+
if (seconds >= durationSeconds - 1) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
return Math.min(seconds, durationSeconds - 0.25)
|
|
41
|
+
}
|
|
42
|
+
return seconds
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadSecureStore(): Promise<typeof import('expo-secure-store') | null> {
|
|
46
|
+
try {
|
|
47
|
+
return await import('expo-secure-store')
|
|
48
|
+
} catch {
|
|
49
|
+
warnOnce('moviie:expo-secure-store-missing-resume', RESUME_SECURE_STORE_MISSING_WARNING)
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readStoredResumeSeconds(embedId: string): Promise<number | null> {
|
|
55
|
+
if (!isSecureStoreSafeEmbedId(embedId)) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
const SecureStore = await loadSecureStore()
|
|
59
|
+
if (!SecureStore) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const raw = await SecureStore.getItemAsync(rememberPlaybackPositionStorageKey(embedId))
|
|
64
|
+
return parseStoredResumeSeconds(raw)
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function writeStoredResumeSeconds(embedId: string, seconds: number): Promise<void> {
|
|
71
|
+
if (!isSecureStoreSafeEmbedId(embedId)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const SecureStore = await loadSecureStore()
|
|
75
|
+
if (!SecureStore) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await SecureStore.setItemAsync(rememberPlaybackPositionStorageKey(embedId), String(seconds))
|
|
80
|
+
} catch {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function clearStoredResumeSeconds(embedId: string): Promise<void> {
|
|
86
|
+
if (!isSecureStoreSafeEmbedId(embedId)) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
const SecureStore = await loadSecureStore()
|
|
90
|
+
if (!SecureStore) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await SecureStore.deleteItemAsync(rememberPlaybackPositionStorageKey(embedId))
|
|
95
|
+
} catch {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MOVIIE_SKIN_LAYOUT_REFERENCE_WIDTH_PT,
|
|
3
|
+
MOVIIE_SKIN_LAYOUT_SCALE_MAX,
|
|
4
|
+
MOVIIE_SKIN_LAYOUT_SCALE_MIN,
|
|
5
|
+
} from "../constants"
|
|
6
|
+
|
|
7
|
+
function clampBetween(value: number, min: number, max: number): number {
|
|
8
|
+
return Math.min(Math.max(value, min), max)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveMoviieSkinLayoutScale(windowWidth: number): number {
|
|
12
|
+
if (!Number.isFinite(windowWidth) || windowWidth <= 0) {
|
|
13
|
+
return 1
|
|
14
|
+
}
|
|
15
|
+
const raw = windowWidth / MOVIIE_SKIN_LAYOUT_REFERENCE_WIDTH_PT
|
|
16
|
+
return clampBetween(raw, MOVIIE_SKIN_LAYOUT_SCALE_MIN, MOVIIE_SKIN_LAYOUT_SCALE_MAX)
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE,
|
|
3
|
+
type MoviieExpoOrientationLockValue,
|
|
4
|
+
} from "../constants"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mapeia a rotação Z do palco fullscreen para o valor de `OrientationLock` do `expo-screen-orientation`,
|
|
8
|
+
* usando {@link MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE} (espelho Enum SDK 52).
|
|
9
|
+
*/
|
|
10
|
+
export function resolveOrientationLockValueForRotateZDeg(
|
|
11
|
+
rotateZDeg: number
|
|
12
|
+
): MoviieExpoOrientationLockValue {
|
|
13
|
+
const normalized = ((rotateZDeg % 360) + 360) % 360
|
|
14
|
+
if (normalized === 90) {
|
|
15
|
+
return MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE.LANDSCAPE_RIGHT
|
|
16
|
+
}
|
|
17
|
+
if (normalized === 270) {
|
|
18
|
+
return MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE.LANDSCAPE_LEFT
|
|
19
|
+
}
|
|
20
|
+
return MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE.LANDSCAPE
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { MOVIIE_SKIN_DEFAULT_ACCENT_HEX } from "../constants"
|
|
2
|
+
|
|
3
|
+
const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/
|
|
4
|
+
|
|
5
|
+
export function resolveSkinAccentColor(primaryColor: string | null): string {
|
|
6
|
+
if (primaryColor != null && HEX_COLOR_REGEX.test(primaryColor)) {
|
|
7
|
+
return primaryColor
|
|
8
|
+
}
|
|
9
|
+
return MOVIIE_SKIN_DEFAULT_ACCENT_HEX
|
|
10
|
+
}
|