@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,76 @@
1
+ import '../chunk-BJTO5JO5.mjs';
2
+ import { withInfoPlist, withAndroidManifest, AndroidConfig } from 'expo/config-plugins';
3
+
4
+ // plugin/validate-options.ts
5
+ var MoviiePluginConfigError = class extends Error {
6
+ constructor(field, reason) {
7
+ super(`[@moviie/player-expo] config plugin: invalid \`${field}\` \u2014 ${reason}`);
8
+ this.name = "MoviiePluginConfigError";
9
+ }
10
+ };
11
+ function validateBoolean(field, raw) {
12
+ if (raw == null) return;
13
+ if (typeof raw !== "boolean") {
14
+ throw new MoviiePluginConfigError(
15
+ field,
16
+ `esperava boolean, recebeu ${typeof raw} (\`${String(raw)}\`).`
17
+ );
18
+ }
19
+ }
20
+ function validateWithMoviieOptions(props) {
21
+ validateBoolean("backgroundPlayback", props.backgroundPlayback);
22
+ validateBoolean("pictureInPicture", props.pictureInPicture);
23
+ }
24
+
25
+ // plugin/with-moviie.ts
26
+ var IOS_BACKGROUND_MODE_AUDIO = "audio";
27
+ var IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE = "picture-in-picture";
28
+ function mergeMoviieUIBackgroundModes(existingModes, props) {
29
+ const backgroundPlayback = props.backgroundPlayback ?? false;
30
+ const pictureInPicture = props.pictureInPicture ?? false;
31
+ const modes = new Set(existingModes ?? []);
32
+ if (pictureInPicture) {
33
+ modes.add(IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE);
34
+ }
35
+ const shouldEnableAudio = backgroundPlayback || pictureInPicture;
36
+ if (shouldEnableAudio) {
37
+ modes.add(IOS_BACKGROUND_MODE_AUDIO);
38
+ }
39
+ if (modes.size === 0) {
40
+ return void 0;
41
+ }
42
+ return [...modes];
43
+ }
44
+ function applyPictureInPictureMainActivityAttributes(activity) {
45
+ activity.$["android:supportsPictureInPicture"] = "true";
46
+ activity.$["android:resizeableActivity"] = "true";
47
+ }
48
+ var withMoviie = (config, props = {}) => {
49
+ validateWithMoviieOptions(props);
50
+ const backgroundPlayback = props.backgroundPlayback ?? false;
51
+ const pictureInPicture = props.pictureInPicture ?? false;
52
+ config = withInfoPlist(config, (mod) => {
53
+ const merged = mergeMoviieUIBackgroundModes(mod.modResults.UIBackgroundModes, {
54
+ backgroundPlayback,
55
+ pictureInPicture
56
+ });
57
+ if (merged) {
58
+ mod.modResults.UIBackgroundModes = merged;
59
+ }
60
+ return mod;
61
+ });
62
+ config = withAndroidManifest(config, (mod) => {
63
+ if (!pictureInPicture) {
64
+ return mod;
65
+ }
66
+ const activity = AndroidConfig.Manifest.getMainActivityOrThrow(mod.modResults);
67
+ applyPictureInPictureMainActivityAttributes(activity);
68
+ return mod;
69
+ });
70
+ return config;
71
+ };
72
+ var with_moviie_default = withMoviie;
73
+
74
+ export { IOS_BACKGROUND_MODE_AUDIO, IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE, MoviiePluginConfigError, applyPictureInPictureMainActivityAttributes, with_moviie_default as default, mergeMoviieUIBackgroundModes, validateBoolean, validateWithMoviieOptions };
75
+ //# sourceMappingURL=with-moviie.mjs.map
76
+ //# sourceMappingURL=with-moviie.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../plugin/validate-options.ts","../../plugin/with-moviie.ts"],"names":[],"mappings":"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","file":"with-moviie.mjs","sourcesContent":["/**\n * Validação dos args do `withMoviie` config plugin. Rejeita valores inválidos\n * cedo (na fase de prebuild/EAS build) com mensagens accionáveis — em vez de\n * arrastar bugs até runtime onde o erro só aparece como 404/timeout opaco.\n *\n * **Sem dependências de runtime.**\n */\n\nimport type { WithMoviieOptions } from './with-moviie-types'\n\nexport class MoviiePluginConfigError extends Error {\n constructor(field: string, reason: string) {\n super(`[@moviie/player-expo] config plugin: invalid \\`${field}\\` — ${reason}`)\n this.name = 'MoviiePluginConfigError'\n }\n}\n\nexport function validateBoolean(field: string, raw: unknown): void {\n if (raw == null) return\n if (typeof raw !== 'boolean') {\n throw new MoviiePluginConfigError(\n field,\n `esperava boolean, recebeu ${typeof raw} (\\`${String(raw)}\\`).`\n )\n }\n}\n\nexport function validateWithMoviieOptions(props: WithMoviieOptions): void {\n validateBoolean('backgroundPlayback', props.backgroundPlayback)\n validateBoolean('pictureInPicture', props.pictureInPicture)\n}\n","import {\n AndroidConfig,\n type ConfigPlugin,\n withAndroidManifest,\n withInfoPlist,\n} from 'expo/config-plugins'\n\nimport { validateWithMoviieOptions } from './validate-options'\nimport type { WithMoviieOptions } from './with-moviie-types'\n\nexport type { WithMoviieOptions } from './with-moviie-types'\nexport {\n MoviiePluginConfigError,\n validateBoolean,\n validateWithMoviieOptions,\n} from './validate-options'\n\n/** Info.plist `UIBackgroundModes` values used by the plugin */\nexport const IOS_BACKGROUND_MODE_AUDIO = 'audio' as const\nexport const IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE = 'picture-in-picture' as const\n\nexport function mergeMoviieUIBackgroundModes(\n existingModes: string[] | undefined,\n props: Pick<WithMoviieOptions, 'backgroundPlayback' | 'pictureInPicture'>\n): string[] | undefined {\n const backgroundPlayback = props.backgroundPlayback ?? false\n const pictureInPicture = props.pictureInPicture ?? false\n\n const modes = new Set<string>(existingModes ?? [])\n if (pictureInPicture) {\n modes.add(IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE)\n }\n const shouldEnableAudio = backgroundPlayback || pictureInPicture\n if (shouldEnableAudio) {\n modes.add(IOS_BACKGROUND_MODE_AUDIO)\n }\n if (modes.size === 0) {\n return undefined\n }\n return [...modes]\n}\n\nexport function applyPictureInPictureMainActivityAttributes(activity: {\n $: Record<string, string | undefined>\n}): void {\n activity.$['android:supportsPictureInPicture'] = 'true'\n activity.$['android:resizeableActivity'] = 'true'\n}\n\n/**\n * Expo config plugin for `@moviie/player-expo`.\n * Applies iOS `UIBackgroundModes` and Android PiP manifest flags. Endpoint\n * overrides são via env var `EXPO_PUBLIC_MOVIIE_API_BASE_URL` (dev only).\n */\nconst withMoviie: ConfigPlugin<WithMoviieOptions> = (config, props = {}) => {\n validateWithMoviieOptions(props)\n\n const backgroundPlayback = props.backgroundPlayback ?? false\n const pictureInPicture = props.pictureInPicture ?? false\n\n config = withInfoPlist(config, (mod) => {\n const merged = mergeMoviieUIBackgroundModes(mod.modResults.UIBackgroundModes, {\n backgroundPlayback,\n pictureInPicture,\n })\n if (merged) {\n mod.modResults.UIBackgroundModes = merged\n }\n return mod\n })\n\n config = withAndroidManifest(config, (mod) => {\n if (!pictureInPicture) {\n return mod\n }\n const activity = AndroidConfig.Manifest.getMainActivityOrThrow(mod.modResults)\n applyPictureInPictureMainActivityAttributes(activity)\n return mod\n })\n\n return config\n}\n\nexport default withMoviie\n"]}
package/package.json ADDED
@@ -0,0 +1,134 @@
1
+ {
2
+ "name": "@moviie/player-expo",
3
+ "version": "0.4.0",
4
+ "private": false,
5
+ "description": "Moviie video player for Expo (expo-video, telemetry, native features).",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.mjs",
10
+ "react-native": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.cjs"
17
+ },
18
+ "./layout": {
19
+ "react-native": "./src/layout.ts",
20
+ "types": "./dist/layout.d.ts",
21
+ "import": "./dist/layout.mjs",
22
+ "require": "./dist/layout.cjs",
23
+ "default": "./dist/layout.mjs"
24
+ },
25
+ "./cast": {
26
+ "react-native": "./src/cast/index.ts",
27
+ "types": "./dist/cast.d.ts",
28
+ "import": "./dist/cast.mjs",
29
+ "require": "./dist/cast.cjs",
30
+ "default": "./dist/cast.mjs"
31
+ },
32
+ "./dist/plugin/with-moviie.js": "./dist/plugin/with-moviie.cjs",
33
+ "./dist/plugin/with-moviie.mjs": "./dist/plugin/with-moviie.mjs",
34
+ "./app.plugin.js": "./app.plugin.cjs"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "plugin",
40
+ "app.plugin.cjs",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "sideEffects": [
45
+ "./dist/index.mjs",
46
+ "./dist/index.cjs",
47
+ "./dist/layout.mjs",
48
+ "./dist/layout.cjs",
49
+ "./src/index.ts",
50
+ "./src/layout.ts",
51
+ "./src/apply-expo-moviie-endpoints.ts"
52
+ ],
53
+ "engines": {
54
+ "node": ">=18.18.0"
55
+ },
56
+ "dependencies": {
57
+ "@babel/runtime": "^7.26.0",
58
+ "@moviie/player-sdk": "0.2.0",
59
+ "@moviie/player-types": "1.1.0"
60
+ },
61
+ "peerDependencies": {
62
+ "expo": ">=53.0.0",
63
+ "expo-application": ">=6.1.0",
64
+ "expo-screen-orientation": ">=8.1.0",
65
+ "expo-secure-store": ">=14.2.0",
66
+ "expo-status-bar": ">=2.2.0",
67
+ "expo-video": ">=2.0.0",
68
+ "react": ">=19.0.0",
69
+ "react-native": ">=0.79.0",
70
+ "react-native-gesture-handler": ">=2.24.0",
71
+ "react-native-google-cast": ">=4.8.0",
72
+ "react-native-reanimated": "^3.17.0",
73
+ "react-native-safe-area-context": ">=5.4.0",
74
+ "react-native-svg": ">=15.11.0"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "expo-secure-store": {
78
+ "optional": true
79
+ },
80
+ "expo-status-bar": {
81
+ "optional": true
82
+ },
83
+ "react-native-google-cast": {
84
+ "optional": true
85
+ }
86
+ },
87
+ "devDependencies": {
88
+ "@testing-library/react-native": "^13.3.3",
89
+ "@types/jest": "^29.5.14",
90
+ "@types/node": "^22.10.2",
91
+ "@types/react": "~19.2.7",
92
+ "@vitest/coverage-v8": "^3.2.4",
93
+ "babel-preset-expo": "~13.0.0",
94
+ "eslint": "^9",
95
+ "expo": "~53.0.27",
96
+ "expo-screen-orientation": "~8.1.7",
97
+ "expo-secure-store": "~14.2.4",
98
+ "expo-status-bar": "~2.2.3",
99
+ "expo-video": "~2.2.3",
100
+ "jest": "^29.7.0",
101
+ "jest-expo": "^53.0.14",
102
+ "react": "19.0.0",
103
+ "react-native": "0.79.6",
104
+ "react-native-google-cast": "^4.9.1",
105
+ "react-native-safe-area-context": "5.4.0",
106
+ "react-native-svg": "15.11.2",
107
+ "react-test-renderer": "19.0.0",
108
+ "tsup": "^8.5.0",
109
+ "typescript": "^5.9.3",
110
+ "vitest": "^3.2.4",
111
+ "@moviie/eslint-config": "0.1.0",
112
+ "@moviie/prettier-config": "0.1.0",
113
+ "@moviie/tsconfig": "0.1.0"
114
+ },
115
+ "prettier": "@moviie/prettier-config",
116
+ "publishConfig": {
117
+ "access": "public"
118
+ },
119
+ "scripts": {
120
+ "build": "tsup",
121
+ "clean": "rm -rf dist",
122
+ "lint": "eslint",
123
+ "prebuild": "node scripts/sync-version.mjs",
124
+ "sync-version": "node scripts/sync-version.mjs",
125
+ "sync-version:check": "node scripts/sync-version.mjs --check",
126
+ "typecheck": "tsc --noEmit",
127
+ "test": "vitest",
128
+ "test:run": "vitest run && jest --runInBand",
129
+ "test:run:vitest": "vitest run",
130
+ "test:run:jest": "jest --runInBand",
131
+ "test:coverage:controls": "jest --runInBand --coverage",
132
+ "test:watch": "vitest"
133
+ }
134
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Validação dos args do `withMoviie` config plugin. Rejeita valores inválidos
3
+ * cedo (na fase de prebuild/EAS build) com mensagens accionáveis — em vez de
4
+ * arrastar bugs até runtime onde o erro só aparece como 404/timeout opaco.
5
+ *
6
+ * **Sem dependências de runtime.**
7
+ */
8
+
9
+ import type { WithMoviieOptions } from './with-moviie-types'
10
+
11
+ export class MoviiePluginConfigError extends Error {
12
+ constructor(field: string, reason: string) {
13
+ super(`[@moviie/player-expo] config plugin: invalid \`${field}\` — ${reason}`)
14
+ this.name = 'MoviiePluginConfigError'
15
+ }
16
+ }
17
+
18
+ export function validateBoolean(field: string, raw: unknown): void {
19
+ if (raw == null) return
20
+ if (typeof raw !== 'boolean') {
21
+ throw new MoviiePluginConfigError(
22
+ field,
23
+ `esperava boolean, recebeu ${typeof raw} (\`${String(raw)}\`).`
24
+ )
25
+ }
26
+ }
27
+
28
+ export function validateWithMoviieOptions(props: WithMoviieOptions): void {
29
+ validateBoolean('backgroundPlayback', props.backgroundPlayback)
30
+ validateBoolean('pictureInPicture', props.pictureInPicture)
31
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Tipos do config plugin separados do entry para evitar ciclos de import quando
3
+ * a validação é referenciada de testes ou outros módulos do plugin.
4
+ *
5
+ * Override de endpoints (apiBaseUrl/eventsBaseUrl) **NÃO** é exposto aqui — é
6
+ * dev-only via `EXPO_PUBLIC_MOVIIE_API_BASE_URL`/`EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL`.
7
+ * Ver `src/apply-expo-moviie-endpoints.ts`.
8
+ */
9
+
10
+ export type WithMoviieOptions = {
11
+ /**
12
+ * Adds iOS `UIBackgroundModes` audio when true (needed for background audio and PiP audio).
13
+ */
14
+ backgroundPlayback?: boolean
15
+ /**
16
+ * Adds iOS `UIBackgroundModes` values `picture-in-picture` and `audio` (PiP with audio needs both).
17
+ * Sets Android main activity `supportsPictureInPicture` and `resizeableActivity` for PiP-friendly layouts.
18
+ * Still configure `expo-video` with `supportsPictureInPicture` per Expo Video docs.
19
+ */
20
+ pictureInPicture?: boolean
21
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ AndroidConfig,
3
+ type ConfigPlugin,
4
+ withAndroidManifest,
5
+ withInfoPlist,
6
+ } from 'expo/config-plugins'
7
+
8
+ import { validateWithMoviieOptions } from './validate-options'
9
+ import type { WithMoviieOptions } from './with-moviie-types'
10
+
11
+ export type { WithMoviieOptions } from './with-moviie-types'
12
+ export {
13
+ MoviiePluginConfigError,
14
+ validateBoolean,
15
+ validateWithMoviieOptions,
16
+ } from './validate-options'
17
+
18
+ /** Info.plist `UIBackgroundModes` values used by the plugin */
19
+ export const IOS_BACKGROUND_MODE_AUDIO = 'audio' as const
20
+ export const IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE = 'picture-in-picture' as const
21
+
22
+ export function mergeMoviieUIBackgroundModes(
23
+ existingModes: string[] | undefined,
24
+ props: Pick<WithMoviieOptions, 'backgroundPlayback' | 'pictureInPicture'>
25
+ ): string[] | undefined {
26
+ const backgroundPlayback = props.backgroundPlayback ?? false
27
+ const pictureInPicture = props.pictureInPicture ?? false
28
+
29
+ const modes = new Set<string>(existingModes ?? [])
30
+ if (pictureInPicture) {
31
+ modes.add(IOS_BACKGROUND_MODE_PICTURE_IN_PICTURE)
32
+ }
33
+ const shouldEnableAudio = backgroundPlayback || pictureInPicture
34
+ if (shouldEnableAudio) {
35
+ modes.add(IOS_BACKGROUND_MODE_AUDIO)
36
+ }
37
+ if (modes.size === 0) {
38
+ return undefined
39
+ }
40
+ return [...modes]
41
+ }
42
+
43
+ export function applyPictureInPictureMainActivityAttributes(activity: {
44
+ $: Record<string, string | undefined>
45
+ }): void {
46
+ activity.$['android:supportsPictureInPicture'] = 'true'
47
+ activity.$['android:resizeableActivity'] = 'true'
48
+ }
49
+
50
+ /**
51
+ * Expo config plugin for `@moviie/player-expo`.
52
+ * Applies iOS `UIBackgroundModes` and Android PiP manifest flags. Endpoint
53
+ * overrides são via env var `EXPO_PUBLIC_MOVIIE_API_BASE_URL` (dev only).
54
+ */
55
+ const withMoviie: ConfigPlugin<WithMoviieOptions> = (config, props = {}) => {
56
+ validateWithMoviieOptions(props)
57
+
58
+ const backgroundPlayback = props.backgroundPlayback ?? false
59
+ const pictureInPicture = props.pictureInPicture ?? false
60
+
61
+ config = withInfoPlist(config, (mod) => {
62
+ const merged = mergeMoviieUIBackgroundModes(mod.modResults.UIBackgroundModes, {
63
+ backgroundPlayback,
64
+ pictureInPicture,
65
+ })
66
+ if (merged) {
67
+ mod.modResults.UIBackgroundModes = merged
68
+ }
69
+ return mod
70
+ })
71
+
72
+ config = withAndroidManifest(config, (mod) => {
73
+ if (!pictureInPicture) {
74
+ return mod
75
+ }
76
+ const activity = AndroidConfig.Manifest.getMainActivityOrThrow(mod.modResults)
77
+ applyPictureInPictureMainActivityAttributes(activity)
78
+ return mod
79
+ })
80
+
81
+ return config
82
+ }
83
+
84
+ export default withMoviie
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Configura endpoints da API Moviie a partir de **variáveis de ambiente**
3
+ * `EXPO_PUBLIC_MOVIIE_API_BASE_URL` e (raro) `EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL`.
4
+ *
5
+ * **Override é apenas para desenvolvimento local.** Apps em produção usam o
6
+ * default `https://api.moviie.ai/v1`. Por isso este módulo:
7
+ *
8
+ * - Só aplica o override quando `__DEV__ === true` (Metro/Hermes dev mode);
9
+ * - Não depende de `expo-constants` — usa só `process.env.*` que o Metro inlina
10
+ * em build time para qualquer var `EXPO_PUBLIC_*`;
11
+ * - Falha silenciosamente em produção (var inexistente → default do SDK).
12
+ *
13
+ * Vars relevantes (definidas em `.env.local` no consumer):
14
+ * EXPO_PUBLIC_MOVIIE_API_BASE_URL=http://localhost:3000/api/v1
15
+ * EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL=http://localhost:3000/telemetry/v1
16
+ */
17
+ import { configureMoviieEndpoints } from '@moviie/player-sdk'
18
+
19
+ import {
20
+ EXPO_PUBLIC_MOVIIE_API_BASE_URL_ENV,
21
+ EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL_ENV,
22
+ } from './constants'
23
+
24
+ function normalize(raw: string | undefined): string | undefined {
25
+ if (typeof raw !== 'string') return undefined
26
+ const trimmed = raw.trim()
27
+ return trimmed.length > 0 ? trimmed.replace(/\/+$/, '') : undefined
28
+ }
29
+
30
+ // `__DEV__` é definido pelo bundler RN. Em web build (Vite/Next) cai pra `process.env.NODE_ENV`.
31
+ const devMode =
32
+ typeof __DEV__ !== 'undefined'
33
+ ? __DEV__
34
+ : typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
35
+
36
+ if (devMode) {
37
+ const apiBaseUrl = normalize(process.env[EXPO_PUBLIC_MOVIIE_API_BASE_URL_ENV])
38
+ const eventsBaseUrl = normalize(process.env[EXPO_PUBLIC_MOVIIE_EVENTS_BASE_URL_ENV])
39
+
40
+ const opts: { apiBaseUrl?: string; eventsBaseUrl?: string } = {}
41
+ if (apiBaseUrl != null) opts.apiBaseUrl = apiBaseUrl
42
+ if (eventsBaseUrl != null) opts.eventsBaseUrl = eventsBaseUrl
43
+
44
+ if (opts.apiBaseUrl != null || opts.eventsBaseUrl != null) {
45
+ configureMoviieEndpoints(opts)
46
+ }
47
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Implementação do {@link MoviieCastAdapter} sobre `react-native-google-cast`.
3
+ *
4
+ * **Atenção**: este arquivo importa `react-native-google-cast` no topo. Por isso
5
+ * só deve ser importado via subpath `@moviie/player-expo/cast`, **nunca** pelo
6
+ * package principal. Consumidores que não usam cast não precisam instalar a lib.
7
+ */
8
+
9
+ import React from 'react'
10
+ import { Platform, View } from 'react-native'
11
+ import GoogleCast, { CastButton, CastState } from 'react-native-google-cast'
12
+
13
+ import type {
14
+ MoviieCastAdapter,
15
+ MoviieCastMediaPayload,
16
+ MoviieCastState,
17
+ } from '../lib/moviie-cast-adapter'
18
+
19
+ const DEFAULT_HLS_CONTENT_TYPE = 'application/x-mpegURL'
20
+
21
+ function mapCastState(state: CastState | null | undefined): MoviieCastState {
22
+ switch (state) {
23
+ case CastState.CONNECTED:
24
+ return 'connected'
25
+ case CastState.CONNECTING:
26
+ return 'connecting'
27
+ case CastState.NOT_CONNECTED:
28
+ return 'disconnected'
29
+ case CastState.NO_DEVICES_AVAILABLE:
30
+ default:
31
+ return 'unavailable'
32
+ }
33
+ }
34
+
35
+ export function createGoogleCastAdapter(): MoviieCastAdapter {
36
+ let lastKnownState: MoviieCastState = 'unavailable'
37
+
38
+ // Hidrata estado inicial em background; subscribe() já entrega o valor real
39
+ void GoogleCast.getCastState()
40
+ .then((state) => {
41
+ lastKnownState = mapCastState(state)
42
+ })
43
+ .catch(() => {
44
+ lastKnownState = 'unavailable'
45
+ })
46
+
47
+ return {
48
+ isAvailable: () => true,
49
+ getState: () => lastKnownState,
50
+ subscribe(callback) {
51
+ // Entrega estado atual imediatamente (async, não bloqueia)
52
+ void GoogleCast.getCastState()
53
+ .then((state) => {
54
+ lastKnownState = mapCastState(state)
55
+ callback(lastKnownState)
56
+ })
57
+ .catch(() => undefined)
58
+
59
+ const subscription = GoogleCast.onCastStateChanged((castState) => {
60
+ lastKnownState = mapCastState(castState)
61
+ callback(lastKnownState)
62
+ })
63
+
64
+ return () => {
65
+ subscription.remove()
66
+ }
67
+ },
68
+ async showDevicePicker() {
69
+ await GoogleCast.showCastDialog()
70
+ },
71
+ async loadMedia(payload: MoviieCastMediaPayload) {
72
+ const sessionManager = GoogleCast.getSessionManager()
73
+ const session = await sessionManager.getCurrentCastSession()
74
+ if (!session) {
75
+ return
76
+ }
77
+ await session.client.loadMedia({
78
+ autoplay: true,
79
+ startTime: payload.startSeconds,
80
+ mediaInfo: {
81
+ contentUrl: payload.uri,
82
+ contentType: payload.contentType ?? DEFAULT_HLS_CONTENT_TYPE,
83
+ streamDuration: payload.durationSeconds ?? undefined,
84
+ metadata: {
85
+ type: 'movie',
86
+ title: payload.title,
87
+ images: payload.posterUrl ? [{ url: payload.posterUrl }] : undefined,
88
+ },
89
+ },
90
+ })
91
+ },
92
+ async endSession() {
93
+ await GoogleCast.getSessionManager().endCurrentSession(true)
94
+ },
95
+ renderAndroidProxy() {
96
+ if (Platform.OS !== 'android') return null
97
+ // Register a MediaRouteButton in the window so showCastDialog() can use it
98
+ // as the primary path. Positioned off-screen so it is never visible.
99
+ return React.createElement(
100
+ View,
101
+ {
102
+ style: { position: 'absolute', top: -9999, left: -9999 },
103
+ pointerEvents: 'none',
104
+ },
105
+ React.createElement(CastButton, {
106
+ style: { width: 48, height: 48 },
107
+ })
108
+ )
109
+ },
110
+ }
111
+ }
@@ -0,0 +1,12 @@
1
+ export { createGoogleCastAdapter } from './google-cast-adapter'
2
+ export type {
3
+ MoviieCastAdapter,
4
+ MoviieCastMediaPayload,
5
+ MoviieCastState,
6
+ } from '../lib/moviie-cast-adapter'
7
+
8
+ // Side-effect: auto-register adapter when this subpath is imported.
9
+ // Enables the zero-config pattern: `import "@moviie/player-expo/cast"` in _layout.tsx.
10
+ import { createGoogleCastAdapter } from './google-cast-adapter'
11
+ import { _registerCastAdapter } from '../lib/cast-adapter-registry'
12
+ _registerCastAdapter(createGoogleCastAdapter())