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
15 changed files with 378 additions and 3 deletions

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`);
}
};