@moviie/player-expo 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/app.plugin.cjs +3 -0
- package/dist/cast.cjs +100 -0
- package/dist/cast.cjs.map +1 -0
- package/dist/cast.d.cts +15 -0
- package/dist/cast.d.ts +15 -0
- package/dist/cast.mjs +92 -0
- package/dist/cast.mjs.map +1 -0
- package/dist/chunk-67DJ7NOB.mjs +294 -0
- package/dist/chunk-67DJ7NOB.mjs.map +1 -0
- package/dist/chunk-7U2LKIGU.mjs +12 -0
- package/dist/chunk-7U2LKIGU.mjs.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +10 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/index.cjs +3934 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +450 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.mjs +3571 -0
- package/dist/index.mjs.map +1 -0
- package/dist/layout.cjs +217 -0
- package/dist/layout.cjs.map +1 -0
- package/dist/layout.d.cts +20 -0
- package/dist/layout.d.ts +20 -0
- package/dist/layout.mjs +4 -0
- package/dist/layout.mjs.map +1 -0
- package/dist/moviie-cast-adapter-DmSU2u3j.d.cts +53 -0
- package/dist/moviie-cast-adapter-DmSU2u3j.d.ts +53 -0
- package/dist/plugin/with-moviie.cjs +88 -0
- package/dist/plugin/with-moviie.cjs.map +1 -0
- package/dist/plugin/with-moviie.d.cts +52 -0
- package/dist/plugin/with-moviie.d.ts +52 -0
- package/dist/plugin/with-moviie.mjs +76 -0
- package/dist/plugin/with-moviie.mjs.map +1 -0
- package/package.json +134 -0
- package/plugin/validate-options.ts +31 -0
- package/plugin/with-moviie-types.ts +21 -0
- package/plugin/with-moviie.ts +84 -0
- package/src/apply-expo-moviie-endpoints.ts +47 -0
- package/src/cast/google-cast-adapter.ts +111 -0
- package/src/cast/index.ts +12 -0
- package/src/components/controls/moviie-bottom-timeline.tsx +477 -0
- package/src/components/controls/moviie-cast-buttons.tsx +96 -0
- package/src/components/controls/moviie-chrome-edge-gradients.tsx +162 -0
- package/src/components/controls/moviie-controls.tsx +585 -0
- package/src/components/controls/moviie-skin-chrome-context.tsx +374 -0
- package/src/components/controls/moviie-skin-smart-progress.tsx +157 -0
- package/src/components/icons/embed-media-icons.tsx +282 -0
- package/src/components/icons/moviie-embed-brand-mark.tsx +58 -0
- package/src/components/moviie-error-boundary.tsx +80 -0
- package/src/components/moviie-player-error-shell.tsx +143 -0
- package/src/components/moviie-player-loading-shell.tsx +59 -0
- package/src/components/moviie-skin-custom-fullscreen-modal.tsx +232 -0
- package/src/components/moviie-video-props.ts +134 -0
- package/src/components/moviie-video.tsx +568 -0
- package/src/components/moviie-video.web.tsx +167 -0
- package/src/components/overlays/moviie-watermark.tsx +53 -0
- package/src/constants.ts +374 -0
- package/src/hooks/use-moviie-event.ts +103 -0
- package/src/hooks/use-moviie-playback-ended.ts +14 -0
- package/src/hooks/use-moviie-playback-resume-persistence.ts +76 -0
- package/src/hooks/use-moviie-playback.ts +99 -0
- package/src/hooks/use-moviie-player-types.ts +55 -0
- package/src/hooks/use-moviie-player.ts +236 -0
- package/src/hooks/use-moviie-player.web.ts +57 -0
- package/src/hooks/use-moviie-telemetry.ts +133 -0
- package/src/index.ts +90 -0
- package/src/layout.ts +4 -0
- package/src/lib/add-moviie-playback-ended-listener.ts +16 -0
- package/src/lib/build-moviie-branding-marketing-url.ts +14 -0
- package/src/lib/build-moviie-embed-brand-home-url.ts +16 -0
- package/src/lib/build-moviie-skin-layout-metrics.ts +166 -0
- package/src/lib/build-moviie-video-presentation.ts +42 -0
- package/src/lib/build-moviie-watch-embed-url.ts +30 -0
- package/src/lib/cast-adapter-registry.ts +13 -0
- package/src/lib/clamp-unit-interval.ts +3 -0
- package/src/lib/compute-custom-fullscreen-stage-dimensions.ts +39 -0
- package/src/lib/compute-moviie-playback-ended.ts +31 -0
- package/src/lib/compute-playback-buffering.ts +41 -0
- package/src/lib/compute-playback-progress-ratio.ts +33 -0
- package/src/lib/compute-timeline-scrub-commit-time.ts +40 -0
- package/src/lib/custom-fullscreen-native-orientation.ts +88 -0
- package/src/lib/format-playback-clock.ts +27 -0
- package/src/lib/jsx-native-bridge.ts +45 -0
- package/src/lib/map-smart-progress-display-ratio.ts +43 -0
- package/src/lib/moviie-cast-adapter.ts +53 -0
- package/src/lib/moviie-error-display.ts +149 -0
- package/src/lib/moviie-shell-tokens.ts +27 -0
- package/src/lib/moviie-telemetry-callbacks.ts +76 -0
- package/src/lib/optional-status-bar.ts +48 -0
- package/src/lib/partition-moviie-video-host-style.ts +54 -0
- package/src/lib/remember-playback-position-storage.ts +98 -0
- package/src/lib/resolve-moviie-skin-layout-scale.ts +17 -0
- package/src/lib/resolve-orientation-lock-for-rotate-z-deg.ts +21 -0
- package/src/lib/resolve-skin-accent-color.ts +10 -0
- package/src/lib/resolve-web-embed-iframe-src.ts +17 -0
- package/src/lib/warn-once.ts +23 -0
- package/src/platform/native-application-id.ts +10 -0
- package/src/platform/platform-client-info.ts +31 -0
- package/src/playback/fetch-playback-fresh.ts +27 -0
- package/src/playback/playback-memory-cache.ts +52 -0
- package/src/provider/moviie-provider.tsx +99 -0
- package/src/secure-store-viewer-token-store.ts +80 -0
|
@@ -0,0 +1,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())
|