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