diff --git a/android/build.gradle b/android/build.gradle index 81e11c04..7920bf65 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,8 +15,11 @@ buildscript { } } +// This looks funny but it's necessary to keep backwards compatibility (: 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() { diff --git a/app.plugin.js b/app.plugin.js new file mode 100644 index 00000000..32cf65a9 --- /dev/null +++ b/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./lib/expo-plugins/withRNVideo'); diff --git a/docs/pages/installation.md b/docs/pages/installation.md index 28c58fca..e09aa972 100644 --- a/docs/pages/installation.md +++ b/docs/pages/installation.md @@ -49,6 +49,8 @@ To enable google IMA usage define add following line in your podfile: $RNVideoUseGoogleIMA=true ``` +**If you are using Expo you can use [expo plugin](other/expo.md) for it** +
Android @@ -67,6 +69,8 @@ buildscript { ### 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: - `useExoplayerIMA` - Enable Google IMA SDK (Ads support) - `useExoplayerRtsp` - Enable RTSP support diff --git a/docs/pages/other/_meta.json b/docs/pages/other/_meta.json index f980cb56..a60eb7ce 100644 --- a/docs/pages/other/_meta.json +++ b/docs/pages/other/_meta.json @@ -3,5 +3,6 @@ "misc": "Misc", "debug": "Debugging", "new-arch": "New Architecture", + "expo": "Expo" "plugin": "Plugin (experimental)" } \ No newline at end of file diff --git a/docs/pages/other/expo.md b/docs/pages/other/expo.md new file mode 100644 index 00000000..fdb41a9e --- /dev/null +++ b/docs/pages/other/expo.md @@ -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 | \ No newline at end of file diff --git a/package.json b/package.json index 6179f953..22e86146 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@types/react": "~18.0.0" }, "devDependencies": { + "@expo/config-plugins": "^8.0.5", "@jamesacarr/eslint-formatter-github-actions": "^0.2.0", "@react-native/eslint-config": "^0.72.2", "@release-it/conventional-changelog": "^7.0.2", diff --git a/src/expo-plugins/@types.ts b/src/expo-plugins/@types.ts new file mode 100644 index 00000000..b9aff226 --- /dev/null +++ b/src/expo-plugins/@types.ts @@ -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; + }; +}; diff --git a/src/expo-plugins/withAds.ts b/src/expo-plugins/withAds.ts new file mode 100644 index 00000000..e816a601 --- /dev/null +++ b/src/expo-plugins/withAds.ts @@ -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 = (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; +}; diff --git a/src/expo-plugins/withAndroidExtensions.ts b/src/expo-plugins/withAndroidExtensions.ts new file mode 100644 index 00000000..d242ff83 --- /dev/null +++ b/src/expo-plugins/withAndroidExtensions.ts @@ -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; + }); +}; diff --git a/src/expo-plugins/withBackgroundAudio.ts b/src/expo-plugins/withBackgroundAudio.ts new file mode 100644 index 00000000..abaf8e5d --- /dev/null +++ b/src/expo-plugins/withBackgroundAudio.ts @@ -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 = ( + 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; + }); +}; diff --git a/src/expo-plugins/withCaching.ts b/src/expo-plugins/withCaching.ts new file mode 100644 index 00000000..75034b4f --- /dev/null +++ b/src/expo-plugins/withCaching.ts @@ -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 = ( + c, + enableCachingExtension, +) => { + const ios_key = 'RNVideoUseVideoCaching'; + + return withDangerousMod(c, [ + 'ios', + (config) => { + writeToPodfile( + config.modRequest.projectRoot, + ios_key, + enableCachingExtension.toString(), + ); + return config; + }, + ]); +}; diff --git a/src/expo-plugins/withNotificationControls.ts b/src/expo-plugins/withNotificationControls.ts new file mode 100644 index 00000000..8c76f814 --- /dev/null +++ b/src/expo-plugins/withNotificationControls.ts @@ -0,0 +1,52 @@ +import {withAndroidManifest, type ConfigPlugin} from '@expo/config-plugins'; + +export const withNotificationControls: ConfigPlugin = ( + 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 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; + }); +}; diff --git a/src/expo-plugins/withRNVideo.ts b/src/expo-plugins/withRNVideo.ts new file mode 100644 index 00000000..35dfb3b2 --- /dev/null +++ b/src/expo-plugins/withRNVideo.ts @@ -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 = (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); diff --git a/src/expo-plugins/writeToPodfile.ts b/src/expo-plugins/writeToPodfile.ts new file mode 100644 index 00000000..85617b76 --- /dev/null +++ b/src/expo-plugins/writeToPodfile.ts @@ -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`); + } +}; diff --git a/tsconfig.json b/tsconfig.json index 14d2f82f..493adda9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "forceConsistentCasingInFileNames": true, "jsx": "react", "lib": ["esnext"], - "module": "esnext", + "module": "CommonJS", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, @@ -27,7 +27,8 @@ "skipLibCheck": true, "strict": true, "target": "esnext", - "verbatimModuleSyntax": true + "verbatimModuleSyntax": false, + "allowSyntheticDefaultImports": true }, "exclude": ["examples", "lib", "docs"] }