feat: add expo plugins (#3933)

* feat: add expo plugins

* add export

* fix import

* fix bugs

* build `lib` to `CommonJS`

* restore `build.gradle`

* remove plugin tmp

* add expo plugin for ios caching

* add docs for expo plugin

* fix expo plugin export

* fix docs
This commit is contained in:
Krzysztof Moch 2024-07-10 11:49:13 +02:00 committed by GitHub
parent 25c74e0534
commit 08f6caa645
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 378 additions and 3 deletions

View File

@ -15,8 +15,11 @@ buildscript {
} }
} }
// This looks funny but it's necessary to keep backwards compatibility (:
def safeExtGet(prop) { def safeExtGet(prop) {
return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : project.properties["RNVideo_" + prop] return rootProject.ext.has(prop) ?
rootProject.ext.get(prop) : rootProject.ext.has("RNVideo_" + prop) ?
rootProject.ext.get("RNVideo_" + prop) : project.properties["RNVideo_" + prop]
} }
def isNewArchitectureEnabled() { def isNewArchitectureEnabled() {

1
app.plugin.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./lib/expo-plugins/withRNVideo');

View File

@ -49,6 +49,8 @@ To enable google IMA usage define add following line in your podfile:
$RNVideoUseGoogleIMA=true $RNVideoUseGoogleIMA=true
``` ```
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
</details> </details>
<details> <details>
<summary>Android</summary> <summary>Android</summary>
@ -67,6 +69,8 @@ buildscript {
### Enable custom feature in gradle file ### Enable custom feature in gradle file
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
You can disable or enable the following features by setting the following variables in your `android/build.gradle` file: You can disable or enable the following features by setting the following variables in your `android/build.gradle` file:
- `useExoplayerIMA` - Enable Google IMA SDK (Ads support) - `useExoplayerIMA` - Enable Google IMA SDK (Ads support)
- `useExoplayerRtsp` - Enable RTSP support - `useExoplayerRtsp` - Enable RTSP support

View File

@ -3,5 +3,6 @@
"misc": "Misc", "misc": "Misc",
"debug": "Debugging", "debug": "Debugging",
"new-arch": "New Architecture", "new-arch": "New Architecture",
"expo": "Expo"
"plugin": "Plugin (experimental)" "plugin": "Plugin (experimental)"
} }

40
docs/pages/other/expo.md Normal file
View File

@ -0,0 +1,40 @@
# Expo
## Expo plugin
From version `6.3.1`, we have added support for expo plugin. You can configure `react-native-video` properties in `app.json` (or `app.config.json` or `app.config.js`) file.
It's useful when you are using `expo` managed workflow (expo prebuild) as it will automatically configure `react-native-video` properties in native part of the expo project.
```json
// app.json
{
{
"name": "my app",
"plugins": [
[
"react-native-video",
{
// ...
"enableNotificationControls": true,
"androidExtensions": {
"useExoplayerRtsp": false,
"useExoplayerSmoothStreaming": false,
"useExoplayerHls": false,
"useExoplayerDash": false,
}
// ...
}
]
]
}
}
```
## Expo Plugin Properties
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| enableNotificationControls | boolean | false | Add required changes on android to use notification controls for video player |
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
| enableCacheExtension | boolean | false | Add required changes to use cache extension for video player on iOS |
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |

View File

@ -16,6 +16,7 @@
"@types/react": "~18.0.0" "@types/react": "~18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@expo/config-plugins": "^8.0.5",
"@jamesacarr/eslint-formatter-github-actions": "^0.2.0", "@jamesacarr/eslint-formatter-github-actions": "^0.2.0",
"@react-native/eslint-config": "^0.72.2", "@react-native/eslint-config": "^0.72.2",
"@release-it/conventional-changelog": "^7.0.2", "@release-it/conventional-changelog": "^7.0.2",

View File

@ -0,0 +1,50 @@
export type ConfigProps = {
/**
* Whether to require permissions to be able to use notification controls.
* @default false
*/
enableNotificationControls?: boolean;
/**
* Whether to enable background audio feature.
* @default false
*/
enableBackgroundAudio?: boolean;
/**
* Whether to include ADS extension in the app (IMA SDK)
* @default false
* @see https://thewidlarzgroup.github.io/react-native-video/component/ads
*/
enableADSExtension?: boolean;
/**
* Whether to enable cache extension for ios in the app.
* @default false
* @see https://thewidlarzgroup.github.io/react-native-video/other/caching
*/
enableCacheExtension?: boolean;
/**
* Android extensions for ExoPlayer - you can choose which extensions to include in order to reduce the size of the app.
* @default { useExoplayerRtsp: false, useExoplayerSmoothStreaming: true, useExoplayerDash: true, useExoplayerHls: true }
*/
androidExtensions?: {
/**
* Whether to use ExoPlayer's RTSP extension.
* @default false
*/
useExoplayerRtsp?: boolean;
/**
* Whether to use ExoPlayer's SmoothStreaming extension.
* @default true
*/
useExoplayerSmoothStreaming?: boolean;
/**
* Whether to use ExoPlayer's Dash extension.
* @default true
*/
useExoplayerDash?: boolean;
/**
* Whether to use ExoPlayer's HLS extension.
* @default true
*/
useExoplayerHls?: boolean;
};
};

View File

@ -0,0 +1,47 @@
import {
withGradleProperties,
type ConfigPlugin,
withDangerousMod,
} from '@expo/config-plugins';
import {writeToPodfile} from './writeToPodfile';
/**
* Sets whether to enable the IMA SDK to use ADS with `react-native-video`.
*/
export const withAds: ConfigPlugin<boolean> = (c, enableADSExtension) => {
const android_key = 'RNVideo_useExoplayerIMA';
const ios_key = 'RNVideoUseGoogleIMA';
// -------------------- ANDROID --------------------
const configWithAndroid = withGradleProperties(c, (config) => {
config.modResults = config.modResults.filter((item) => {
if (item.type === 'property' && item.key === android_key) {
return false;
}
return true;
});
config.modResults.push({
type: 'property',
key: android_key,
value: enableADSExtension.toString(),
});
return config;
});
// -------------------- IOS --------------------
const complectedConfig = withDangerousMod(configWithAndroid, [
'ios',
(config) => {
writeToPodfile(
config.modRequest.projectRoot,
ios_key,
enableADSExtension.toString(),
);
return config;
},
]);
return complectedConfig;
};

View File

@ -0,0 +1,53 @@
import {withGradleProperties, type ConfigPlugin} from '@expo/config-plugins';
import type {ConfigProps} from './@types';
/**
* Sets the Android extensions for ExoPlayer in `gradle.properties`.
* You can choose which extensions to include in order to reduce the size of the app.
*/
export const withAndroidExtensions: ConfigPlugin<
ConfigProps['androidExtensions']
> = (c, androidExtensions) => {
const keys = [
'RNVideo_useExoplayerRtsp',
'RNVideo_useExoplayerSmoothStreaming',
'RNVideo_useExoplayerDash',
'RNVideo_useExoplayerHls',
];
if (!androidExtensions) {
androidExtensions = {
useExoplayerRtsp: false,
useExoplayerSmoothStreaming: true,
useExoplayerDash: true,
useExoplayerHls: true,
};
}
return withGradleProperties(c, (config) => {
config.modResults = config.modResults.filter((item) => {
if (item.type === 'property' && keys.includes(item.key)) {
return false;
}
return true;
});
for (const key of keys) {
const valueKey = key.replace(
'RNVideo_',
'',
) as keyof typeof androidExtensions;
const value = androidExtensions
? androidExtensions[valueKey] ?? false
: false;
config.modResults.push({
type: 'property',
key,
value: value.toString(),
});
}
return config;
});
};

View File

@ -0,0 +1,26 @@
import {withInfoPlist, type ConfigPlugin} from '@expo/config-plugins';
/**
* Sets `UIBackgroundModes` in `Info.plist` to enable background audio on Apple platforms.
* This is required for audio to continue playing when the app is in the background.
*/
export const withBackgroundAudio: ConfigPlugin<boolean> = (
c,
enableBackgroundAudio,
) => {
return withInfoPlist(c, (config) => {
const modes = config.modResults.UIBackgroundModes || [];
if (enableBackgroundAudio) {
if (!modes.includes('audio')) {
modes.push('audio');
}
} else {
config.modResults.UIBackgroundModes = modes.filter(
(mode: string) => mode !== 'audio',
);
}
return config;
});
};

View File

@ -0,0 +1,24 @@
import {type ConfigPlugin, withDangerousMod} from '@expo/config-plugins';
import {writeToPodfile} from './writeToPodfile';
/**
* Sets whether to include the cache dependency to use cache on iOS with `react-native-video`.
*/
export const withCaching: ConfigPlugin<boolean> = (
c,
enableCachingExtension,
) => {
const ios_key = 'RNVideoUseVideoCaching';
return withDangerousMod(c, [
'ios',
(config) => {
writeToPodfile(
config.modRequest.projectRoot,
ios_key,
enableCachingExtension.toString(),
);
return config;
},
]);
};

View File

@ -0,0 +1,52 @@
import {withAndroidManifest, type ConfigPlugin} from '@expo/config-plugins';
export const withNotificationControls: ConfigPlugin<boolean> = (
c,
enableNotificationControls,
) => {
return withAndroidManifest(c, (config) => {
const manifest = config.modResults.manifest;
if (!enableNotificationControls) {
return config;
}
if (!manifest.application) {
console.warn(
'AndroidManifest.xml is missing an <application> element - skipping adding notification controls related config.',
);
return config;
}
// Add the service to the AndroidManifest.xml
manifest.application.map((application) => {
if (!application.service) {
application.service = [];
}
application.service.push({
$: {
'android:name': 'com.brentvatne.exoplayer.VideoPlaybackService',
'android:exported': 'false',
// @ts-expect-error: 'android:foregroundServiceType' does not exist in type 'ManifestServiceAttributes'.
'android:foregroundServiceType': 'mediaPlayback',
},
'intent-filter': [
{
action: [
{
$: {
'android:name': 'androidx.media3.session.MediaSessionService',
},
},
],
},
],
});
return application;
});
return config;
});
};

View File

@ -0,0 +1,45 @@
import {type ConfigPlugin, createRunOncePlugin} from '@expo/config-plugins';
import type {ConfigProps} from './@types';
import {withNotificationControls} from './withNotificationControls';
import {withAndroidExtensions} from './withAndroidExtensions';
import {withAds} from './withAds';
import {withBackgroundAudio} from './withBackgroundAudio';
import {withPermissions} from '@expo/config-plugins/build/android/Permissions';
import {withCaching} from './withCaching';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../package.json');
const withRNVideo: ConfigPlugin<ConfigProps> = (config, props = {}) => {
const androidPermissions = [];
if (props.enableNotificationControls) {
config = withNotificationControls(config, props.enableNotificationControls);
androidPermissions.push('android.permission.FOREGROUND_SERVICE');
androidPermissions.push(
'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
);
}
if (props.androidExtensions != null) {
config = withAndroidExtensions(config, props.androidExtensions);
}
if (props.enableADSExtension) {
config = withAds(config, props.enableADSExtension);
}
if (props.enableCacheExtension) {
config = withCaching(config, props.enableCacheExtension);
}
if (props.enableBackgroundAudio) {
config = withBackgroundAudio(config, props.enableBackgroundAudio);
}
config = withPermissions(config, androidPermissions);
return config;
};
export default createRunOncePlugin(withRNVideo, pkg.name, pkg.version);

View File

@ -0,0 +1,27 @@
import fs from 'fs';
import path from 'path';
import {mergeContents} from '@expo/config-plugins/build/utils/generateCode';
export const writeToPodfile = (
projectRoot: string,
key: string,
value: string,
) => {
const podfilePath = path.join(projectRoot, 'ios', 'Podfile');
const podfileContent = fs.readFileSync(podfilePath, 'utf8');
const newPodfileContent = mergeContents({
tag: `rn-video-set-${key.toLowerCase()}`,
src: podfileContent,
newSrc: `$${key} = ${value}`,
anchor: /platform :ios/,
offset: 0,
comment: '#',
});
if (newPodfileContent.didMerge) {
fs.writeFileSync(podfilePath, newPodfileContent.contents);
} else {
console.warn(`RNV - Failed to write "$${key} = ${value}" to Podfile`);
}
};

View File

@ -14,7 +14,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"jsx": "react", "jsx": "react",
"lib": ["esnext"], "lib": ["esnext"],
"module": "esnext", "module": "CommonJS",
"moduleResolution": "node", "moduleResolution": "node",
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitReturns": true, "noImplicitReturns": true,
@ -27,7 +27,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "esnext", "target": "esnext",
"verbatimModuleSyntax": true "verbatimModuleSyntax": false,
"allowSyntheticDefaultImports": true
}, },
"exclude": ["examples", "lib", "docs"] "exclude": ["examples", "lib", "docs"]
} }