@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
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ import './apply-expo-moviie-endpoints'
2
+ export * from '@moviie/player-sdk'
3
+
4
+ export {
5
+ MOVIIE_PLAYER_EXPO_PKG_VERSION,
6
+ MOVIIE_PLAYBACK_MEMORY_CACHE_TTL_MS,
7
+ MOVIIE_PLAYBACK_PROFILE,
8
+ MOVIIE_VIDEO_DEFAULT_CONTENT_FIT,
9
+ MOVIIE_SKIN_CONTROLS_AUTO_HIDE_MS,
10
+ MOVIIE_SKIN_CONTROL_ICON_PX,
11
+ MOVIIE_SKIN_DEFAULT_ACCENT_HEX,
12
+ MOVIIE_SKIN_VOLUME_LOW_ICON_THRESHOLD,
13
+ MOVIIE_SKIN_TIMELINE_END_SNAP_EPSILON_SECONDS,
14
+ MOVIIE_REMEMBER_POSITION_DEBOUNCE_MS,
15
+ MOVIIE_REMEMBER_POSITION_STORAGE_PREFIX,
16
+ EXPO_PUBLIC_MOVIIE_API_BASE_URL_ENV,
17
+ EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL_ENV,
18
+ MOVIIE_WATCH_ORIGIN_DEFAULT,
19
+ MOVIIE_WATCH_EMBED_JS_PATH,
20
+ MOVIIE_PLAYER_EXPO_EMBED_JS_MARKER_ATTR,
21
+ EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN_ENV,
22
+ } from './constants'
23
+
24
+ export {
25
+ MOVIIE_VIEWER_TOKEN_SECURE_STORE_KEY,
26
+ SecureStoreViewerTokenStore,
27
+ } from './secure-store-viewer-token-store'
28
+
29
+ export { MoviieProvider, useMoviieContext } from './provider/moviie-provider'
30
+ export type { MoviieContextValue } from './provider/moviie-provider'
31
+
32
+ export { useMoviiePlayback } from './hooks/use-moviie-playback'
33
+ export type { UseMoviiePlaybackArgs, UseMoviiePlaybackResult } from './hooks/use-moviie-playback'
34
+
35
+ export { useMoviiePlayer } from './hooks/use-moviie-player'
36
+ export type { UseMoviiePlayerOptions, UseMoviiePlayerResult } from './hooks/use-moviie-player-types'
37
+
38
+ export {
39
+ buildMoviieVideoPresentationProps,
40
+ type MoviieVideoPresentationProps,
41
+ } from './lib/build-moviie-video-presentation'
42
+
43
+ export { useMoviieEvent } from './hooks/use-moviie-event'
44
+
45
+ export { useMoviiePlaybackEnded } from './hooks/use-moviie-playback-ended'
46
+
47
+ export {
48
+ computeMoviiePlaybackEnded,
49
+ type MoviiePlaybackEndedUiArgs,
50
+ } from './lib/compute-moviie-playback-ended'
51
+
52
+ export {
53
+ addMoviiePlaybackEndedListener,
54
+ type MoviiePlaybackEndedSubscription,
55
+ } from './lib/add-moviie-playback-ended-listener'
56
+
57
+ export {
58
+ buildMoviieWatchEmbedUrl,
59
+ normalizeWatchOrigin,
60
+ resolveMoviieWatchOrigin,
61
+ } from './lib/build-moviie-watch-embed-url'
62
+
63
+ export type {
64
+ MoviieCastAdapter,
65
+ MoviieCastMediaPayload,
66
+ MoviieCastState,
67
+ } from './lib/moviie-cast-adapter'
68
+
69
+ export { MoviieVideo } from './components/moviie-video'
70
+ export type {
71
+ MoviieVideoProps,
72
+ MoviieWatchEmbedIframeHandle,
73
+ MoviieWatchEmbedIframeRefCallback,
74
+ } from './components/moviie-video-props'
75
+
76
+ export { MoviieErrorBoundary } from './components/moviie-error-boundary'
77
+ export type { MoviieErrorBoundaryProps } from './components/moviie-error-boundary'
78
+
79
+ export type {
80
+ MoviieTelemetryErrorContext,
81
+ MoviieTelemetryErrorPhase,
82
+ MoviieTelemetryLifecycleEvent,
83
+ MoviieTelemetryLifecycleEventName,
84
+ MoviieTelemetryOnError,
85
+ MoviieTelemetryOnEvent,
86
+ } from './lib/moviie-telemetry-callbacks'
87
+
88
+ export type { MoviieVideoSurfaceRef } from './components/controls/moviie-controls'
89
+
90
+ export { PLAYER_API_EVENTS, PLAYER_API_METHODS, PLAYER_API_PROPERTIES } from '@moviie/player-types'
package/src/layout.ts ADDED
@@ -0,0 +1,4 @@
1
+ import "./apply-expo-moviie-endpoints"
2
+
3
+ export { MoviieProvider, useMoviieContext } from "./provider/moviie-provider"
4
+ export type { MoviieContextValue } from "./provider/moviie-provider"
@@ -0,0 +1,16 @@
1
+ import type { VideoPlayer } from "expo-video"
2
+
3
+ export type MoviiePlaybackEndedSubscription = {
4
+ remove: () => void
5
+ }
6
+
7
+ /**
8
+ * Imperative listener for expo-video `playToEnd`. Prefer {@link useMoviiePlaybackEnded} in React components.
9
+ */
10
+ export function addMoviiePlaybackEndedListener(
11
+ player: VideoPlayer,
12
+ onEnded: () => void
13
+ ): MoviiePlaybackEndedSubscription {
14
+ const sub = player.addListener("playToEnd", onEnded)
15
+ return { remove: () => sub.remove() }
16
+ }
@@ -0,0 +1,14 @@
1
+ import {
2
+ MOVIIE_BRANDING_MARKETING_ORIGIN,
3
+ MOVIIE_BRANDING_UTM_MEDIUM_VALUE,
4
+ MOVIIE_BRANDING_UTM_PARAM_MEDIUM,
5
+ MOVIIE_BRANDING_UTM_PARAM_SOURCE,
6
+ MOVIIE_BRANDING_UTM_SOURCE_VALUE,
7
+ } from "../constants"
8
+
9
+ export function buildMoviieBrandingMarketingUrl(): string {
10
+ const url = new URL(MOVIIE_BRANDING_MARKETING_ORIGIN)
11
+ url.searchParams.set(MOVIIE_BRANDING_UTM_PARAM_SOURCE, MOVIIE_BRANDING_UTM_SOURCE_VALUE)
12
+ url.searchParams.set(MOVIIE_BRANDING_UTM_PARAM_MEDIUM, MOVIIE_BRANDING_UTM_MEDIUM_VALUE)
13
+ return url.toString()
14
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ MOVIIE_BRANDING_MARKETING_ORIGIN,
3
+ MOVIIE_BRANDING_UTM_PARAM_CAMPAIGN,
4
+ MOVIIE_BRANDING_UTM_PARAM_MEDIUM,
5
+ MOVIIE_BRANDING_UTM_PARAM_SOURCE,
6
+ MOVIIE_EMBED_BRAND_UTM,
7
+ } from "../constants"
8
+
9
+ /** Espelha `buildPlayerBrandHomeHref` do embed web (`apps/web/src/app/embed/[id]/utils.ts`). */
10
+ export function buildMoviieEmbedBrandHomeUrl(): string {
11
+ const url = new URL(`${MOVIIE_BRANDING_MARKETING_ORIGIN}/`)
12
+ url.searchParams.set(MOVIIE_BRANDING_UTM_PARAM_SOURCE, MOVIIE_EMBED_BRAND_UTM.SOURCE)
13
+ url.searchParams.set(MOVIIE_BRANDING_UTM_PARAM_MEDIUM, MOVIIE_EMBED_BRAND_UTM.MEDIUM)
14
+ url.searchParams.set(MOVIIE_BRANDING_UTM_PARAM_CAMPAIGN, MOVIIE_EMBED_BRAND_UTM.CAMPAIGN)
15
+ return url.toString()
16
+ }
@@ -0,0 +1,166 @@
1
+ import {
2
+ MOVIIE_SKIN_BRAND_MARK_ARTBOARD_HEIGHT_PX,
3
+ MOVIIE_SKIN_BRAND_MARK_ARTBOARD_WIDTH_PX,
4
+ MOVIIE_SKIN_BRAND_MARK_TARGET_WIDTH_AT_REFERENCE_PT,
5
+ MOVIIE_SKIN_BUFFERING_INDICATOR_SLOT_PX,
6
+ MOVIIE_SKIN_CENTER_CLUSTER_COLUMN_GAP_PX,
7
+ MOVIIE_SKIN_CENTER_PLAY_ICON_PX,
8
+ MOVIIE_SKIN_CENTER_PLAY_SCRIM_DIAMETER_AT_REFERENCE_PT,
9
+ MOVIIE_SKIN_CUSTOM_FULLSCREEN_HUD_EDGE_BOOST_REFERENCE_PT,
10
+ MOVIIE_SKIN_CHROME_EDGE_GRADIENT_HEIGHT_AT_REFERENCE_PT,
11
+ MOVIIE_SKIN_CLOCK_TEXT_FONT_SIZE_AT_REFERENCE_PT,
12
+ MOVIIE_SKIN_CONTROL_ICON_PX,
13
+ MOVIIE_SKIN_FLOATING_FULLSCREEN_ICON_AT_REFERENCE_PT,
14
+ MOVIIE_SKIN_FLOATING_FULLSCREEN_SCRIM_DIAMETER_AT_REFERENCE_PT,
15
+ MOVIIE_SKIN_FLOATING_FULLSCREEN_TOP_ICON_AT_REFERENCE_PT,
16
+ MOVIIE_SKIN_FLOATING_HUD_BUTTON_GAP_AT_REFERENCE_PT,
17
+ MOVIIE_SKIN_FLOATING_HUD_CLOCK_CORNER_RADIUS_PX,
18
+ MOVIIE_SKIN_FLOATING_HUD_EDGE_INSET_PX,
19
+ MOVIIE_SKIN_FLOATING_HUD_ABOVE_TIMELINE_GAP_AT_REFERENCE_PT,
20
+ MOVIIE_SKIN_SEEK_ICON_PX,
21
+ MOVIIE_SKIN_CENTER_SEEK_SCRIM_DIAMETER_AT_REFERENCE_PT,
22
+ MOVIIE_SKIN_TIMELINE_BAR_HEIGHT_PX,
23
+ MOVIIE_SKIN_TIMELINE_THUMB_DIAMETER_PX,
24
+ MOVIIE_SKIN_TIMELINE_TOUCH_PADDING_VERTICAL_PX,
25
+ } from '../constants'
26
+
27
+ import { resolveMoviieSkinLayoutScale } from './resolve-moviie-skin-layout-scale'
28
+
29
+ /**
30
+ * Métricas em unidades lógicas (o mesmo tipo de número que `StyleSheet`: dp/pt por plataforma).
31
+ * O React Native já aplica a densidade do ecrã — evitar arredondamento manual com PixelRatio aqui.
32
+ * O único escalonamento extra é {@link resolveMoviieSkinLayoutScale} pela largura da janela (layout responsivo).
33
+ */
34
+ export type MoviieSkinLayoutMetrics = {
35
+ readonly layoutScale: number
36
+ readonly brandMarkWidthPx: number
37
+ readonly brandMarkHeightPx: number
38
+ readonly hudEdgeInsetPx: number
39
+ readonly floatingHudAboveTimelineGapPx: number
40
+ readonly hudClockCornerRadiusPx: number
41
+ readonly controlIconPx: number
42
+ readonly seekIconPx: number
43
+ readonly seekScrimOuterDiameterPx: number
44
+ readonly centerPlayIconPx: number
45
+ readonly centerClusterColumnGapPx: number
46
+ readonly playScrimOuterDiameterPx: number
47
+ readonly clockTextFontSizePx: number
48
+ readonly timelineThumbDiameterPx: number
49
+ readonly timelineTouchPaddingVerticalPx: number
50
+ readonly timelineBarHeightPx: number
51
+ readonly timelineThumbBottomInsetPx: number
52
+ readonly timelineStackHeightChromeVisiblePx: number
53
+ readonly bufferingIndicatorSlotPx: number
54
+ readonly chromeEdgeGradientHeightPx: number
55
+ readonly floatingHudFullscreenScrimDiameterPx: number
56
+ readonly floatingHudFullscreenIconPx: number
57
+ readonly floatingHudFullscreenTopIconPx: number
58
+ readonly floatingHudButtonGapPx: number
59
+ }
60
+
61
+ function scaleSkinLayoutDp(layoutScale: number, baseAtReference: number): number {
62
+ return baseAtReference * layoutScale
63
+ }
64
+
65
+ export type BuildMoviieSkinLayoutMetricsOptions = {
66
+ /** Fullscreen JS rotacionado: inset extra no HUD/timeline para não encostar à zona segura. */
67
+ fullscreenHudProtection?: boolean
68
+ }
69
+
70
+ export function buildMoviieSkinLayoutMetrics(
71
+ windowWidth: number,
72
+ options?: BuildMoviieSkinLayoutMetricsOptions
73
+ ): MoviieSkinLayoutMetrics {
74
+ const layoutScale = resolveMoviieSkinLayoutScale(windowWidth)
75
+
76
+ const brandMarkWidthPx = scaleSkinLayoutDp(
77
+ layoutScale,
78
+ MOVIIE_SKIN_BRAND_MARK_TARGET_WIDTH_AT_REFERENCE_PT
79
+ )
80
+ const brandMarkHeightPx =
81
+ brandMarkWidthPx *
82
+ (MOVIIE_SKIN_BRAND_MARK_ARTBOARD_HEIGHT_PX / MOVIIE_SKIN_BRAND_MARK_ARTBOARD_WIDTH_PX)
83
+
84
+ const timelineThumbDiameterPx = scaleSkinLayoutDp(
85
+ layoutScale,
86
+ MOVIIE_SKIN_TIMELINE_THUMB_DIAMETER_PX
87
+ )
88
+ const timelineBarHeightPx = scaleSkinLayoutDp(layoutScale, MOVIIE_SKIN_TIMELINE_BAR_HEIGHT_PX)
89
+ const timelineThumbBottomInsetPx = timelineBarHeightPx / 2 - timelineThumbDiameterPx / 2
90
+
91
+ const timelineTouchPaddingVerticalPx = scaleSkinLayoutDp(
92
+ layoutScale,
93
+ MOVIIE_SKIN_TIMELINE_TOUCH_PADDING_VERTICAL_PX
94
+ )
95
+
96
+ const fullscreenHudBoostPx =
97
+ options?.fullscreenHudProtection === true
98
+ ? scaleSkinLayoutDp(layoutScale, MOVIIE_SKIN_CUSTOM_FULLSCREEN_HUD_EDGE_BOOST_REFERENCE_PT)
99
+ : 0
100
+
101
+ const hudEdgeInsetPx =
102
+ scaleSkinLayoutDp(layoutScale, MOVIIE_SKIN_FLOATING_HUD_EDGE_INSET_PX) + fullscreenHudBoostPx
103
+
104
+ return {
105
+ layoutScale,
106
+ brandMarkWidthPx,
107
+ brandMarkHeightPx,
108
+ hudEdgeInsetPx,
109
+ floatingHudAboveTimelineGapPx: scaleSkinLayoutDp(
110
+ layoutScale,
111
+ MOVIIE_SKIN_FLOATING_HUD_ABOVE_TIMELINE_GAP_AT_REFERENCE_PT
112
+ ),
113
+ hudClockCornerRadiusPx: scaleSkinLayoutDp(
114
+ layoutScale,
115
+ MOVIIE_SKIN_FLOATING_HUD_CLOCK_CORNER_RADIUS_PX
116
+ ),
117
+ controlIconPx: scaleSkinLayoutDp(layoutScale, MOVIIE_SKIN_CONTROL_ICON_PX),
118
+ seekIconPx: scaleSkinLayoutDp(layoutScale, MOVIIE_SKIN_SEEK_ICON_PX),
119
+ seekScrimOuterDiameterPx: scaleSkinLayoutDp(
120
+ layoutScale,
121
+ MOVIIE_SKIN_CENTER_SEEK_SCRIM_DIAMETER_AT_REFERENCE_PT
122
+ ),
123
+ centerPlayIconPx: scaleSkinLayoutDp(layoutScale, MOVIIE_SKIN_CENTER_PLAY_ICON_PX),
124
+ centerClusterColumnGapPx: scaleSkinLayoutDp(
125
+ layoutScale,
126
+ MOVIIE_SKIN_CENTER_CLUSTER_COLUMN_GAP_PX
127
+ ),
128
+ playScrimOuterDiameterPx: scaleSkinLayoutDp(
129
+ layoutScale,
130
+ MOVIIE_SKIN_CENTER_PLAY_SCRIM_DIAMETER_AT_REFERENCE_PT
131
+ ),
132
+ clockTextFontSizePx: scaleSkinLayoutDp(
133
+ layoutScale,
134
+ MOVIIE_SKIN_CLOCK_TEXT_FONT_SIZE_AT_REFERENCE_PT
135
+ ),
136
+ timelineThumbDiameterPx,
137
+ timelineTouchPaddingVerticalPx,
138
+ timelineBarHeightPx,
139
+ timelineThumbBottomInsetPx,
140
+ timelineStackHeightChromeVisiblePx: timelineTouchPaddingVerticalPx + timelineThumbDiameterPx,
141
+ bufferingIndicatorSlotPx: scaleSkinLayoutDp(
142
+ layoutScale,
143
+ MOVIIE_SKIN_BUFFERING_INDICATOR_SLOT_PX
144
+ ),
145
+ chromeEdgeGradientHeightPx: scaleSkinLayoutDp(
146
+ layoutScale,
147
+ MOVIIE_SKIN_CHROME_EDGE_GRADIENT_HEIGHT_AT_REFERENCE_PT
148
+ ),
149
+ floatingHudFullscreenScrimDiameterPx: scaleSkinLayoutDp(
150
+ layoutScale,
151
+ MOVIIE_SKIN_FLOATING_FULLSCREEN_SCRIM_DIAMETER_AT_REFERENCE_PT
152
+ ),
153
+ floatingHudFullscreenIconPx: scaleSkinLayoutDp(
154
+ layoutScale,
155
+ MOVIIE_SKIN_FLOATING_FULLSCREEN_ICON_AT_REFERENCE_PT
156
+ ),
157
+ floatingHudFullscreenTopIconPx: scaleSkinLayoutDp(
158
+ layoutScale,
159
+ MOVIIE_SKIN_FLOATING_FULLSCREEN_TOP_ICON_AT_REFERENCE_PT
160
+ ),
161
+ floatingHudButtonGapPx: scaleSkinLayoutDp(
162
+ layoutScale,
163
+ MOVIIE_SKIN_FLOATING_HUD_BUTTON_GAP_AT_REFERENCE_PT
164
+ ),
165
+ }
166
+ }
@@ -0,0 +1,42 @@
1
+ import { MOVIIE_PLAYBACK_PROFILE } from "../constants"
2
+
3
+ export type MoviieVideoPresentationProps = {
4
+ allowsPictureInPicture: boolean
5
+ startsPictureInPictureAutomatically: boolean
6
+ requiresLinearPlayback: boolean
7
+ }
8
+
9
+ export interface BuildMoviieVideoPresentationArgs {
10
+ profile: string
11
+ showPip: boolean
12
+ /**
13
+ * Integrator override: when set, replaces embed `showPip` for `allowsPictureInPicture`.
14
+ * When omitted, `showPip` is used.
15
+ */
16
+ pictureInPicture?: boolean
17
+ /**
18
+ * Integrator override for `startsPictureInPictureAutomatically`.
19
+ * When omitted, defaults to `false` (no embed field today).
20
+ */
21
+ pictureInPictureAutoStart?: boolean
22
+ }
23
+
24
+ /**
25
+ * Maps embed playback flags onto `VideoView` presentation props.
26
+ * Optional fields let integrators override embed defaults explicitly; omit them to follow the API.
27
+ */
28
+ export function buildMoviieVideoPresentationProps(
29
+ args: BuildMoviieVideoPresentationArgs
30
+ ): MoviieVideoPresentationProps {
31
+ const allowsPictureInPicture =
32
+ args.pictureInPicture !== undefined
33
+ ? args.pictureInPicture
34
+ : args.showPip
35
+
36
+ return {
37
+ allowsPictureInPicture,
38
+ startsPictureInPictureAutomatically:
39
+ args.pictureInPictureAutoStart ?? false,
40
+ requiresLinearPlayback: args.profile === MOVIIE_PLAYBACK_PROFILE.VSL,
41
+ }
42
+ }
@@ -0,0 +1,30 @@
1
+ import { MOVIIE_WATCH_ORIGIN_DEFAULT } from "../constants"
2
+
3
+ export function normalizeWatchOrigin(origin: string): string {
4
+ return origin.trim().replace(/\/+$/, "")
5
+ }
6
+
7
+ export function buildMoviieWatchEmbedUrl(embedId: string, watchOrigin: string): string {
8
+ const base = normalizeWatchOrigin(watchOrigin)
9
+ const id = embedId.trim()
10
+ return `${base}/embed/${encodeURIComponent(id)}`
11
+ }
12
+
13
+ /**
14
+ * Resolve o origin público da página watch/embed.
15
+ * Usa `process.env.EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN` quando definido (nome documentado como `EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN_ENV` em `@moviie/player-expo`).
16
+ */
17
+ export function resolveMoviieWatchOrigin(): string {
18
+ try {
19
+ const raw =
20
+ typeof process !== "undefined"
21
+ ? process.env.EXPO_PUBLIC_MOVIIE_WATCH_ORIGIN?.trim()
22
+ : undefined
23
+ if (raw) {
24
+ return normalizeWatchOrigin(raw)
25
+ }
26
+ } catch {
27
+ /* ignore */
28
+ }
29
+ return MOVIIE_WATCH_ORIGIN_DEFAULT
30
+ }
@@ -0,0 +1,13 @@
1
+ import type { MoviieCastAdapter } from './moviie-cast-adapter'
2
+
3
+ let _registered: MoviieCastAdapter | null = null
4
+
5
+ /** Called automatically when `@moviie/player-expo/cast` is imported. */
6
+ export function _registerCastAdapter(adapter: MoviieCastAdapter): void {
7
+ _registered = adapter
8
+ }
9
+
10
+ /** Returns the auto-registered adapter, or null if cast subpath was never imported. */
11
+ export function getRegisteredCastAdapter(): MoviieCastAdapter | null {
12
+ return _registered
13
+ }
@@ -0,0 +1,3 @@
1
+ export function clampUnitInterval(value: number): number {
2
+ return Math.min(1, Math.max(0, value))
3
+ }
@@ -0,0 +1,39 @@
1
+ import type { EdgeInsets } from "react-native-safe-area-context"
2
+
3
+ export type CustomFullscreenStageDimensions = {
4
+ longSide: number
5
+ shortSide: number
6
+ layoutMetricsBasisWidth: number
7
+ }
8
+
9
+ export function computeCustomFullscreenStageDimensions(args: {
10
+ windowWidth: number
11
+ windowHeight: number
12
+ safeInsets: EdgeInsets
13
+ stageOverscanPx: number
14
+ }): CustomFullscreenStageDimensions {
15
+ const pad = args.stageOverscanPx
16
+ const usableW = Math.max(
17
+ 0,
18
+ args.windowWidth -
19
+ args.safeInsets.left -
20
+ args.safeInsets.right -
21
+ 2 * pad
22
+ )
23
+ const usableH = Math.max(
24
+ 0,
25
+ args.windowHeight -
26
+ args.safeInsets.top -
27
+ args.safeInsets.bottom -
28
+ 2 * pad
29
+ )
30
+
31
+ const longSide = Math.max(usableW, usableH)
32
+ const shortSide = Math.min(usableW, usableH)
33
+
34
+ return {
35
+ longSide,
36
+ shortSide,
37
+ layoutMetricsBasisWidth: longSide,
38
+ }
39
+ }
@@ -0,0 +1,31 @@
1
+ import { MOVIIE_SKIN_TIMELINE_END_SNAP_EPSILON_SECONDS } from "../constants"
2
+
3
+ export type MoviiePlaybackEndedUiArgs = {
4
+ playing: boolean
5
+ currentTime: number
6
+ duration: number
7
+ isLive: boolean
8
+ loop: boolean
9
+ }
10
+
11
+ export function computeMoviiePlaybackEnded(args: MoviiePlaybackEndedUiArgs): boolean {
12
+ if (args.loop) {
13
+ return false
14
+ }
15
+ if (args.isLive) {
16
+ return false
17
+ }
18
+ if (args.playing) {
19
+ return false
20
+ }
21
+ if (!Number.isFinite(args.duration) || args.duration <= 0) {
22
+ return false
23
+ }
24
+ if (!Number.isFinite(args.currentTime)) {
25
+ return false
26
+ }
27
+ return (
28
+ args.currentTime >=
29
+ args.duration - MOVIIE_SKIN_TIMELINE_END_SNAP_EPSILON_SECONDS
30
+ )
31
+ }
@@ -0,0 +1,41 @@
1
+ import { MOVIIE_SKIN_BUFFER_UNDERRUN_LEAD_SECONDS } from "../constants"
2
+ import { computeMoviiePlaybackEnded } from "./compute-moviie-playback-ended"
3
+
4
+ const PLAYER_STATUS_LOADING = "loading" as const
5
+
6
+ export function computePlaybackBuffering(state: {
7
+ bufferedPosition: number
8
+ currentTime: number
9
+ duration: number
10
+ playing: boolean
11
+ status: string
12
+ isLive: boolean
13
+ loop: boolean
14
+ }): boolean {
15
+ if (
16
+ computeMoviiePlaybackEnded({
17
+ playing: state.playing,
18
+ currentTime: state.currentTime,
19
+ duration: state.duration,
20
+ isLive: state.isLive,
21
+ loop: state.loop,
22
+ })
23
+ ) {
24
+ return false
25
+ }
26
+ if (state.status === PLAYER_STATUS_LOADING) {
27
+ return true
28
+ }
29
+ if (
30
+ state.playing &&
31
+ Number.isFinite(state.duration) &&
32
+ state.duration > 0 &&
33
+ state.bufferedPosition >= 0 &&
34
+ state.currentTime > 0.08 &&
35
+ state.bufferedPosition <
36
+ state.currentTime + MOVIIE_SKIN_BUFFER_UNDERRUN_LEAD_SECONDS
37
+ ) {
38
+ return true
39
+ }
40
+ return false
41
+ }
@@ -0,0 +1,33 @@
1
+ import { MOVIIE_SKIN_TIMELINE_END_SNAP_EPSILON_SECONDS } from "../constants"
2
+
3
+ import { clampUnitInterval } from "./clamp-unit-interval"
4
+
5
+ export function computePlaybackProgressRatio(state: {
6
+ durationOk: boolean
7
+ currentTime: number
8
+ duration: number
9
+ playing: boolean
10
+ }): number {
11
+ if (
12
+ !state.durationOk ||
13
+ !Number.isFinite(state.currentTime) ||
14
+ !Number.isFinite(state.duration) ||
15
+ state.duration <= 0
16
+ ) {
17
+ return 0
18
+ }
19
+
20
+ const raw = state.currentTime / state.duration
21
+ if (raw >= 1) {
22
+ return 1
23
+ }
24
+
25
+ const snapToEndWhilePaused =
26
+ !state.playing &&
27
+ state.currentTime >=
28
+ state.duration - MOVIIE_SKIN_TIMELINE_END_SNAP_EPSILON_SECONDS
29
+
30
+ const ratio = snapToEndWhilePaused ? 1 : raw
31
+
32
+ return clampUnitInterval(ratio)
33
+ }
@@ -0,0 +1,40 @@
1
+ import { MOVIIE_SKIN_TIMELINE_CHAPTER_SNAP_THRESHOLD_SECONDS } from "../constants"
2
+
3
+ export type TimelineChapterBoundary = {
4
+ readonly startTimeSeconds: number
5
+ }
6
+
7
+ /**
8
+ * Converte um `ratio` [0–1] de scrub em segundos absolutos, com snap automático
9
+ * ao boundary de capítulo mais próximo dentro do threshold configurado.
10
+ */
11
+ export function computeTimelineScrubCommitTime(params: {
12
+ ratio: number
13
+ durationSeconds: number
14
+ chapters: ReadonlyArray<TimelineChapterBoundary>
15
+ snapThresholdSeconds?: number
16
+ }): number {
17
+ const {
18
+ ratio,
19
+ durationSeconds,
20
+ chapters,
21
+ snapThresholdSeconds = MOVIIE_SKIN_TIMELINE_CHAPTER_SNAP_THRESHOLD_SECONDS,
22
+ } = params
23
+
24
+ const rawTime = ratio * durationSeconds
25
+
26
+ let nearest: number | null = null
27
+ let nearestDist = Infinity
28
+
29
+ for (const chapter of chapters) {
30
+ const dist = Math.abs(chapter.startTimeSeconds - rawTime)
31
+ if (dist < nearestDist) {
32
+ nearestDist = dist
33
+ nearest = chapter.startTimeSeconds
34
+ }
35
+ }
36
+
37
+ return nearest !== null && nearestDist <= snapThresholdSeconds
38
+ ? nearest
39
+ : rawTime
40
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ OrientationLock,
3
+ getOrientationLockAsync,
4
+ lockAsync,
5
+ unlockAsync,
6
+ } from "expo-screen-orientation"
7
+ import { Platform } from "react-native"
8
+
9
+ import type { MoviieExpoOrientationLockValue } from "../constants"
10
+
11
+ async function restoreOrientationLockPolicy(
12
+ previousLock: OrientationLock | undefined
13
+ ): Promise<void> {
14
+ try {
15
+ if (
16
+ previousLock === undefined ||
17
+ previousLock === OrientationLock.DEFAULT ||
18
+ previousLock === OrientationLock.UNKNOWN
19
+ ) {
20
+ await unlockAsync()
21
+ return
22
+ }
23
+ await lockAsync(previousLock)
24
+ } catch {
25
+ try {
26
+ await unlockAsync()
27
+ } catch {
28
+ // noop
29
+ }
30
+ }
31
+ }
32
+
33
+ export type CustomFullscreenNativeOrientationSubscription = {
34
+ finish: () => Promise<void>
35
+ }
36
+
37
+ export function subscribeCustomFullscreenNativeOrientation(
38
+ targetLock: MoviieExpoOrientationLockValue
39
+ ): CustomFullscreenNativeOrientationSubscription {
40
+ if (Platform.OS === "web") {
41
+ return { finish: async () => {} }
42
+ }
43
+
44
+ let cancelled = false
45
+ let locked = false
46
+ let previousLock: OrientationLock | undefined
47
+
48
+ const setup = (async () => {
49
+ try {
50
+ previousLock = await getOrientationLockAsync()
51
+ if (cancelled) {
52
+ return
53
+ }
54
+
55
+ await lockAsync(targetLock as OrientationLock)
56
+ if (cancelled) {
57
+ await restoreOrientationLockPolicy(previousLock)
58
+ return
59
+ }
60
+
61
+ locked = true
62
+ } catch {
63
+ // noop
64
+ }
65
+ })()
66
+
67
+ let finishPromise: Promise<void> | null = null
68
+
69
+ return {
70
+ finish: () => {
71
+ if (finishPromise != null) {
72
+ return finishPromise
73
+ }
74
+
75
+ cancelled = true
76
+
77
+ finishPromise = (async () => {
78
+ await setup
79
+ if (locked) {
80
+ locked = false
81
+ await restoreOrientationLockPolicy(previousLock)
82
+ }
83
+ })()
84
+
85
+ return finishPromise
86
+ },
87
+ }
88
+ }