@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,167 @@
1
+ import {
2
+ createElement,
3
+ forwardRef,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ type ForwardedRef,
8
+ } from "react"
9
+ import { View } from "react-native"
10
+
11
+ import {
12
+ MOVIIE_PLAYER_EXPO_EMBED_JS_MARKER_ATTR,
13
+ MOVIIE_WATCH_EMBED_JS_PATH,
14
+ } from "../constants"
15
+ import { resolveMoviieWatchOrigin } from "../lib/build-moviie-watch-embed-url"
16
+ import { resolveWebEmbedIframeSrc } from "../lib/resolve-web-embed-iframe-src"
17
+
18
+ import type { MoviieVideoProps, MoviieWatchEmbedIframeHandle } from "./moviie-video-props"
19
+ import type { MoviieVideoSurfaceRef } from "./controls/moviie-controls"
20
+
21
+ function assignForwardedRef<T>(
22
+ forwarded: ForwardedRef<T>,
23
+ instance: T | null
24
+ ): void {
25
+ if (typeof forwarded === "function") {
26
+ forwarded(instance)
27
+ return
28
+ }
29
+ if (forwarded) {
30
+ forwarded.current = instance
31
+ }
32
+ }
33
+
34
+ function toEmbedIframeHandle(
35
+ node: HTMLIFrameElement | null
36
+ ): MoviieWatchEmbedIframeHandle | null {
37
+ if (node == null) {
38
+ return null
39
+ }
40
+ return {
41
+ get contentWindow() {
42
+ const win = node.contentWindow
43
+ if (win == null) {
44
+ return null
45
+ }
46
+ return {
47
+ postMessage(message: unknown, targetOrigin: string): void {
48
+ win.postMessage(message, targetOrigin)
49
+ },
50
+ }
51
+ },
52
+ }
53
+ }
54
+
55
+ const WEB_VIDEO_SURFACE_STUB: MoviieVideoSurfaceRef = {
56
+ async enterFullscreen(): Promise<void> {
57
+ /* iframe UI gerido pelo embed */
58
+ },
59
+ async exitFullscreen(): Promise<void> {
60
+ /* iframe UI gerido pelo embed */
61
+ },
62
+ async startPictureInPicture(): Promise<void> {
63
+ /* não aplicável ao stub web */
64
+ },
65
+ async stopPictureInPicture(): Promise<void> {
66
+ /* não aplicável ao stub web */
67
+ },
68
+ }
69
+
70
+ export const MoviieVideo = forwardRef<MoviieVideoSurfaceRef, MoviieVideoProps>(
71
+ function MoviieVideo(props, forwardedRef) {
72
+ const {
73
+ player: _player,
74
+ playback = null,
75
+ children,
76
+ style,
77
+ loadEmbedScript = true,
78
+ onEmbedIframeRef,
79
+ nativeControls: _nativeControls,
80
+ native: _native,
81
+ controlsAutoHideMs: _controlsAutoHideMs,
82
+ controlsVisibleByDefault: _controlsVisibleByDefault,
83
+ pictureInPicture: _pictureInPicture,
84
+ pictureInPictureAutoStart: _pictureInPictureAutoStart,
85
+ } = props
86
+
87
+ useEffect(() => {
88
+ assignForwardedRef(forwardedRef, WEB_VIDEO_SURFACE_STUB)
89
+ return () => {
90
+ assignForwardedRef(forwardedRef, null)
91
+ }
92
+ }, [forwardedRef])
93
+
94
+ useEffect(() => {
95
+ if (!loadEmbedScript) {
96
+ return
97
+ }
98
+ if (typeof document === "undefined") {
99
+ return
100
+ }
101
+ const origin = resolveMoviieWatchOrigin()
102
+ const scriptSrc = `${origin}${MOVIIE_WATCH_EMBED_JS_PATH}`
103
+ const selector = `script[${MOVIIE_PLAYER_EXPO_EMBED_JS_MARKER_ATTR}]`
104
+ if (document.querySelector(selector) != null) {
105
+ return
106
+ }
107
+ const s = document.createElement("script")
108
+ s.setAttribute(MOVIIE_PLAYER_EXPO_EMBED_JS_MARKER_ATTR, "1")
109
+ s.src = scriptSrc
110
+ s.async = true
111
+ document.head.appendChild(s)
112
+ }, [loadEmbedScript])
113
+
114
+ const iframeSrc = useMemo(
115
+ () => resolveWebEmbedIframeSrc(playback),
116
+ [playback]
117
+ )
118
+
119
+ const iframeTitle = playback?.title ?? "Vídeo"
120
+
121
+ const bridgeIframeRef = useCallback(
122
+ (node: HTMLIFrameElement | null) => {
123
+ onEmbedIframeRef?.(toEmbedIframeHandle(node))
124
+ },
125
+ [onEmbedIframeRef]
126
+ )
127
+
128
+ const iframeStyle = useMemo(
129
+ () => ({
130
+ borderWidth: 0,
131
+ flexGrow: 1,
132
+ width: "100%" as const,
133
+ height: "100%" as const,
134
+ }),
135
+ []
136
+ )
137
+
138
+ if (iframeSrc == null) {
139
+ if (__DEV__) {
140
+ console.warn(
141
+ "[@moviie/player-expo] MoviieVideo (web) precisa de `playback.embedId` — obtenha `playback` com `useMoviiePlayer`."
142
+ )
143
+ }
144
+ return (
145
+ <View style={[{ flexGrow: 1, alignSelf: "stretch" }, style]}>
146
+ {children}
147
+ </View>
148
+ )
149
+ }
150
+
151
+ return (
152
+ <View style={[{ flexGrow: 1, alignSelf: "stretch" }, style]}>
153
+ {createElement("iframe", {
154
+ ref: bridgeIframeRef,
155
+ src: iframeSrc,
156
+ title: iframeTitle,
157
+ allow:
158
+ "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen",
159
+ allowFullScreen: true,
160
+ loading: "lazy",
161
+ style: iframeStyle,
162
+ })}
163
+ {children}
164
+ </View>
165
+ )
166
+ }
167
+ )
@@ -0,0 +1,53 @@
1
+ import { useCallback } from "react"
2
+ import { Linking, Pressable, StyleSheet, Text, View } from "react-native"
3
+
4
+ import type { MoviiePlaybackData } from "@moviie/player-sdk"
5
+
6
+ import { MOVIIE_SKIN_WATERMARK_OPACITY } from "../../constants"
7
+ import { buildMoviieBrandingMarketingUrl } from "../../lib/build-moviie-branding-marketing-url"
8
+
9
+ export function MoviieWatermark({ playback }: { playback: MoviiePlaybackData }) {
10
+ if (!playback.branding.showWatermark) {
11
+ return null
12
+ }
13
+
14
+ const openMarketingSite = useCallback(() => {
15
+ void Linking.openURL(buildMoviieBrandingMarketingUrl())
16
+ }, [])
17
+
18
+ return (
19
+ <View style={styles.host} pointerEvents="box-none">
20
+ <Pressable
21
+ accessibilityRole="link"
22
+ accessibilityHint="Abre o site da Moviie no navegador"
23
+ accessibilityLabel="Marca Moviie"
24
+ onPress={openMarketingSite}
25
+ style={styles.mark}
26
+ >
27
+ <Text style={styles.markText}>Moviie</Text>
28
+ </Pressable>
29
+ </View>
30
+ )
31
+ }
32
+
33
+ const styles = StyleSheet.create({
34
+ host: {
35
+ ...StyleSheet.absoluteFillObject,
36
+ justifyContent: "flex-end",
37
+ alignItems: "flex-end",
38
+ padding: 12,
39
+ pointerEvents: "box-none",
40
+ },
41
+ mark: {
42
+ opacity: MOVIIE_SKIN_WATERMARK_OPACITY,
43
+ paddingHorizontal: 8,
44
+ paddingVertical: 4,
45
+ borderRadius: 6,
46
+ backgroundColor: "rgba(0,0,0,0.35)",
47
+ },
48
+ markText: {
49
+ color: "#ffffff",
50
+ fontSize: 12,
51
+ fontWeight: "600",
52
+ },
53
+ })
@@ -0,0 +1,374 @@
1
+ export const MOVIIE_PLAYER_EXPO_PKG_VERSION = '0.4.0' as const
2
+
3
+ export const MOVIIE_PLAYBACK_MEMORY_CACHE_TTL_MS = 5 * 60 * 1000
4
+
5
+ /** Playback profile strings returned by the playback API (aligned with web embed). */
6
+ export const MOVIIE_PLAYBACK_PROFILE = {
7
+ VSL: 'vsl',
8
+ CUSTOM: 'custom',
9
+ } as const
10
+
11
+ /**
12
+ * Smart Progress curve — mirrors {@link SMART_PROGRESS_CONFIG} on the web embed
13
+ * (`apps/web/src/app/embed/[id]/constants.ts`).
14
+ */
15
+ export const MOVIIE_SMART_PROGRESS_CONFIG = {
16
+ R1: 0.18,
17
+ P1: 0.62,
18
+ R2: 0.8,
19
+ P2: 0.95,
20
+ P_MAX_BEFORE_END: 0.99,
21
+ } as const
22
+
23
+ /** Fill animation duration (ms); aligns with web `.moviie-smart-progress-fill` transition. */
24
+ export const MOVIIE_SMART_PROGRESS_FILL_TRANSITION_MS = 150 as const
25
+
26
+ /** Absolute bottom bar height (dp) for native Smart Progress strip. */
27
+ export const MOVIIE_SMART_PROGRESS_BAR_HEIGHT_DP = 12 as const
28
+
29
+ export const MOVIIE_SMART_PROGRESS_Z_INDEX = 34 as const
30
+
31
+ /**
32
+ * SecureStore key prefix for resume position (`{prefix}{embedId}`).
33
+ * Embed IDs are UUIDs from the API.
34
+ */
35
+ export const MOVIIE_REMEMBER_POSITION_STORAGE_PREFIX = 'moviie.resume_position.' as const
36
+
37
+ /** Debounce interval before persisting resume position while playing. */
38
+ export const MOVIIE_REMEMBER_POSITION_DEBOUNCE_MS = 5000 as const
39
+
40
+ /** Matches embed/web framing defaults when `contentFit` is omitted on `MoviieVideo`. */
41
+ export const MOVIIE_VIDEO_DEFAULT_CONTENT_FIT = {
42
+ VERTICAL: 'contain',
43
+ HORIZONTAL: 'cover',
44
+ } as const
45
+
46
+ export const MOVIIE_BRANDING_MARKETING_ORIGIN = 'https://moviie.ai' as const
47
+
48
+ /** Origin público da página watch/embed (sem barra final). Override: `EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN`. */
49
+ export const MOVIIE_WATCH_ORIGIN_DEFAULT = 'https://watch.moviie.ai' as const
50
+
51
+ /**
52
+ * Variável `EXPO_PUBLIC_*` para override do origin watch/embed em Expo Web (`resolveMoviieWatchOrigin`).
53
+ */
54
+ export const EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN_ENV = 'EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN' as const
55
+
56
+ /** Path público do script da API JS do embed (relativo ao watch origin). */
57
+ export const MOVIIE_WATCH_EMBED_JS_PATH = '/embed.js' as const
58
+
59
+ /**
60
+ * Atributo único no `<script>` injectado em Expo Web para evitar duplicar `embed.js`
61
+ * quando há vários `MoviieVideo` montados.
62
+ */
63
+ export const MOVIIE_PLAYER_EXPO_EMBED_JS_MARKER_ATTR = 'data-moviie-player-expo-embed-js' as const
64
+
65
+ /**
66
+ * Variável `EXPO_PUBLIC_*` que override a base da API Moviie em **dev only**.
67
+ * O Metro inlina qualquer `EXPO_PUBLIC_*` em build time, sem precisar de
68
+ * `expo-constants`. Em produção o SDK usa sempre o default `https://api.moviie.ai/v1`.
69
+ *
70
+ * Exemplo `.env.local`:
71
+ * EXPO_PUBLIC_MOVIIE_API_BASE_URL=http://localhost:3000/api/v1
72
+ */
73
+ export const EXPO_PUBLIC_MOVIIE_API_BASE_URL_ENV = 'EXPO_PUBLIC_MOVIIE_API_BASE_URL' as const
74
+
75
+ /**
76
+ * Variável `EXPO_PUBLIC_*` que override a base de telemetria em **dev only**.
77
+ * Caso omitida, deriva-se de `{origin}/telemetry/v1` a partir de `apiBaseUrl`.
78
+ */
79
+ export const EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL_ENV = 'EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL' as const
80
+
81
+ export const MOVIIE_BRANDING_UTM_PARAM_SOURCE = 'utm_source' as const
82
+ export const MOVIIE_BRANDING_UTM_PARAM_MEDIUM = 'utm_medium' as const
83
+ export const MOVIIE_BRANDING_UTM_PARAM_CAMPAIGN = 'utm_campaign' as const
84
+
85
+ export const MOVIIE_BRANDING_UTM_SOURCE_VALUE = 'player-expo' as const
86
+ export const MOVIIE_BRANDING_UTM_MEDIUM_VALUE = 'branding' as const
87
+
88
+ /** Espelha `PLAYER_EMBED_BRAND_UTM` do embed web (`apps/web/src/app/embed/[id]/constants.ts`). */
89
+ export const MOVIIE_EMBED_BRAND_UTM = {
90
+ SOURCE: 'moviie_embed_player',
91
+ MEDIUM: 'video_control_bar',
92
+ CAMPAIGN: 'logo_click',
93
+ } as const
94
+
95
+ export const MOVIIE_SKIN_DEFAULT_ACCENT_HEX = '#6366f1' as const
96
+
97
+ /**
98
+ * Valor legado para a prop `controlsAutoHideMs` / `autoHideMs`: o chrome da skin Moviie já não
99
+ * usa temporizador para ocultar — só ao toque fora ou ações (play, seek).
100
+ */
101
+ export const MOVIIE_SKIN_CONTROLS_AUTO_HIDE_MS = 3000 as const
102
+
103
+ export const MOVIIE_SKIN_CONTROLS_FADE_MS = 240 as const
104
+
105
+ export const MOVIIE_SKIN_WATERMARK_OPACITY = 0.7 as const
106
+
107
+ export const MOVIIE_SKIN_CONTROL_ICON_PX = 22 as const
108
+
109
+ /**
110
+ * Largura lógica de referência do layout da skin (ex.: frame Figma / iPhone ~390pt).
111
+ * Dimensões em `MOVIIE_SKIN_*_AT_REFERENCE` / tokens da timeline escalam com `windowWidth`.
112
+ */
113
+ export const MOVIIE_SKIN_LAYOUT_REFERENCE_WIDTH_PT = 390 as const
114
+
115
+ /** Limite inferior do factor de escala (telefones mais estreitos). */
116
+ export const MOVIIE_SKIN_LAYOUT_SCALE_MIN = 0.72 as const
117
+
118
+ /**
119
+ * Limite superior do factor de escala: não aumenta além do design de referência em ecrãs largos.
120
+ */
121
+ export const MOVIIE_SKIN_LAYOUT_SCALE_MAX = 1 as const
122
+
123
+ /**
124
+ * Distância do HUD inferior (tempo, marca) às bordas laterais do vídeo e ao padding interno do relógio —
125
+ * valor na largura de referência ({@link MOVIIE_SKIN_LAYOUT_REFERENCE_WIDTH_PT}).
126
+ */
127
+ export const MOVIIE_SKIN_FLOATING_HUD_EDGE_INSET_PX = 6 as const
128
+
129
+ /**
130
+ * Espaço entre a linha do relógio/marca e o topo da área da timeline — referência.
131
+ * Separado de {@link MOVIIE_SKIN_FLOATING_HUD_EDGE_INSET_PX} para poder encostar ao footer sem afetar margens horizontais.
132
+ * Valor 0 = máximo colado (útil para experimentar layout).
133
+ */
134
+ export const MOVIIE_SKIN_FLOATING_HUD_ABOVE_TIMELINE_GAP_AT_REFERENCE_PT = 0 as const
135
+
136
+ /** Hit slop do wordmark e do fullscreen substituto no HUD inferior — px (valor físico independente da escala do layout). */
137
+ export const MOVIIE_SKIN_FLOATING_HUD_TRAILING_HIT_SLOP_PX = 8 as const
138
+
139
+ /**
140
+ * Diâmetro da película circular dos botões do HUD inferior (fullscreen, PiP, cast) — referência.
141
+ */
142
+ export const MOVIIE_SKIN_FLOATING_FULLSCREEN_SCRIM_DIAMETER_AT_REFERENCE_PT = 44 as const
143
+
144
+ /**
145
+ * Tamanho do ícone dentro dessa película — referência.
146
+ */
147
+ export const MOVIIE_SKIN_FLOATING_FULLSCREEN_ICON_AT_REFERENCE_PT = 26 as const
148
+
149
+ /**
150
+ * Ícones do canto superior (cast, PiP, fullscreen) sem película, com marca visível — referência.
151
+ * Cada ícone aplica seu próprio fator de compensação visual sobre este valor, pois os
152
+ * paths SVG têm densidades de preenchimento diferentes dentro do viewBox.
153
+ */
154
+ export const MOVIIE_SKIN_FLOATING_FULLSCREEN_TOP_ICON_AT_REFERENCE_PT = 36 as const
155
+
156
+ /**
157
+ * Fatores de escala visual por ícone do canto superior.
158
+ * Cast/PiP usam viewBox 0 0 24 24 (Material); Fullscreen usa viewBox 0 0 32 32 (media-icons 1.1.5).
159
+ * As densidades de preenchimento são semelhantes entre si, mas Cast (Material 24px) ocupa mais
160
+ * área visual que Fullscreen (32px com cantos L), daí o cast ter escala ligeiramente menor.
161
+ */
162
+ export const MOVIIE_SKIN_TOP_ICON_CAST_VISUAL_SCALE = 0.65 as const
163
+ export const MOVIIE_SKIN_TOP_ICON_PIP_VISUAL_SCALE = 0.8 as const
164
+ export const MOVIIE_SKIN_TOP_ICON_FULLSCREEN_VISUAL_SCALE = 0.8 as const
165
+
166
+ /**
167
+ * Espaço entre botões da linha direita do HUD (cast, PiP, fullscreen) — referência.
168
+ * Maior que {@link MOVIIE_SKIN_FLOATING_HUD_EDGE_INSET_PX} para evitar toques acidentais
169
+ * em botões adjacentes.
170
+ */
171
+ export const MOVIIE_SKIN_FLOATING_HUD_BUTTON_GAP_AT_REFERENCE_PT = 10 as const
172
+
173
+ /** Raio dos cantos do indicador de tempo — valor na largura de referência. */
174
+ export const MOVIIE_SKIN_FLOATING_HUD_CLOCK_CORNER_RADIUS_PX = 6 as const
175
+
176
+ /** Opacidade da película preta detrás dos botões de chrome (0–1). */
177
+ export const MOVIIE_SKIN_CHROME_SCRIM_BLACK_ALPHA = 0.5 as const
178
+
179
+ /** Película preta por detrás dos elementos de chrome (timing, ícones). */
180
+ export const MOVIIE_SKIN_CHROME_SCRIM_BLACK_RGBA =
181
+ `rgba(0,0,0,${MOVIIE_SKIN_CHROME_SCRIM_BLACK_ALPHA})` as const
182
+
183
+ /** Opacidade dos controlos do chrome no estado normal (repouso). */
184
+ export const MOVIIE_SKIN_CHROME_CONTROL_REST_OPACITY = 1 as const
185
+
186
+ /**
187
+ * Opacidade dos controlos do chrome ao premir — feedback visual (seek, play, fullscreen).
188
+ */
189
+ export const MOVIIE_SKIN_CHROME_CONTROL_PRESSED_OPACITY = 0.72 as const
190
+
191
+ /** Preto nos stops do degradê vertical da skin (opacidade via `stopOpacity`). */
192
+ export const MOVIIE_SKIN_CHROME_GRADIENT_BLACK_HEX = '#000000' as const
193
+
194
+ /** Opacidade na borda “cheia” do degradê (outro extremo é totalmente transparente). */
195
+ export const MOVIIE_SKIN_CHROME_GRADIENT_EDGE_ALPHA = 0.62 as const
196
+
197
+ /**
198
+ * Altura da faixa reservada ao degradê nas bordas — referência (maior = mais “respiro” transparente).
199
+ * O escuro concentra-se junto à borda via {@link MOVIIE_SKIN_CHROME_EDGE_GRADIENT_EDGE_SOLID_FADE_END_FRACTION}.
200
+ * O inferior acrescenta a altura da timeline quando a barra de progresso está visível.
201
+ */
202
+ export const MOVIIE_SKIN_CHROME_EDGE_GRADIENT_HEIGHT_AT_REFERENCE_PT = 104 as const
203
+
204
+ /**
205
+ * Ao longo da faixa, fração [0–1] medida a partir da borda escura até estar 100% transparente.
206
+ * Ex.: 0.48 → ~48% da altura da faixa com degradê; o resto é totalmente transparente (valor intermédio entre faixa longa e lábio rente à borda).
207
+ */
208
+ export const MOVIIE_SKIN_CHROME_EDGE_GRADIENT_EDGE_SOLID_FADE_END_FRACTION = 0.48 as const
209
+
210
+ /**
211
+ * Altura mínima transparente entre faixa superior e inferior após escalar somas —
212
+ * evita sobreposição das duas faixas (“película” em todo o vídeo).
213
+ */
214
+ export const MOVIIE_SKIN_CHROME_EDGE_GRADIENT_MIN_CLEAR_MIDDLE_PX = 56 as const
215
+
216
+ /** IDs estáveis para `LinearGradient` em SVG (skin chrome). */
217
+ export const MOVIIE_SKIN_CHROME_SVG_GRADIENT_BOTTOM_EDGE_ID =
218
+ 'moviie_skin_chrome_gradient_bottom_edge' as const
219
+
220
+ export const MOVIIE_SKIN_CHROME_SVG_GRADIENT_TOP_EDGE_ID =
221
+ 'moviie_skin_chrome_gradient_top_edge' as const
222
+
223
+ /** Volume normalizado (0–1) abaixo disto usa o ícone `volume-low` como no Vidstack. */
224
+ export const MOVIIE_SKIN_VOLUME_LOW_ICON_THRESHOLD = 0.45 as const
225
+
226
+ /** Foreground dos ícones e texto da skin (branco sólido). */
227
+ export const MOVIIE_SKIN_CHROME_FOREGROUND_HEX = '#ffffff' as const
228
+
229
+ /**
230
+ * Espelha valores numéricos de `OrientationLock` do pacote `expo-screen-orientation` (SDK 52).
231
+ * Mantidos aqui para funções puras e testes Vitest sem resolver o entry nativo do Expo.
232
+ */
233
+ export const MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE = {
234
+ LANDSCAPE: 5,
235
+ LANDSCAPE_LEFT: 6,
236
+ LANDSCAPE_RIGHT: 7,
237
+ } as const
238
+
239
+ export type MoviieExpoOrientationLockValue =
240
+ (typeof MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE)[keyof typeof MOVIIE_EXPO_SCREEN_ORIENTATION_LOCK_VALUE]
241
+
242
+ /** Duração da transição (rotação + escala) do fullscreen JS na skin Moviie. */
243
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_TRANSITION_MS = 420 as const
244
+
245
+ /**
246
+ * Rotação final em graus no eixo Z (sentido horário no React Native): vídeo + chrome giram juntos.
247
+ */
248
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_ROTATE_Z_DEGREES = 90 as const
249
+
250
+ /** Escala inicial ao entrar em fullscreen (anima até 1). Saída anima de 1 até este valor. */
251
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_ENTER_SCALE_START = 0.78 as const
252
+
253
+ /**
254
+ * `contentFit` no fullscreen rotacionado: preenche a altura útil do palco; barras laterais ficam a preto do palco.
255
+ */
256
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_VIDEO_CONTENT_FIT = 'contain' as const
257
+
258
+ /** Fundo do palco em fullscreen JS e placeholder inline durante o modo expandido. */
259
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_BACKGROUND_HEX = '#000000' as const
260
+
261
+ /**
262
+ * Margem extra (dp/pt) para dentro dos safe area insets no palco fullscreen rotacionado —
263
+ * evita cortar chrome por cantos redondos / overshoot da animação.
264
+ */
265
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_STAGE_OVERSCAN_PX = 12 as const
266
+
267
+ /**
268
+ * Reforço da margem do HUD/timeline em fullscreen rotacionado (escala com {@link MOVIIE_SKIN_LAYOUT_REFERENCE_WIDTH_PT}).
269
+ */
270
+ export const MOVIIE_SKIN_CUSTOM_FULLSCREEN_HUD_EDGE_BOOST_REFERENCE_PT = 14 as const
271
+
272
+ /** Margem de buffer (s) abaixo da cabeça de reprodução para detetar stalls em HLS. */
273
+ export const MOVIIE_SKIN_BUFFER_UNDERRUN_LEAD_SECONDS = 0.35 as const
274
+
275
+ export const MOVIIE_SKIN_SEEK_JUMP_SECONDS = 10 as const
276
+
277
+ export const MOVIIE_SKIN_CENTER_PLAY_ICON_PX = 40 as const
278
+
279
+ /**
280
+ * Diâmetro da película circular do play/pause central — referência.
281
+ * Entre o círculo compacto (~52) e o layout antigo (~108 na referência).
282
+ */
283
+ export const MOVIIE_SKIN_CENTER_PLAY_SCRIM_DIAMETER_AT_REFERENCE_PT = 80 as const
284
+
285
+ /**
286
+ * Diâmetro da película circular dos botões seek (±10 s) junto ao play — referência.
287
+ * Compacto em torno do ícone ({@link MOVIIE_SKIN_SEEK_ICON_PX}); inferior ao play.
288
+ */
289
+ export const MOVIIE_SKIN_CENTER_SEEK_SCRIM_DIAMETER_AT_REFERENCE_PT = 46 as const
290
+
291
+ /** Ícone seek ±10 s no cluster central — referência (escala com layout). */
292
+ export const MOVIIE_SKIN_SEEK_ICON_PX = 28 as const
293
+
294
+ /** Espaço horizontal entre botões do cluster central — valor na largura de referência. */
295
+ export const MOVIIE_SKIN_CENTER_CLUSTER_COLUMN_GAP_PX = 12 as const
296
+
297
+ /** Tamanho do texto do relógio — valor na largura de referência. */
298
+ export const MOVIIE_SKIN_CLOCK_TEXT_FONT_SIZE_AT_REFERENCE_PT = 12 as const
299
+
300
+ /** Altura da faixa única da timeline — valor na largura de referência. */
301
+ export const MOVIIE_SKIN_TIMELINE_BAR_HEIGHT_PX = 3 as const
302
+
303
+ /** Diâmetro do thumb de scrub — valor na largura de referência. */
304
+ export const MOVIIE_SKIN_TIMELINE_THUMB_DIAMETER_PX = 14 as const
305
+
306
+ /** Padding vertical acima da timeline — referência (0 = mínimo para testes de densidade). */
307
+ export const MOVIIE_SKIN_TIMELINE_TOUCH_PADDING_VERTICAL_PX = 0 as const
308
+
309
+ /**
310
+ * Padding vertical mínimo (dp lógico) por cima do rail dentro da área do vídeo — bem menor que antes:
311
+ * o alvo tátil prioritário é {@link MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_BOTTOM_DP} por baixo do frame.
312
+ */
313
+ export const MOVIIE_SKIN_TIMELINE_TOUCH_EXPANSION_VERTICAL_DP = 2 as const
314
+
315
+ /**
316
+ * Expansão lateral modesta (`View.hitSlop`) — evita faixa enorme dentro do vídeo.
317
+ */
318
+ export const MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_HORIZONTAL_DP = 3 as const
319
+
320
+ /**
321
+ * Pouca expansão para cima: apenas cobrir metade superior do thumb / gestos desde logo acima do trilho.
322
+ */
323
+ export const MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_TOP_DP = 2 as const
324
+
325
+ /**
326
+ * Expansão inferior dominante — cobre o thumb quando ultrapassa o limite inferior do vídeo
327
+ * (`hitSlop` não ocupa layout dentro do frame; toques contam por baixo da overlay).
328
+ */
329
+ export const MOVIIE_SKIN_TIMELINE_SCRUB_HIT_SLOP_BOTTOM_DP = 14 as const
330
+
331
+ /** Trilho base da timeline: branco ~30%. */
332
+ export const MOVIIE_SKIN_TIMELINE_RAIL_BACKGROUND_RGBA = 'rgba(255,255,255,0.3)' as const
333
+
334
+ /** Camada de buffer sobre o trilho (mesma cor/opacidade que o trilho). */
335
+ export const MOVIIE_SKIN_TIMELINE_BUFFER_LAYER_RGBA = MOVIIE_SKIN_TIMELINE_RAIL_BACKGROUND_RGBA
336
+
337
+ /** Contorno do thumb da timeline para contraste sobre vídeo claro. */
338
+ export const MOVIIE_SKIN_TIMELINE_THUMB_BORDER_RGBA = 'rgba(0,0,0,0.35)' as const
339
+
340
+ /** Diâmetro do thumb durante scrub (drag) relativamente ao thumb em repouso. */
341
+ export const MOVIIE_SKIN_TIMELINE_THUMB_DRAG_DIAMETER_MULTIPLIER = 1.65 as const
342
+
343
+ /**
344
+ * Segundos antes do fim onde o progresso mostrado fixa em 100% quando não está a reproduzir —
345
+ * o `currentTime` do motor costuma ficar ligeiramente abaixo de `duration` no fim.
346
+ */
347
+ export const MOVIIE_SKIN_TIMELINE_END_SNAP_EPSILON_SECONDS = 0.15 as const
348
+
349
+ /**
350
+ * Distância máxima (s) entre a posição de release do scrub e um boundary de capítulo
351
+ * para que o snap automático seja ativado.
352
+ */
353
+ export const MOVIIE_SKIN_TIMELINE_CHAPTER_SNAP_THRESHOLD_SECONDS = 2 as const
354
+
355
+ /** Área do `ActivityIndicator` small no overlay de buffering — valor na largura de referência. */
356
+ export const MOVIIE_SKIN_BUFFERING_INDICATOR_SLOT_PX = 28 as const
357
+
358
+ /** Largura intrínseca do SVG wordmark (viewBox). */
359
+ export const MOVIIE_SKIN_BRAND_MARK_ARTBOARD_WIDTH_PX = 173 as const
360
+
361
+ /** Altura intrínseca do SVG wordmark (viewBox). */
362
+ export const MOVIIE_SKIN_BRAND_MARK_ARTBOARD_HEIGHT_PX = 46 as const
363
+
364
+ /**
365
+ * Largura visual alvo do wordmark quando `windowWidth` = {@link MOVIIE_SKIN_LAYOUT_REFERENCE_WIDTH_PT}
366
+ * (entre o tamanho compacto antigo e o artboard completo).
367
+ */
368
+ export const MOVIIE_SKIN_BRAND_MARK_TARGET_WIDTH_AT_REFERENCE_PT = 68 as const
369
+
370
+ /** ViewBox do SVG wordmark Moviie (173×46). */
371
+ export const MOVIIE_EMBED_BRAND_VIEW_BOX = '0 0 173 46' as const
372
+
373
+ /** Espessura do traço das letras “ii” no wordmark (coordenadas do viewBox). */
374
+ export const MOVIIE_EMBED_BRAND_LETTERMARK_STROKE_WIDTH = 3 as const
@@ -0,0 +1,103 @@
1
+ import { useEffect } from "react"
2
+
3
+ import type { VideoPlayer } from "expo-video"
4
+ import { PLAYER_API_EVENTS } from "@moviie/player-types"
5
+
6
+ type PublicPlayerEvent =
7
+ (typeof PLAYER_API_EVENTS)[keyof typeof PLAYER_API_EVENTS]
8
+
9
+ export function useMoviieEvent(
10
+ player: VideoPlayer | null,
11
+ eventName: PublicPlayerEvent,
12
+ handler: () => void
13
+ ): void {
14
+ useEffect(() => {
15
+ if (!player) {
16
+ return
17
+ }
18
+
19
+ if (eventName === PLAYER_API_EVENTS.TIME_UPDATE) {
20
+ const previousInterval = player.timeUpdateEventInterval
21
+ if (player.timeUpdateEventInterval <= 0) {
22
+ player.timeUpdateEventInterval = 0.25
23
+ }
24
+ const sub = player.addListener("timeUpdate", () => {
25
+ handler()
26
+ })
27
+ return () => {
28
+ sub.remove()
29
+ player.timeUpdateEventInterval = previousInterval
30
+ }
31
+ }
32
+
33
+ if (eventName === PLAYER_API_EVENTS.PLAY) {
34
+ const sub = player.addListener("playingChange", ({ isPlaying }) => {
35
+ if (isPlaying) {
36
+ handler()
37
+ }
38
+ })
39
+ return () => {
40
+ sub.remove()
41
+ }
42
+ }
43
+
44
+ if (eventName === PLAYER_API_EVENTS.PAUSE) {
45
+ const sub = player.addListener("playingChange", ({ isPlaying }) => {
46
+ if (!isPlaying) {
47
+ handler()
48
+ }
49
+ })
50
+ return () => {
51
+ sub.remove()
52
+ }
53
+ }
54
+
55
+ if (eventName === PLAYER_API_EVENTS.ENDED) {
56
+ const sub = player.addListener("playToEnd", () => {
57
+ handler()
58
+ })
59
+ return () => {
60
+ sub.remove()
61
+ }
62
+ }
63
+
64
+ if (eventName === PLAYER_API_EVENTS.DURATION_CHANGE) {
65
+ const sub = player.addListener("sourceChange", () => {
66
+ handler()
67
+ })
68
+ return () => {
69
+ sub.remove()
70
+ }
71
+ }
72
+
73
+ if (eventName === PLAYER_API_EVENTS.VOLUME_CHANGE) {
74
+ const sub = player.addListener("volumeChange", () => {
75
+ handler()
76
+ })
77
+ return () => {
78
+ sub.remove()
79
+ }
80
+ }
81
+
82
+ if (eventName === PLAYER_API_EVENTS.READY) {
83
+ const sub = player.addListener("statusChange", ({ status }) => {
84
+ if (status === "readyToPlay") {
85
+ handler()
86
+ }
87
+ })
88
+ return () => {
89
+ sub.remove()
90
+ }
91
+ }
92
+
93
+ if (
94
+ typeof __DEV__ !== "undefined" &&
95
+ __DEV__
96
+ ) {
97
+ console.warn(
98
+ `[@moviie/player-expo] useMoviieEvent: "${eventName}" não tem mapeamento para expo-video nesta versão`
99
+ )
100
+ }
101
+ return undefined
102
+ }, [player, eventName, handler])
103
+ }