diff --git a/apps/brownfield-tester/ios/Podfile b/apps/brownfield-tester/ios/Podfile
index afecad9ebb649d..ab14d620583f3a 100644
--- a/apps/brownfield-tester/ios/Podfile
+++ b/apps/brownfield-tester/ios/Podfile
@@ -12,7 +12,6 @@ target 'BrownfieldTester' do
projectRoot: project_root,
exclude: [
'expo-splash-screen',
- 'expo-dev-menu'
],
})
diff --git a/apps/brownfield-tester/ios/Podfile.lock b/apps/brownfield-tester/ios/Podfile.lock
index 66890a08a74de5..c5ec804bf1341e 100644
--- a/apps/brownfield-tester/ios/Podfile.lock
+++ b/apps/brownfield-tester/ios/Podfile.lock
@@ -37,6 +37,35 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
+ - expo-dev-menu (55.0.5):
+ - boost
+ - DoubleConversion
+ - expo-dev-menu/Main (= 55.0.5)
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- expo-dev-menu-interface (55.0.1)
- ExpoAsset (55.0.5):
- ExpoModulesCore
@@ -2951,6 +2980,7 @@ DEPENDENCIES:
- EXJSONUtils (from `../../../packages/expo-json-utils/ios`)
- EXManifests (from `../../../packages/expo-manifests/ios`)
- Expo (from `../../../packages/expo`)
+ - expo-dev-menu (from `../../../packages/expo-dev-menu`)
- expo-dev-menu-interface (from `../../../packages/expo-dev-menu-interface/ios`)
- ExpoAsset (from `../../../packages/expo-asset/ios`)
- ExpoBrownfield (from `../../../packages/expo-brownfield/ios`)
@@ -3076,6 +3106,8 @@ EXTERNAL SOURCES:
:path: "../../../packages/expo-manifests/ios"
Expo:
:path: "../../../packages/expo"
+ expo-dev-menu:
+ :path: "../../../packages/expo-dev-menu"
expo-dev-menu-interface:
:path: "../../../packages/expo-dev-menu-interface/ios"
ExpoAsset:
@@ -3388,6 +3420,6 @@ SPEC CHECKSUMS:
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 3d56e80930898685da43cbf53b5843932df8e765
-PODFILE CHECKSUM: 138a890cb547592c982ba14a2a7126f821424938
+PODFILE CHECKSUM: 1ca978f73a30f65633c68b87b4dcd2037d2b9a00
COCOAPODS: 1.16.2
diff --git a/apps/native-component-list/src/screens/UI/ScrollViewScreen.ios.tsx b/apps/native-component-list/src/screens/UI/ScrollViewScreen.ios.tsx
new file mode 100644
index 00000000000000..0e4c1d6ffc6b76
--- /dev/null
+++ b/apps/native-component-list/src/screens/UI/ScrollViewScreen.ios.tsx
@@ -0,0 +1,63 @@
+import { Host, ScrollView, VStack, Text, RoundedRectangle } from '@expo/ui/swift-ui';
+import { frame, foregroundStyle, padding, font } from '@expo/ui/swift-ui/modifiers';
+
+export default function ScrollViewScreen() {
+ return (
+
+
+
+ {Array.from({ length: 20 }, (_, i) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function ScrollViewHorizontalScreen() {
+ return (
+
+
+ {Array.from({ length: 20 }, (_, i) => (
+
+ ))}
+
+
+ );
+}
+
+export function ScrollViewHideIndicatorsScreen() {
+ return (
+
+
+
+ {Array.from({ length: 30 }, (_, i) => (
+
+ {`Item ${i + 1}`}
+
+ ))}
+
+
+
+ );
+}
+
+ScrollViewScreen.navigationOptions = {
+ title: 'ScrollView',
+};
diff --git a/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx b/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx
index 8d724a55db898a..39894553d7166a 100644
--- a/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx
+++ b/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx
@@ -194,6 +194,14 @@ export const UIScreens = [
return optionalRequire(() => require('./MatchedGeometryEffectScreen'));
},
},
+ {
+ name: 'ScrollView component',
+ route: 'ui/scrollview',
+ options: {},
+ getComponent() {
+ return optionalRequire(() => require('./ScrollViewScreen'));
+ },
+ },
{
name: 'Shapes',
route: 'ui/shapes',
diff --git a/docs/package.json b/docs/package.json
index 8a5bdf8e0bec4c..81b5afc2e06ddd 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -36,7 +36,7 @@
"@expo/styleguide": "^9.3.1",
"@expo/styleguide-base": "^2.0.5",
"@expo/styleguide-icons": "^2.3.4",
- "@expo/styleguide-search-ui": "^3.3.1",
+ "@expo/styleguide-search-ui": "^3.3.3",
"@kapaai/react-sdk": "^0.9.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
diff --git a/docs/pages/develop/development-builds/expo-go-to-dev-build.mdx b/docs/pages/develop/development-builds/expo-go-to-dev-build.mdx
index 3f8c0c576ee5ee..d0290e2d914cc3 100644
--- a/docs/pages/develop/development-builds/expo-go-to-dev-build.mdx
+++ b/docs/pages/develop/development-builds/expo-go-to-dev-build.mdx
@@ -38,7 +38,7 @@ The Expo Dev Client library includes the launcher UI (shown in the screenshots b
With Expo Go, you only needed to build the JavaScript bundle, but with development builds you also need to compile the native app. With Expo, there are two parts to building your native app:
-1. Generate the native **android** and/or **ios** directories ([read more](/develop/development-builds/expo-go-to-dev-build/#cng-and-prebuild) on when and how you'll need to do this)
+1. Generate the native **android** and/or **ios** directories ([read more](/develop/development-builds/expo-go-to-dev-build/#prebuild) on when and how you'll need to do this)
2. Use native build tools to compile the native app(s)
Once you've built your native app, you won't need to build it again unless you add or update a library with native code, or change any native code or configuration, such as the app name.
diff --git a/docs/pages/develop/tools.mdx b/docs/pages/develop/tools.mdx
index d8bf98668ffe21..41939a7751b8ca 100644
--- a/docs/pages/develop/tools.mdx
+++ b/docs/pages/develop/tools.mdx
@@ -16,7 +16,7 @@ When you create a new project with Expo, learning about the following essential
## Expo CLI
-Expo CLI is a development tool and is installed automatically with `expo` package when you create a new project. You can use it by leveraging `npx` (a Node.js package runner).
+Expo CLI is a development tool and is installed automatically with the `expo` package when you create a new project. You can use it by leveraging `npx` (a Node.js package runner).
It is designed to help you move faster during the app development phase. For example, your first interaction with Expo CLI is starting the development server by running the command: `npx expo start`.
diff --git a/docs/pages/guides/icons.mdx b/docs/pages/guides/icons.mdx
index 2e292ce59824cf..13deec3f1b6d52 100644
--- a/docs/pages/guides/icons.mdx
+++ b/docs/pages/guides/icons.mdx
@@ -69,6 +69,8 @@ export default function CustomIconExample() {
The `createIconSetFromIcoMoon` method is used to create a custom font based on an [IcoMoon](https://icomoon.io/) config file. You have to save the **selection.json** and **.ttf** in your project, preferably in the **assets** directory, and then load the font using either `useFonts` hook or `Font.loadAsync` method from `expo-font`.
+> **IcoMoon app versions:** The [new IcoMoon app](https://icomoon.io/new-app) exports a different JSON format than the [old IcoMoon app](https://icomoon.io/app). The current `createIconSetFromIcoMoon` function only supports the old app's output. See the [Pull Request on GitHub](https://github.com/expo/vector-icons/pull/356), which adds support for the new format. This support will work once released in `@expo/vector-icons`.
+
See the example below that uses the `useFonts` hook to load the font:
+
+## Usage
+
+### Basic vertical scroll view
+
+A simple vertically scrollable list of text items.
+
+```tsx ScrollViewVerticalExample.tsx
+import { Host, ScrollView, VStack, Text } from '@expo/ui/swift-ui';
+import { padding } from '@expo/ui/swift-ui/modifiers';
+
+export default function ScrollViewVerticalExample() {
+ return (
+
+
+
+ {Array.from({ length: 30 }, (_, i) => (
+
+ {`Item ${i + 1}`}
+
+ ))}
+
+
+
+ );
+}
+```
+
+### Horizontal scroll view
+
+Use the `axes` prop to scroll horizontally.
+
+```tsx ScrollViewHorizontalExample.tsx
+import { Host, ScrollView, HStack, RoundedRectangle } from '@expo/ui/swift-ui';
+import { frame, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
+
+export default function ScrollViewHorizontalExample() {
+ return (
+
+
+
+ {Array.from({ length: 20 }, (_, i) => (
+
+ ))}
+
+
+
+ );
+}
+```
+
+### Hidden scroll indicators
+
+Set `showsIndicators` to `false` to hide the scroll bars.
+
+```tsx ScrollViewHiddenIndicatorsExample.tsx
+import { Host, ScrollView, VStack, Text } from '@expo/ui/swift-ui';
+
+export default function ScrollViewHiddenIndicatorsExample() {
+ return (
+
+
+
+ {Array.from({ length: 30 }, (_, i) => (
+ {`Item ${i + 1}`}
+ ))}
+
+
+
+ );
+}
+```
+
+## API
+
+```tsx
+import { ScrollView } from '@expo/ui/swift-ui';
+```
+
+
diff --git a/docs/public/static/data/unversioned/expo-ui/swift-ui/scrollview.json b/docs/public/static/data/unversioned/expo-ui/swift-ui/scrollview.json
new file mode 100644
index 00000000000000..d7d1d8b5c29528
--- /dev/null
+++ b/docs/public/static/data/unversioned/expo-ui/swift-ui/scrollview.json
@@ -0,0 +1 @@
+{"schemaVersion":"2.0","name":"expo-ui/swift-ui/scrollview","variant":"project","kind":1,"children":[{"name":"ScrollViewProps","variant":"declaration","kind":2097152,"type":{"type":"intersection","types":[{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"axes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The scrollable axes."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'vertical'"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"vertical"},{"type":"literal","value":"horizontal"},{"type":"literal","value":"both"}]}},{"name":"children","variant":"declaration","kind":1024,"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"React.ReactNode","package":"@types/react"}},{"name":"showsIndicators","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show scroll indicators."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]}},{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/swift-ui/types.ts","qualifiedName":"CommonViewModifierProps"},"name":"CommonViewModifierProps","package":"@expo/ui"}]}},{"name":"ScrollView","variant":"declaration","kind":64,"signatures":[{"name":"ScrollView","variant":"signature","kind":4096,"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"ScrollViewProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"React.JSX.Element"}}]}],"packageName":"@expo/ui"}
\ No newline at end of file
diff --git a/docs/public/static/data/unversioned/expo-video.json b/docs/public/static/data/unversioned/expo-video.json
index 0c57bda278475d..31beb0537de14e 100644
--- a/docs/public/static/data/unversioned/expo-video.json
+++ b/docs/public/static/data/unversioned/expo-video.json
@@ -1 +1 @@
-{"schemaVersion":"2.0","name":"expo-video","variant":"project","kind":1,"children":[{"name":"VideoPlayer","variant":"declaration","kind":128,"comment":{"summary":[{"kind":"text","text":"A class that represents an instance of the video player."}]},"children":[{"name":"allowsExternalPlayback","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the player should allow external playback."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"audioMixingMode","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines how the player will interact with other audio playing in the system."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'auto'"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"AudioMixingMode","package":"expo-video"}},{"name":"audioTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the audio track currently played by the player. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" when no audio is played."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"null"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"reference","name":"AudioTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"availableAudioTracks","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"An array of audio tracks available for the current video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","name":"AudioTrack","package":"expo-video"}}},{"name":"availableSubtitleTracks","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"An array of subtitle tracks available for the current video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","name":"SubtitleTrack","package":"expo-video"}}},{"name":"availableVideoTracks","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"An array of video tracks available for the current video.\n\n> On iOS, when using a HLS source, make sure that the uri contains "},{"kind":"code","text":"`.m3u8`"},{"kind":"text","text":" extension or that the ["},{"kind":"code","text":"`contentType`"},{"kind":"text","text":"](#contenttype) property of the ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource) has been set to "},{"kind":"code","text":"`'hls'`"},{"kind":"text","text":". Otherwise, the video tracks will not be available."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","name":"VideoTrack","package":"expo-video"}}},{"name":"bufferedPosition","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Float value indicating how far the player has buffered the video in seconds.\n\nThis value is 0 when the player has not buffered up to the current playback time.\nWhen it's impossible to determine the buffer state (for example, when the player isn't playing any media), this value is -1."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"bufferOptions","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies buffer options which will be used by the player when buffering the video.\n\n> You should provide a "},{"kind":"code","text":"`BufferOptions`"},{"kind":"text","text":" object when setting this property. Setting individual buffer properties is not supported."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"BufferOptions","package":"expo-video"}},{"name":"currentLiveTimestamp","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"The exact timestamp when the currently displayed video frame was sent from the server,\nbased on the "},{"kind":"code","text":"`EXT-X-PROGRAM-DATE-TIME`"},{"kind":"text","text":" tag in the livestream metadata.\nIf this metadata is missing, this property will return "},{"kind":"code","text":"`null`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentOffsetFromLive","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Float value indicating the latency of the live stream in seconds.\nIf a livestream doesn't have the required metadata, this will return "},{"kind":"code","text":"`null`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current playback time in seconds.\n\nIf the player is not yet playing, this value indicates the time position\nat which playback will begin once the "},{"kind":"code","text":"`play()`"},{"kind":"text","text":" method is called.\n\nSetting "},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":" to a new value seeks the player to the given time.\nCheck out the ["},{"kind":"code","text":"`seekTolerance`"},{"kind":"text","text":"](#seektolerance) property to configure the seeking precision."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"duration","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Float value indicating the duration of the current video in seconds."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"isExternalPlaybackActive","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Indicates whether the player is currently playing back the media to an external device via AirPlay."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"isLive","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Boolean value indicating whether the player is currently playing a live stream."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"keepScreenOnWhilePlaying","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean indicating if the player should keep the screen on while playing.\n\n> On Android, this property has an effect only when a ["},{"kind":"code","text":"`VideoView`"},{"kind":"text","text":"](#videoview) is visible. If you want to keep the screen awake at all times use ["},{"kind":"code","text":"`expo-keep-awake`"},{"kind":"text","text":"]("},{"kind":"relative-link","text":"./keep-awake/"},{"kind":"text","text":")."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"loop","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the player should automatically replay after reaching the end of the video."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"muted","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently muted.\nSetting this property to "},{"kind":"code","text":"`true`"},{"kind":"text","text":"/"},{"kind":"code","text":"`false`"},{"kind":"text","text":" will mute/unmute the player."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"playbackRate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value between "},{"kind":"code","text":"`0`"},{"kind":"text","text":" and "},{"kind":"code","text":"`16.0`"},{"kind":"text","text":" indicating the current playback speed of the player."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"1.0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"playing","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently playing.\n> Use "},{"kind":"code","text":"`play`"},{"kind":"text","text":" and "},{"kind":"code","text":"`pause`"},{"kind":"text","text":" methods to control the playback."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"preservesPitch","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value indicating if the player should correct audio pitch when the playback speed changes."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"scrubbingModeOptions","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the scrubbing mode is enabled and what scrubbing optimizations should be enabled.\n\n> See ["},{"kind":"code","text":"`SeekTolerance`"},{"kind":"text","text":"](#seektolerance) to set the seeking tolerance, which can also affect the scrubbing performance."}]},"type":{"type":"reference","name":"ScrubbingModeOptions","package":"expo-video"}},{"name":"seekTolerance","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n\nThis property affects the precision of setting the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property and the ["},{"kind":"code","text":"`seekBy`"},{"kind":"text","text":"](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n\nBy default, the player seeks to the exact requested time.\n\n> If you are trying to optimize for scrubbing (many frequent seeks), also see ["},{"kind":"code","text":"`ScrubbingModeOptions`"},{"kind":"text","text":"](#scrubbingmodeoptions-1)."}]},"type":{"type":"reference","name":"SeekTolerance","package":"expo-video"}},{"name":"showNowPlayingNotification","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value determining whether the player should show the now playing notification.\n\n> **Note**: On Android, "},{"kind":"code","text":"`supportsBackgroundPlayback`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be "},{"kind":"code","text":"`true`"},{"kind":"text","text":" for the now playing notification to work."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"status","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Indicates the current status of the player."}]},"type":{"type":"reference","name":"VideoPlayerStatus","package":"expo-video"}},{"name":"staysActiveInBackground","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the player should continue playing after the app enters the background.\n\n> **Note**: The "},{"kind":"code","text":"`supportsBackgroundPlayback`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be "},{"kind":"code","text":"`true`"},{"kind":"text","text":" for the background playback to work."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"subtitleTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the subtitle track which is currently displayed by the player. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" when no subtitles are displayed.\n\n> To ensure a valid subtitle track, always assign one of the subtitle tracks from the ["},{"kind":"code","text":"`availableSubtitleTracks`"},{"kind":"text","text":"](#availablesubtitletracks) array."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"null"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"reference","name":"SubtitleTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"targetOffsetFromLive","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the time offset from the live in seconds."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"timeUpdateEventInterval","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the interval in seconds at which the player will emit the ["},{"kind":"code","text":"`timeUpdate`"},{"kind":"text","text":"](#videoplayerevents) event.\nWhen the value is equal to "},{"kind":"code","text":"`0`"},{"kind":"text","text":", the event will not be emitted."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"videoTrack","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Specifies the video track currently played by the player. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" when no video is displayed."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"null"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"volume","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value between "},{"kind":"code","text":"`0`"},{"kind":"text","text":" and "},{"kind":"code","text":"`1.0`"},{"kind":"text","text":" representing the current volume.\nMuting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\nwhen unmuted. Similarly, setting the volume doesn't unmute the player."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"1.0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"addListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"addListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Adds a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"type":{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/ts-declarations/EventEmitter.ts","qualifiedName":"EventSubscription"},"name":"EventSubscription","package":"expo-modules-core"},"inheritedFrom":{"type":"reference","name":"SharedObject.addListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.addListener","package":"expo-modules-core"}},{"name":"emit","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"emit","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Synchronously calls all the listeners attached to that specific event.\nThe event can include any number of arguments that will be passed to the listeners."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"args","variant":"param","kind":32768,"flags":{"isRest":true},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Parameters"},"typeArguments":[{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}],"name":"Parameters","package":"typescript"}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.emit","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.emit","package":"expo-modules-core"}},{"name":"generateThumbnailsAsync","variant":"declaration","kind":2048,"signatures":[{"name":"generateThumbnailsAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Generates thumbnails from the currently played asset. The thumbnails are references to native images,\nthus they can be used as a source of the "},{"kind":"code","text":"`Image`"},{"kind":"text","text":" component from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"times","variant":"param","kind":32768,"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"array","elementType":{"type":"intrinsic","name":"number"}}]}},{"name":"options","variant":"param","kind":32768,"flags":{"isOptional":true},"type":{"type":"reference","name":"VideoThumbnailOptions","package":"expo-video"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"array","elementType":{"type":"reference","name":"VideoThumbnail","package":"expo-video"}}],"name":"Promise","package":"typescript"}}]},{"name":"listenerCount","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"listenerCount","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Returns a number of listeners added to the given event."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"number"},"inheritedFrom":{"type":"reference","name":"SharedObject.listenerCount","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.listenerCount","package":"expo-modules-core"}},{"name":"pause","variant":"declaration","kind":2048,"signatures":[{"name":"pause","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Pauses the player."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"play","variant":"declaration","kind":2048,"signatures":[{"name":"play","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Resumes the player."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"release","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"release","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"A function that detaches the JS and native objects to let the native object deallocate\nbefore the JS object gets deallocated by the JS garbage collector. Any subsequent calls to native\nfunctions of the object will throw an error as it is no longer associated with its native counterpart.\n\nIn most cases, you should never need to use this function, except some specific performance-critical cases when\nmanual memory management makes sense and the native object is known to exclusively retain some native memory\n(such as binary data or image bitmap). Before calling this function, you should ensure that nothing else will use\nthis object later on. Shared objects created by React hooks are usually automatically released in the effect's cleanup phase,\nfor example: "},{"kind":"code","text":"`useVideoPlayer()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-video`"},{"kind":"text","text":" and "},{"kind":"code","text":"`useImage()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.release","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.release","package":"expo-modules-core"}},{"name":"removeAllListeners","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeAllListeners","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes all listeners for the given event name."}]},"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.removeAllListeners","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.removeAllListeners","package":"expo-modules-core"}},{"name":"removeListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.removeListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.removeListener","package":"expo-modules-core"}},{"name":"replace","variant":"declaration","kind":2048,"signatures":[{"name":"replace","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Replaces the current source with a new one.\n\n> On iOS, this method loads the asset data synchronously on the UI thread and can block it for extended periods of time.\n> Use "},{"kind":"code","text":"`replaceAsync`"},{"kind":"text","text":" to load the asset asynchronously and avoid UI lags.\n\n> This method will be deprecated in the future."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"disableWarning","variant":"param","kind":32768,"flags":{"isOptional":true},"type":{"type":"intrinsic","name":"boolean"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"replaceAsync","variant":"declaration","kind":2048,"signatures":[{"name":"replaceAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Replaces the current source with a new one, while offloading loading of the asset to a different thread.\n\n> On Android and Web, this method is equivalent to "},{"kind":"code","text":"`replace`"},{"kind":"text","text":"."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoSource","package":"expo-video"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"replay","variant":"declaration","kind":2048,"signatures":[{"name":"replay","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Seeks the playback to the beginning."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"seekBy","variant":"declaration","kind":2048,"signatures":[{"name":"seekBy","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Seeks the playback by the given number of seconds. The time to which the player seeks may differ from the specified requested time for efficiency,\ndepending on the encoding and what is currently buffered by the player. Use this function to implement playback controls that seek by specific amount of time,\nin which case, the actual time usually does not have to be precise. For frame accurate seeking, use the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property."}]},"parameters":[{"name":"seconds","variant":"param","kind":32768,"type":{"type":"intrinsic","name":"number"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"startObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"startObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the first listener for an event with the given name is added.\nOverride it in a subclass to perform some additional setup once the event started being observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.startObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.startObserving","package":"expo-modules-core"}},{"name":"stopObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"stopObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the last listener for an event with the given name is removed.\nOverride it in a subclass to perform some additional cleanup once the event is no longer observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.stopObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.stopObserving","package":"expo-modules-core"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedObject.ts","qualifiedName":"SharedObject"},"typeArguments":[{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}],"name":"SharedObject","package":"expo-modules-core"}]},{"name":"VideoView","variant":"declaration","kind":128,"children":[{"name":"constructor","variant":"declaration","kind":512,"flags":{"isExternal":true},"signatures":[{"name":"VideoView","variant":"signature","kind":16384,"flags":{"isExternal":true},"parameters":[{"name":"props","variant":"param","kind":32768,"flags":{"isExternal":true},"type":{"type":"reference","name":"VideoViewProps","package":"expo-video"}}],"type":{"type":"reference","name":"VideoView","package":"expo-video"},"inheritedFrom":{"type":"reference","name":"PureComponent.constructor","package":"@types/react"}},{"name":"VideoView","variant":"signature","kind":16384,"flags":{"isExternal":true},"parameters":[{"name":"props","variant":"param","kind":32768,"flags":{"isExternal":true},"type":{"type":"reference","name":"VideoViewProps","package":"expo-video"}},{"name":"context","variant":"param","kind":32768,"flags":{"isExternal":true},"comment":{"summary":[{"kind":"text","text":"value of the parent "},{"kind":"inline-tag","tag":"@link","text":"Context"},{"kind":"text","text":" specified\nin "},{"kind":"code","text":"`contextType`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"any"}}],"type":{"type":"reference","name":"VideoView","package":"expo-video"},"inheritedFrom":{"type":"reference","name":"PureComponent.constructor","package":"@types/react"}}],"inheritedFrom":{"type":"reference","name":"PureComponent.constructor","package":"@types/react"}},{"name":"nativeRef","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"A reference to the underlying native view. On web it is a reference to the HTMLVideoElement."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.RefObject"},"typeArguments":[{"type":"intrinsic","name":"any"}],"name":"RefObject","package":"@types/react","qualifiedName":"React.RefObject"},"defaultValue":"..."},{"name":"enterFullscreen","variant":"declaration","kind":2048,"signatures":[{"name":"enterFullscreen","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Enters fullscreen mode."}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"exitFullscreen","variant":"declaration","kind":2048,"signatures":[{"name":"exitFullscreen","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Exits fullscreen mode."}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"render","variant":"declaration","kind":2048,"signatures":[{"name":"render","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"},"overwrites":{"type":"reference","name":"PureComponent.render","package":"@types/react"}}],"overwrites":{"type":"reference","name":"PureComponent.render","package":"@types/react"}},{"name":"startPictureInPicture","variant":"declaration","kind":2048,"signatures":[{"name":"startPictureInPicture","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Enters Picture in Picture (PiP) mode. Throws an exception if the device does not support PiP.\n> **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n\n> **Note:** The "},{"kind":"code","text":"`supportsPictureInPicture`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be configured for the PiP to work."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"stopPictureInPicture","variant":"declaration","kind":2048,"signatures":[{"name":"stopPictureInPicture","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Exits Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}],"extendedTypes":[{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.PureComponent"},"typeArguments":[{"type":"reference","name":"VideoViewProps","package":"expo-video"}],"name":"PureComponent","package":"@types/react","qualifiedName":"React.PureComponent"}]},{"name":"VideoAirPlayButtonProps","variant":"declaration","kind":256,"children":[{"name":"activeTint","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the button icon while AirPlay sharing is active."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"undefined"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"onBeginPresentingRoutes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback called when the AirPlay route selection popup is about to show."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onEndPresentingRoutes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback called when the AirPlay route selection popup has disappeared."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"prioritizeVideoDevices","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the AirPlay device selection popup should show video outputs first."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"tint","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the button icon while AirPlay sharing is not active."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"undefined"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Omit"},"typeArguments":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/Components/View/ViewPropTypes.d.ts","qualifiedName":"ViewProps"},"name":"ViewProps","package":"react-native"},{"type":"literal","value":"children"}],"name":"Omit","package":"typescript"}]},{"name":"VideoThumbnail","variant":"declaration","kind":256,"comment":{"summary":[{"kind":"text","text":"Represents a video thumbnail that references a native image.\nInstances of this class can be passed as a source to the "},{"kind":"code","text":"`Image`"},{"kind":"text","text":" component from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"actualTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The time in seconds at which the thumbnail was actually generated."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"height","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Height of the created thumbnail."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"nativeRefType","variant":"declaration","kind":1024,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"The type of the native reference."}]},"type":{"type":"intrinsic","name":"string"},"inheritedFrom":{"type":"reference","name":"SharedRef.nativeRefType","package":"expo-modules-core"}},{"name":"requestedTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The time in seconds at which the thumbnail was to be created."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"width","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Width of the created thumbnail."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"addListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"addListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Adds a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"never"},{"type":"intrinsic","name":"never"}],"name":"Record","package":"typescript"}}}],"type":{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/ts-declarations/EventEmitter.ts","qualifiedName":"EventSubscription"},"name":"EventSubscription","package":"expo-modules-core"},"inheritedFrom":{"type":"reference","name":"SharedRef.addListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.addListener","package":"expo-modules-core"}},{"name":"emit","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"emit","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Synchronously calls all the listeners attached to that specific event.\nThe event can include any number of arguments that will be passed to the listeners."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"args","variant":"param","kind":32768,"flags":{"isRest":true},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Parameters"},"typeArguments":[{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"never"},{"type":"intrinsic","name":"never"}],"name":"Record","package":"typescript"}}],"name":"Parameters","package":"typescript"}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.emit","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.emit","package":"expo-modules-core"}},{"name":"listenerCount","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"listenerCount","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Returns a number of listeners added to the given event."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"number"},"inheritedFrom":{"type":"reference","name":"SharedRef.listenerCount","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.listenerCount","package":"expo-modules-core"}},{"name":"release","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"release","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"A function that detaches the JS and native objects to let the native object deallocate\nbefore the JS object gets deallocated by the JS garbage collector. Any subsequent calls to native\nfunctions of the object will throw an error as it is no longer associated with its native counterpart.\n\nIn most cases, you should never need to use this function, except some specific performance-critical cases when\nmanual memory management makes sense and the native object is known to exclusively retain some native memory\n(such as binary data or image bitmap). Before calling this function, you should ensure that nothing else will use\nthis object later on. Shared objects created by React hooks are usually automatically released in the effect's cleanup phase,\nfor example: "},{"kind":"code","text":"`useVideoPlayer()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-video`"},{"kind":"text","text":" and "},{"kind":"code","text":"`useImage()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.release","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.release","package":"expo-modules-core"}},{"name":"removeAllListeners","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeAllListeners","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes all listeners for the given event name."}]},"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"intrinsic","name":"never"}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.removeAllListeners","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.removeAllListeners","package":"expo-modules-core"}},{"name":"removeListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"never"},{"type":"intrinsic","name":"never"}],"name":"Record","package":"typescript"}}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.removeListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.removeListener","package":"expo-modules-core"}},{"name":"startObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"startObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the first listener for an event with the given name is added.\nOverride it in a subclass to perform some additional setup once the event started being observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.startObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.startObserving","package":"expo-modules-core"}},{"name":"stopObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"stopObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the last listener for an event with the given name is removed.\nOverride it in a subclass to perform some additional cleanup once the event is no longer observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.stopObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.stopObserving","package":"expo-modules-core"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedRef.ts","qualifiedName":"SharedRef"},"typeArguments":[{"type":"literal","value":"image"}],"name":"SharedRef","package":"expo-modules-core"}]},{"name":"VideoViewProps","variant":"declaration","kind":256,"children":[{"name":"allowsPictureInPicture","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player allows Picture in Picture (PiP) mode.\n> **Note:** The "},{"kind":"code","text":"`supportsPictureInPicture`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be configured for the PiP to work."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"allowsVideoFrameAnalysis","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies whether to perform video frame analysis (Live Text in videos).\nCheck official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios 16.0+"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"buttonOptions","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Configuration for controlling the visibility of player control buttons."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"reference","name":"ButtonOptions","package":"expo-video"}},{"name":"contentFit","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Describes how the video should be scaled to fit in the container.\nOptions are "},{"kind":"code","text":"`'contain'`"},{"kind":"text","text":", "},{"kind":"code","text":"`'cover'`"},{"kind":"text","text":", and "},{"kind":"code","text":"`'fill'`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'contain'"}]}]},"type":{"type":"reference","name":"VideoContentFit","package":"expo-video"}},{"name":"contentPosition","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the position offset of the video inside the container."}],"blockTags":[{"tag":"@default","content":[]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"dx","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"intrinsic","name":"number"}},{"name":"dy","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"intrinsic","name":"number"}}]}}},{"name":"crossOrigin","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\nIf "},{"kind":"code","text":"`undefined`"},{"kind":"text","text":" (default), does not use CORS at all. If set to "},{"kind":"code","text":"`'anonymous'`"},{"kind":"text","text":", the video will be loaded with CORS enabled.\nNote that some videos may not play if CORS is enabled, depending on the CDN settings.\nIf you encounter issues, consider adjusting the "},{"kind":"code","text":"`crossOrigin`"},{"kind":"text","text":" property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"web"}]},{"tag":"@default","content":[{"kind":"text","text":"undefined"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"anonymous"},{"type":"literal","value":"use-credentials"}]}},{"name":"fullscreenOptions","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the fullscreen mode options."}]},"type":{"type":"reference","target":{"packageName":"expo-video","packagePath":"src/VideoView.types.ts","qualifiedName":"FullscreenOptions"},"name":"FullscreenOptions","package":"expo-video"}},{"name":"nativeControls","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether native controls should be displayed or not.\n\n> **Note**: Due to platform limitations, the native controls are always enabled in fullscreen mode."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"onFirstFrameRender","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the mounted "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" has rendered the first frame into the "},{"kind":"code","text":"`VideoView`"},{"kind":"text","text":".\nThis event can be used to hide any cover images that conceal the initial loading of the player.\n> **Note:** This event may also be called during playback when the current video track changes (for example when the player switches video quality)."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onFullscreenEnter","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player enters fullscreen mode."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onFullscreenExit","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player exits fullscreen mode."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPictureInPictureStart","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player enters Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPictureInPictureStop","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player exits Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"player","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A video player instance. Use ["},{"kind":"code","text":"`useVideoPlayer()`"},{"kind":"text","text":"](#usevideoplayersource-setup) hook to create one."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoPlayer","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"playsInline","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether a video should be played \"inline\", that is, within the element's playback area."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"requiresLinearPlayback","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player allows the user to skip media content."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showsTimecodes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the timecodes should be displayed or not."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"startsPictureInPictureAutomatically","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n> **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n\n> **Note:** The "},{"kind":"code","text":"`supportsPictureInPicture`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be configured for the PiP to work."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android 12+"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"surfaceType","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the type of the surface used to render the video.\n> This prop should not be changed at runtime."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'surfaceView'"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"reference","name":"SurfaceType","package":"expo-video"}},{"name":"useAudioNodePlayback","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Use Audio Nodes for sound playback. When the same player is playing in multiple video views the audio won't increase in volume\nas the number of players increases.\n\n> **Note**: This property is experimental, when enabled it is known to break audio for some sources. Do not change this property at runtime."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}],"modifierTags":["@experimental"]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"useExoShutter","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player should use the default ExoPlayer shutter that covers the "},{"kind":"code","text":"`VideoView`"},{"kind":"text","text":" before the first video frame is rendered.\nSetting this property to "},{"kind":"code","text":"`false`"},{"kind":"text","text":" makes the Android behavior the same as iOS."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/Components/View/ViewPropTypes.d.ts","qualifiedName":"ViewProps"},"name":"ViewProps","package":"react-native"}]},{"name":"AudioMixingMode","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies the audio mode that the player should use. Audio mode is set on per-app basis, if there are multiple players playing and\nhave different a "},{"kind":"code","text":"`AudioMode`"},{"kind":"text","text":" specified, the highest priority mode will be used. Priority order: 'doNotMix' > 'auto' > 'duckOthers' > 'mixWithOthers'.\n\n- "},{"kind":"code","text":"`mixWithOthers`"},{"kind":"text","text":": The player will mix its audio output with other apps.\n- "},{"kind":"code","text":"`duckOthers`"},{"kind":"text","text":": The player will lower the volume of other apps if any of the active players is outputting audio.\n- "},{"kind":"code","text":"`auto`"},{"kind":"text","text":": The player will allow other apps to keep playing audio only when it is muted. On iOS it will always interrupt other apps when "},{"kind":"code","text":"`showNowPlayingNotification`"},{"kind":"text","text":" is "},{"kind":"code","text":"`true`"},{"kind":"text","text":" due to system requirements.\n- "},{"kind":"code","text":"`doNotMix`"},{"kind":"text","text":": The player will pause playback in other apps, even when it's muted.\n\n> On iOS, the Now Playing notification is dependent on the audio mode. If the audio mode is different from "},{"kind":"code","text":"`doNotMix`"},{"kind":"text","text":" or "},{"kind":"code","text":"`auto`"},{"kind":"text","text":" this feature will not work."}]},"type":{"type":"union","types":[{"type":"literal","value":"mixWithOthers"},{"type":"literal","value":"duckOthers"},{"type":"literal","value":"auto"},{"type":"literal","value":"doNotMix"}]}},{"name":"AudioTrack","variant":"declaration","kind":2097152,"children":[{"name":"id","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A string used by expo-video to identify the audio track."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"label","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Label of the audio track in the language of the device."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"language","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Language of the audio track. For example, 'en', 'pl', 'de'."}]},"type":{"type":"intrinsic","name":"string"}}]},{"name":"BufferOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies buffer options which will be used by the player when buffering the video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"maxBufferBytes","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"The maximum number of bytes that the player can buffer from the network.\nWhen 0 the player will automatically decide appropriate buffer size."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"minBufferForPlayback","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Minimum duration of the buffer in seconds required to continue playing after the player has been paused or started buffering.\n\n> This property will be ignored if "},{"kind":"code","text":"`preferredForwardBufferDuration`"},{"kind":"text","text":" is lower."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"2"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"preferredForwardBufferDuration","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"The duration in seconds which determines how much media the player should buffer ahead of the current playback time.\n\nOn iOS when set to "},{"kind":"code","text":"`0`"},{"kind":"text","text":" the player will automatically decide appropriate buffer duration.\n\nEquivalent to ["},{"kind":"code","text":"`AVPlayerItem.preferredForwardBufferDuration`"},{"kind":"text","text":"](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"Android: 20, iOS: 0"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"prioritizeTimeOverSizeThreshold","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"A Boolean value which determines whether the player should prioritize time over size when buffering media."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"waitsToMinimizeStalling","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.\n\nEquivalent to ["},{"kind":"code","text":"`AVPlayer.automaticallyWaitsToMinimizeStalling`"},{"kind":"text","text":"](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ButtonOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Configuration for controlling the visibility of player control buttons.\n\n> The fullscreen button should be controlled with ["},{"kind":"code","text":"`fullscreenOptions.enable`"},{"kind":"text","text":"](#fullscreenoptions)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"children":[{"name":"showBottomBar","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the bottom control bar (containing time, progress bar, and buttons).\nWhen set to "},{"kind":"code","text":"`false`"},{"kind":"text","text":", the entire bottom bar including the progress bar will be hidden.\n\n> **Note**: The bottom bar is always visible in fullscreen mode to allow users to exit fullscreen."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showNext","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the next button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showPlayPause","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the play/pause button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showPrevious","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the previous button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSeekBackward","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the seek backward button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSeekForward","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the seek forward button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSettings","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the settings button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSubtitles","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the subtitles button.\n- "},{"kind":"code","text":"`true`"},{"kind":"text","text":": Button is always visible\n- "},{"kind":"code","text":"`false`"},{"kind":"text","text":": Button is never visible\n- "},{"kind":"code","text":"`undefined`"},{"kind":"text","text":": Button is visible only when subtitles are available (default behavior)"}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"undefined"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"boolean"},{"type":"literal","value":null}]}}]},{"name":"ContentType","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies the content type of the source.\n\n- "},{"kind":"code","text":"`auto`"},{"kind":"text","text":": The player will automatically determine the content type of the video.\n- "},{"kind":"code","text":"`progressive`"},{"kind":"text","text":": The player will use progressive download content type. This is the default "},{"kind":"code","text":"`ContentType`"},{"kind":"text","text":" when the uri does not contain an extension.\n- "},{"kind":"code","text":"`hls`"},{"kind":"text","text":": The player will use HLS content type.\n- "},{"kind":"code","text":"`dash`"},{"kind":"text","text":": The player will use DASH content type (Android-only).\n- "},{"kind":"code","text":"`smoothStreaming`"},{"kind":"text","text":": The player will use SmoothStreaming content type (Android-only)."}],"blockTags":[{"tag":"@default","content":[{"kind":"code","text":"`auto`"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"auto"},{"type":"literal","value":"progressive"},{"type":"literal","value":"hls"},{"type":"literal","value":"dash"},{"type":"literal","value":"smoothStreaming"}]}},{"name":"DRMOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies DRM options which will be used by the player while loading the video."}]},"children":[{"name":"base64CertificateData","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the base64 encoded certificate data for the FairPlay DRM.\nWhen this property is set, the "},{"kind":"code","text":"`certificateUrl`"},{"kind":"text","text":" property is ignored."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"certificateUrl","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the certificate URL for the FairPlay DRM."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"contentId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the content ID of the stream."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"headers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines headers sent to the license server on license requests."}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"string"},{"type":"intrinsic","name":"string"}],"name":"Record","package":"typescript"}},{"name":"licenseServer","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines the license server URL."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"multiKey","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies whether the DRM is a multi-key DRM."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"type","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines which type of DRM to use."}]},"type":{"type":"reference","name":"DRMType","package":"expo-video"}}]},{"name":"DRMType","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies which type of DRM to use:\n- Android supports ClearKey, PlayReady and Widevine.\n- iOS supports FairPlay."}]},"type":{"type":"union","types":[{"type":"literal","value":"clearkey"},{"type":"literal","value":"fairplay"},{"type":"literal","value":"playready"},{"type":"literal","value":"widevine"}]}},{"name":"IsExternalPlaybackActiveChangeEventPayload","variant":"declaration","kind":2097152,"children":[{"name":"isExternalPlaybackActive","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The current external playback status."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"oldIsExternalPlaybackActive","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The previous external playback status."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"MutedChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`mutedChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"muted","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently muted."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"oldMuted","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`isMuted`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"PlaybackRateChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`playbackRateChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"oldPlaybackRate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`playbackRate`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"playbackRate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current playback speed of the player."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"PlayerError","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Contains information about any errors that the player encountered during the playback"}]},"children":[{"name":"message","variant":"declaration","kind":1024,"type":{"type":"intrinsic","name":"string"}}]},{"name":"PlayingChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`playingChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"isPlaying","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently playing."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"oldIsPlaying","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`isPlaying`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ScrubbingModeOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Defines scrubbing mode options used by a ["},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":"](#videoplayer)."}]},"children":[{"name":"allowSkippingMediaCodecFlush","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Sets whether to avoid flushing the decoder (where possible) in scrubbing mode.\nWhen "},{"kind":"code","text":"`true`"},{"kind":"text","text":", avoids flushing the decoder when a new seek starts decoding from a key-frame in compatible content."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"enableDynamicScheduling","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Sets whether ExoPlayer's dynamic scheduling should be enabled in scrubbing mode.\nThis can result in available output buffers being handled more quickly when seeking."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"increaseCodecOperatingRate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the codec operating rate should be increased in scrubbing mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"scrubbingModeEnabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the codec operating rate should be increased in scrubbing mode.\n\nYou should only enable this when the player is receiving a large number of seeks in a short period of time. For less frequent seeks, fine-tuning the ["},{"kind":"code","text":"`SeekTolerance`"},{"kind":"text","text":"](#seektolerance-1) may be sufficient.\n\nOn Android, the player may consume more resources in this mode, so it should only be used for short periods of time in response to user interaction (for example, dragging on a progress bar UI element).\n\nOn Android, when "},{"kind":"code","text":"`scrubbingModeEnabled`"},{"kind":"text","text":" is "},{"kind":"code","text":"`true`"},{"kind":"text","text":", the playback is suppressed. You should set this property back to "},{"kind":"code","text":"`false`"},{"kind":"text","text":" when the user interaction ends to allow the playback to resume.\nFor best results, on iOS you should pause the playback when scrubbing.\n\n> For best scrubbing performance, consider also increasing the seeking tolerance using the ["},{"kind":"code","text":"`SeekTolerance`"},{"kind":"text","text":"](#seektolerance-1) property.\n\n> Other scrubbing mode options will have no effect when this is "},{"kind":"code","text":"`false`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"useDecodeOnlyFlag","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Sets whether to use "},{"kind":"code","text":"`MediaCodec.BUFFER_FLAG_DECODE_ONLY`"},{"kind":"text","text":" in scrubbing mode.\nWhen playback is using MediaCodec on API 34+, this flag can speed up seeking by signalling that the decoded output of buffers between the previous keyframe and the target frame is not needed by the player."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"SeekTolerance","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Determines the time that the actual position seeked to may precede or exceed the requested seek position.\nLarger tolerance will usually result in faster seeking.\nThis property affects the precision of setting the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property and the ["},{"kind":"code","text":"`seekBy`"},{"kind":"text","text":"](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n\n> If you are trying to optimize for scrubbing (many frequent seeks), also see ["},{"kind":"code","text":"`ScrubbingModeOptions`"},{"kind":"text","text":"](#scrubbingmodeoptions-1)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"toleranceAfter","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The maximum time that the actual position seeked to may exceed the requested seek position, in seconds. Must be non-negative."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"toleranceBefore","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The maximum time that the actual position seeked to may precede the requested seek position, in seconds. Must be non-negative."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"SourceChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`sourceChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"oldSource","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous source of the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"source","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New source of the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}}]},{"name":"SourceLoadEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`sourceLoad`"},{"kind":"text","text":"](#videoplayerevents) event, contains information about the video source that has finished loading."}]},"children":[{"name":"availableAudioTracks","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Audio tracks available for the loaded video source."}]},"type":{"type":"array","elementType":{"type":"reference","name":"AudioTrack","package":"expo-video"}}},{"name":"availableSubtitleTracks","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Subtitle tracks available for the loaded video source."}]},"type":{"type":"array","elementType":{"type":"reference","name":"SubtitleTrack","package":"expo-video"}}},{"name":"availableVideoTracks","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Video tracks available for the loaded video source.\n\n> On iOS, when using a HLS source, make sure that the uri contains "},{"kind":"code","text":"`.m3u8`"},{"kind":"text","text":" extension or that the ["},{"kind":"code","text":"`contentType`"},{"kind":"text","text":"](#contenttype) property of the ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource) has been set to "},{"kind":"code","text":"`'hls'`"},{"kind":"text","text":". Otherwise, the video tracks will not be available."}]},"type":{"type":"array","elementType":{"type":"reference","name":"VideoTrack","package":"expo-video"}}},{"name":"duration","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Duration of the video source in seconds."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"videoSource","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The video source that has been loaded."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoSource","package":"expo-video"},{"type":"literal","value":null}]}}]},{"name":"StatusChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`statusChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"error","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Error object containing information about the error that occurred."}]},"type":{"type":"reference","name":"PlayerError","package":"expo-video"}},{"name":"oldStatus","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous status of the player."}]},"type":{"type":"reference","name":"VideoPlayerStatus","package":"expo-video"}},{"name":"status","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New status of the player."}]},"type":{"type":"reference","name":"VideoPlayerStatus","package":"expo-video"}}]},{"name":"SubtitleTrack","variant":"declaration","kind":2097152,"children":[{"name":"id","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A string used by "},{"kind":"code","text":"`expo-video`"},{"kind":"text","text":" to identify the subtitle track."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"label","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Label of the subtitle track in the language of the device."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"language","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Language of the subtitle track. For example, "},{"kind":"code","text":"`en`"},{"kind":"text","text":", "},{"kind":"code","text":"`pl`"},{"kind":"text","text":", "},{"kind":"code","text":"`de`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"string"}}]},{"name":"SubtitleTrackChangeEventPayload","variant":"declaration","kind":2097152,"children":[{"name":"oldSubtitleTrack","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous subtitle track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"SubtitleTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"subtitleTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New subtitle track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"SubtitleTrack","package":"expo-video"},{"type":"literal","value":null}]}}]},{"name":"SurfaceType","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Describes the type of the surface used to render the video.\n- "},{"kind":"code","text":"`surfaceView`"},{"kind":"text","text":": Uses the "},{"kind":"code","text":"`SurfaceView`"},{"kind":"text","text":" to render the video. This value should be used in the majority of cases. Provides significantly lower power consumption, better performance, and more features.\n- "},{"kind":"code","text":"`textureView`"},{"kind":"text","text":": Uses the "},{"kind":"code","text":"`TextureView`"},{"kind":"text","text":" to render the video. Should be used in cases where the SurfaceView is not supported or causes issues (for example, overlapping video views).\n\nYou can learn more about surface types in the official [ExoPlayer documentation](https://developer.android.com/media/media3/ui/playerview#surfacetype)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"textureView"},{"type":"literal","value":"surfaceView"}]}},{"name":"TimeUpdateEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`timeUpdate`"},{"kind":"text","text":"](#videoplayerevents) event, contains information about the current playback progress."}]},"children":[{"name":"bufferedPosition","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating how far the player has buffered the video in seconds.\nSame as the ["},{"kind":"code","text":"`bufferedPosition`"},{"kind":"text","text":"](#bufferedPosition) property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"currentLiveTimestamp","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The exact timestamp when the currently displayed video frame was sent from the server,\nbased on the "},{"kind":"code","text":"`EXT-X-PROGRAM-DATE-TIME`"},{"kind":"text","text":" tag in the livestream metadata.\nSame as the ["},{"kind":"code","text":"`currentLiveTimestamp`"},{"kind":"text","text":"](#currentlivetimestamp) property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentOffsetFromLive","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the latency of the live stream in seconds.\nSame as the ["},{"kind":"code","text":"`currentOffsetFromLive`"},{"kind":"text","text":"](#currentoffsetfromlive) property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current playback time in seconds. Same as the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"VideoContentFit","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Describes how a video should be scaled to fit in a container.\n- "},{"kind":"code","text":"`contain`"},{"kind":"text","text":": The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n- "},{"kind":"code","text":"`cover`"},{"kind":"text","text":": The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n- "},{"kind":"code","text":"`fill`"},{"kind":"text","text":": The video stretches/squeezes to completely fill the container, potentially causing distortion."}]},"type":{"type":"union","types":[{"type":"literal","value":"contain"},{"type":"literal","value":"cover"},{"type":"literal","value":"fill"}]}},{"name":"VideoMetadata","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Contains information that will be displayed in the now playing notification when the video is playing."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"artist","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Secondary text that will be displayed under the title."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"artwork","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The uri of the video artwork."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"title","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The title of the video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}}]},{"name":"VideoPlayerEvents","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Handlers for events which can be emitted by the player."}]},"children":[{"name":"audioTrackChange","variant":"declaration","kind":2048,"signatures":[{"name":"audioTrackChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current audio track changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"expo-video","packagePath":"src/VideoPlayerEvents.types.ts","qualifiedName":"AudioTrackChangeEventPayload"},"name":"AudioTrackChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"availableAudioTracksChange","variant":"declaration","kind":2048,"signatures":[{"name":"availableAudioTracksChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the available audio tracks change."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"expo-video","packagePath":"src/VideoPlayerEvents.types.ts","qualifiedName":"AvailableAudioTracksChangeEventPayload"},"name":"AvailableAudioTracksChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"availableSubtitleTracksChange","variant":"declaration","kind":2048,"signatures":[{"name":"availableSubtitleTracksChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the available subtitle tracks change."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"AvailableSubtitleTracksChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"isExternalPlaybackActiveChange","variant":"declaration","kind":2048,"signatures":[{"name":"isExternalPlaybackActiveChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the video player starts or stops sharing the video via AirPlay."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"IsExternalPlaybackActiveChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"mutedChange","variant":"declaration","kind":2048,"signatures":[{"name":"mutedChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the "},{"kind":"code","text":"`muted`"},{"kind":"text","text":" property of the player changes"}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"MutedChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"playbackRateChange","variant":"declaration","kind":2048,"signatures":[{"name":"playbackRateChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the "},{"kind":"code","text":"`playbackRate`"},{"kind":"text","text":" property of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"PlaybackRateChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"playingChange","variant":"declaration","kind":2048,"signatures":[{"name":"playingChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the player starts or stops playback."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"PlayingChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"playToEnd","variant":"declaration","kind":2048,"signatures":[{"name":"playToEnd","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the player plays to the end of the current source."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"sourceChange","variant":"declaration","kind":2048,"signatures":[{"name":"sourceChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current media source of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"SourceChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"sourceLoad","variant":"declaration","kind":2048,"signatures":[{"name":"sourceLoad","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the player has finished loading metadata for the current video source.\nThis event is emitted when the player has finished metadata for a ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource), but it doesn't mean that there is enough data buffered to start the playback."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"SourceLoadEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"statusChange","variant":"declaration","kind":2048,"signatures":[{"name":"statusChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the status of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"StatusChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"subtitleTrackChange","variant":"declaration","kind":2048,"signatures":[{"name":"subtitleTrackChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current subtitle track changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"SubtitleTrackChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"timeUpdate","variant":"declaration","kind":2048,"signatures":[{"name":"timeUpdate","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted in a given interval specified by the "},{"kind":"code","text":"`timeUpdateEventInterval`"},{"kind":"text","text":"."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"TimeUpdateEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"videoTrackChange","variant":"declaration","kind":2048,"signatures":[{"name":"videoTrackChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current video track changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoTrackChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"volumeChange","variant":"declaration","kind":2048,"signatures":[{"name":"volumeChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the "},{"kind":"code","text":"`volume`"},{"kind":"text","text":" of "},{"kind":"code","text":"`muted`"},{"kind":"text","text":" property of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"VolumeChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]}]},{"name":"VideoPlayerStatus","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Describes the current status of the player.\n- "},{"kind":"code","text":"`idle`"},{"kind":"text","text":": The player is not playing or loading any videos.\n- "},{"kind":"code","text":"`loading`"},{"kind":"text","text":": The player is loading video data from the provided source\n- "},{"kind":"code","text":"`readyToPlay`"},{"kind":"text","text":": The player has loaded enough data to start playing or to continue playback.\n- "},{"kind":"code","text":"`error`"},{"kind":"text","text":": The player has encountered an error while loading or playing the video."}]},"type":{"type":"union","types":[{"type":"literal","value":"idle"},{"type":"literal","value":"loading"},{"type":"literal","value":"readyToPlay"},{"type":"literal","value":"error"}]}},{"name":"VideoSize","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies the size of a video track."}]},"children":[{"name":"height","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Height of the video track in pixels."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"width","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Width of the video track in pixels."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"VideoSource","variant":"declaration","kind":2097152,"type":{"type":"union","types":[{"type":"intrinsic","name":"string"},{"type":"intrinsic","name":"number"},{"type":"literal","value":null},{"type":"reference","name":"VideoSourceObject","package":"expo-video"}]}},{"name":"VideoSourceObject","variant":"declaration","kind":2097152,"children":[{"name":"assetId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The asset ID of a local video asset, acquired with the "},{"kind":"code","text":"`require`"},{"kind":"text","text":" function.\nThis property is exclusive with the "},{"kind":"code","text":"`uri`"},{"kind":"text","text":" property. When both are present, the "},{"kind":"code","text":"`assetId`"},{"kind":"text","text":" will be ignored."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"contentType","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the content type of the video source. When set to "},{"kind":"code","text":"`'auto'`"},{"kind":"text","text":", the player will try to automatically determine the content type.\n\nYou should use this property when playing HLS, SmoothStreaming or DASH videos from an uri, which does not contain a standardized extension for the corresponding media type."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'auto'"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"ContentType","package":"expo-video"}},{"name":"drm","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the DRM options which will be used by the player while loading the video."}]},"type":{"type":"reference","name":"DRMOptions","package":"expo-video"}},{"name":"headers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies headers sent with the video request.\n> For DRM license headers use the "},{"kind":"code","text":"`headers`"},{"kind":"text","text":" field of ["},{"kind":"code","text":"`DRMOptions`"},{"kind":"text","text":"](#drmoptions)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"string"},{"type":"intrinsic","name":"string"}],"name":"Record","package":"typescript"}},{"name":"metadata","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies information which will be displayed in the now playing notification.\nWhen undefined the player will display information contained in the video metadata."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"VideoMetadata","package":"expo-video"}},{"name":"uri","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The URI of the video.\n\nOn iOS, "},{"kind":"code","text":"`PHAsset`"},{"kind":"text","text":" URIs are supported, but can only be loaded using the ["},{"kind":"code","text":"`replaceAsync`"},{"kind":"text","text":"](#replaceasyncsource) method or the default ["},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":"](#videoplayer) constructor.\n\nThis property is exclusive with the "},{"kind":"code","text":"`assetId`"},{"kind":"text","text":" property. When both are present, the "},{"kind":"code","text":"`assetId`"},{"kind":"text","text":" will be ignored."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"useCaching","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies whether the player should use caching for the video.\n> Due to platform limitations, the cache cannot be used with HLS video sources on iOS. Caching DRM-protected videos is not supported on Android and iOS."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"VideoThumbnailOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Additional options for video thumbnails generation."}]},"children":[{"name":"maxHeight","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"If provided, the generated thumbnail will not exceed this height in pixels, preserving its aspect ratio."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"maxWidth","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"If provided, the generated thumbnail will not exceed this width in pixels, preserving its aspect ratio."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"VideoTrack","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies a VideoTrack loaded from a ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource)."}]},"children":[{"name":"averageBitrate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the average bitrate in bits per second or null if the value is unknown."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"bitrate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate if known, or else null."}],"blockTags":[{"tag":"@deprecated","content":[{"kind":"text","text":"Use "},{"kind":"code","text":"`peakBitrate`"},{"kind":"text","text":" or "},{"kind":"code","text":"`averageBitrate`"},{"kind":"text","text":" instead."}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"frameRate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the frame rate of the video track in frames per second."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"id","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The id of the video track.\n\n> This field is platform-specific and may return different depending on the operating system."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"isSupported","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Indicates whether the video track format is supported by the device."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"mimeType","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"MimeType of the video track or null if unknown."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"string"},{"type":"literal","value":null}]}},{"name":"peakBitrate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the average bitrate in bits per second or null if the value is unknown."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"size","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Size of the video track."}]},"type":{"type":"reference","name":"VideoSize","package":"expo-video"}}]},{"name":"VideoTrackChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`videoTrackChange`"},{"kind":"text","text":"](#videoplayerevents) event, contains information about the video track which is currently being played."}]},"children":[{"name":"oldVideoTrack","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous video track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"videoTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New video track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoTrack","package":"expo-video"},{"type":"literal","value":null}]}}]},{"name":"VolumeChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`volumeChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"oldVolume","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`volume`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"volume","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current volume of the player."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"clearVideoCacheAsync","variant":"declaration","kind":64,"signatures":[{"name":"clearVideoCacheAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Clears all video cache.\n> This function can be called only if there are no existing "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" instances."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that fulfills after the cache has been cleaned."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"createVideoPlayer","variant":"declaration","kind":64,"signatures":[{"name":"createVideoPlayer","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates a direct instance of "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" that doesn't release automatically.\n\n> **info** For most use cases you should use the ["},{"kind":"code","text":"`useVideoPlayer`"},{"kind":"text","text":"](#usevideoplayer) hook instead. See the [Using the VideoPlayer Directly](#using-the-videoplayer-directly) section for more details."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoSource","package":"expo-video"}}],"type":{"type":"reference","name":"VideoPlayer","package":"expo-video"}}]},{"name":"getCurrentVideoCacheSize","variant":"declaration","kind":64,"signatures":[{"name":"getCurrentVideoCacheSize","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Returns the space currently occupied by the video cache in bytes."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"isPictureInPictureSupported","variant":"declaration","kind":64,"signatures":[{"name":"isPictureInPictureSupported","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Returns whether the current device supports Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A "},{"kind":"code","text":"`boolean`"},{"kind":"text","text":" which is "},{"kind":"code","text":"`true`"},{"kind":"text","text":" if the device supports PiP mode, and "},{"kind":"code","text":"`false`"},{"kind":"text","text":" otherwise."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"setVideoCacheSizeAsync","variant":"declaration","kind":64,"signatures":[{"name":"setVideoCacheSizeAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Sets desired video cache size in bytes. The default video cache size is 1GB. Value set by this function is persistent.\nThe cache size is not guaranteed to be exact and the actual cache size may be slightly larger. The cache is evicted on a least-recently-used basis.\n> This function can be called only if there are no existing "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" instances."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that fulfills after the cache size has been set."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"sizeBytes","variant":"param","kind":32768,"type":{"type":"intrinsic","name":"number"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"useVideoPlayer","variant":"declaration","kind":64,"signatures":[{"name":"useVideoPlayer","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates a "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":", which will be automatically cleaned up when the component is unmounted."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"comment":{"summary":[{"kind":"text","text":"A video source that is used to initialize the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"setup","variant":"param","kind":32768,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A function that allows setting up the player. It will run after the player is created."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"parameters":[{"name":"player","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoPlayer","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]}}}],"type":{"type":"reference","name":"VideoPlayer","package":"expo-video"}}]},{"name":"VideoAirPlayButton","variant":"declaration","kind":64,"signatures":[{"name":"VideoAirPlayButton","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A view displaying the ["},{"kind":"code","text":"`AVRoutePickerView`"},{"kind":"text","text":"](https://developer.apple.com/documentation/avkit/avroutepickerview). Shows a button, when pressed, an AirPlay device picker shows up, allowing users to stream the currently playing video\nto any available AirPlay sink.\n\n> When using this view, make sure that the ["},{"kind":"code","text":"`allowsExternalPlayback`"},{"kind":"text","text":"](#allowsexternalplayback) player property is set to "},{"kind":"code","text":"`true`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoAirPlayButtonProps","package":"expo-video"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"React.JSX.Element"}}]}],"packageName":"expo-video"}
\ No newline at end of file
+{"schemaVersion":"2.0","name":"expo-video","variant":"project","kind":1,"children":[{"name":"VideoPlayer","variant":"declaration","kind":128,"comment":{"summary":[{"kind":"text","text":"A class that represents an instance of the video player."}]},"children":[{"name":"allowsExternalPlayback","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the player should allow external playback."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"audioMixingMode","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines how the player will interact with other audio playing in the system."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'auto'"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"AudioMixingMode","package":"expo-video"}},{"name":"audioTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the audio track currently played by the player. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" when no audio is played."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"null"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"reference","name":"AudioTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"availableAudioTracks","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"An array of audio tracks available for the current video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","name":"AudioTrack","package":"expo-video"}}},{"name":"availableSubtitleTracks","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"An array of subtitle tracks available for the current video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","name":"SubtitleTrack","package":"expo-video"}}},{"name":"availableVideoTracks","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"An array of video tracks available for the current video.\n\n> On iOS, when using a HLS source, make sure that the uri contains "},{"kind":"code","text":"`.m3u8`"},{"kind":"text","text":" extension or that the ["},{"kind":"code","text":"`contentType`"},{"kind":"text","text":"](#contenttype) property of the ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource) has been set to "},{"kind":"code","text":"`'hls'`"},{"kind":"text","text":". Otherwise, the video tracks will not be available."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"reference","name":"VideoTrack","package":"expo-video"}}},{"name":"bufferedPosition","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Float value indicating how far the player has buffered the video in seconds.\n\nThis value is 0 when the player has not buffered up to the current playback time.\nWhen it's impossible to determine the buffer state (for example, when the player isn't playing any media), this value is -1."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"bufferOptions","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies buffer options which will be used by the player when buffering the video.\n\n> You should provide a "},{"kind":"code","text":"`BufferOptions`"},{"kind":"text","text":" object when setting this property. Setting individual buffer properties is not supported."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"BufferOptions","package":"expo-video"}},{"name":"currentLiveTimestamp","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"The exact timestamp when the currently displayed video frame was sent from the server,\nbased on the "},{"kind":"code","text":"`EXT-X-PROGRAM-DATE-TIME`"},{"kind":"text","text":" tag in the livestream metadata.\nIf this metadata is missing, this property will return "},{"kind":"code","text":"`null`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentOffsetFromLive","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Float value indicating the latency of the live stream in seconds.\nIf a livestream doesn't have the required metadata, this will return "},{"kind":"code","text":"`null`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current playback time in seconds.\n\nIf the player is not yet playing, this value indicates the time position\nat which playback will begin once the "},{"kind":"code","text":"`play()`"},{"kind":"text","text":" method is called.\n\nSetting "},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":" to a new value seeks the player to the given time.\nCheck out the ["},{"kind":"code","text":"`seekTolerance`"},{"kind":"text","text":"](#seektolerance) property to configure the seeking precision."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"duration","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Float value indicating the duration of the current video in seconds."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"isExternalPlaybackActive","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Indicates whether the player is currently playing back the media to an external device via AirPlay."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"isLive","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Boolean value indicating whether the player is currently playing a live stream."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"keepScreenOnWhilePlaying","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean indicating if the player should keep the screen on while playing.\n\n> On Android, this property has an effect only when a ["},{"kind":"code","text":"`VideoView`"},{"kind":"text","text":"](#videoview) is visible. If you want to keep the screen awake at all times use ["},{"kind":"code","text":"`expo-keep-awake`"},{"kind":"text","text":"]("},{"kind":"relative-link","text":"./keep-awake/"},{"kind":"text","text":")."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"loop","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the player should automatically replay after reaching the end of the video."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"muted","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently muted.\nSetting this property to "},{"kind":"code","text":"`true`"},{"kind":"text","text":"/"},{"kind":"code","text":"`false`"},{"kind":"text","text":" will mute/unmute the player."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"playbackRate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value between "},{"kind":"code","text":"`0`"},{"kind":"text","text":" and "},{"kind":"code","text":"`16.0`"},{"kind":"text","text":" indicating the current playback speed of the player."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"1.0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"playing","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently playing.\n> Use "},{"kind":"code","text":"`play`"},{"kind":"text","text":" and "},{"kind":"code","text":"`pause`"},{"kind":"text","text":" methods to control the playback."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"preservesPitch","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value indicating if the player should correct audio pitch when the playback speed changes."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"scrubbingModeOptions","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the scrubbing mode is enabled and what scrubbing optimizations should be enabled.\n\n> See ["},{"kind":"code","text":"`SeekTolerance`"},{"kind":"text","text":"](#seektolerance) to set the seeking tolerance, which can also affect the scrubbing performance."}]},"type":{"type":"reference","name":"ScrubbingModeOptions","package":"expo-video"}},{"name":"seekTolerance","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n\nThis property affects the precision of setting the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property and the ["},{"kind":"code","text":"`seekBy`"},{"kind":"text","text":"](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n\nBy default, the player seeks to the exact requested time.\n\n> If you are trying to optimize for scrubbing (many frequent seeks), also see ["},{"kind":"code","text":"`ScrubbingModeOptions`"},{"kind":"text","text":"](#scrubbingmodeoptions-1)."}]},"type":{"type":"reference","name":"SeekTolerance","package":"expo-video"}},{"name":"showNowPlayingNotification","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value determining whether the player should show the now playing notification.\n\n> **Note**: On Android, "},{"kind":"code","text":"`supportsBackgroundPlayback`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be "},{"kind":"code","text":"`true`"},{"kind":"text","text":" for the now playing notification to work."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"status","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Indicates the current status of the player."}]},"type":{"type":"reference","name":"VideoPlayerStatus","package":"expo-video"}},{"name":"staysActiveInBackground","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines whether the player should continue playing after the app enters the background.\n\n> **Note**: The "},{"kind":"code","text":"`supportsBackgroundPlayback`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be "},{"kind":"code","text":"`true`"},{"kind":"text","text":" for the background playback to work."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"subtitleTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the subtitle track which is currently displayed by the player. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" when no subtitles are displayed.\n\n> To ensure a valid subtitle track, always assign one of the subtitle tracks from the ["},{"kind":"code","text":"`availableSubtitleTracks`"},{"kind":"text","text":"](#availablesubtitletracks) array."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"null"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"reference","name":"SubtitleTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"targetOffsetFromLive","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the time offset from the live in seconds."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"timeUpdateEventInterval","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the interval in seconds at which the player will emit the ["},{"kind":"code","text":"`timeUpdate`"},{"kind":"text","text":"](#videoplayerevents) event.\nWhen the value is equal to "},{"kind":"code","text":"`0`"},{"kind":"text","text":", the event will not be emitted."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"videoTrack","variant":"declaration","kind":1024,"flags":{"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Specifies the video track currently played by the player. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" when no video is displayed."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"null"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"volume","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value between "},{"kind":"code","text":"`0`"},{"kind":"text","text":" and "},{"kind":"code","text":"`1.0`"},{"kind":"text","text":" representing the current volume.\nMuting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\nwhen unmuted. Similarly, setting the volume doesn't unmute the player."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"1.0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"addListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"addListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Adds a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"type":{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/ts-declarations/EventEmitter.ts","qualifiedName":"EventSubscription"},"name":"EventSubscription","package":"expo-modules-core"},"inheritedFrom":{"type":"reference","name":"SharedObject.addListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.addListener","package":"expo-modules-core"}},{"name":"emit","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"emit","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Synchronously calls all the listeners attached to that specific event.\nThe event can include any number of arguments that will be passed to the listeners."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"args","variant":"param","kind":32768,"flags":{"isRest":true},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Parameters"},"typeArguments":[{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}],"name":"Parameters","package":"typescript"}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.emit","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.emit","package":"expo-modules-core"}},{"name":"generateThumbnailsAsync","variant":"declaration","kind":2048,"signatures":[{"name":"generateThumbnailsAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Generates thumbnails from the currently played asset. The thumbnails are references to native images,\nthus they can be used as a source of the "},{"kind":"code","text":"`Image`"},{"kind":"text","text":" component from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"times","variant":"param","kind":32768,"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"array","elementType":{"type":"intrinsic","name":"number"}}]}},{"name":"options","variant":"param","kind":32768,"flags":{"isOptional":true},"type":{"type":"reference","name":"VideoThumbnailOptions","package":"expo-video"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"array","elementType":{"type":"reference","name":"VideoThumbnail","package":"expo-video"}}],"name":"Promise","package":"typescript"}}]},{"name":"listenerCount","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"listenerCount","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Returns a number of listeners added to the given event."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"number"},"inheritedFrom":{"type":"reference","name":"SharedObject.listenerCount","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.listenerCount","package":"expo-modules-core"}},{"name":"pause","variant":"declaration","kind":2048,"signatures":[{"name":"pause","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Pauses the player."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"play","variant":"declaration","kind":2048,"signatures":[{"name":"play","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Resumes the player."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"release","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"release","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"A function that detaches the JS and native objects to let the native object deallocate\nbefore the JS object gets deallocated by the JS garbage collector. Any subsequent calls to native\nfunctions of the object will throw an error as it is no longer associated with its native counterpart.\n\nIn most cases, you should never need to use this function, except some specific performance-critical cases when\nmanual memory management makes sense and the native object is known to exclusively retain some native memory\n(such as binary data or image bitmap). Before calling this function, you should ensure that nothing else will use\nthis object later on. Shared objects created by React hooks are usually automatically released in the effect's cleanup phase,\nfor example: "},{"kind":"code","text":"`useVideoPlayer()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-video`"},{"kind":"text","text":" and "},{"kind":"code","text":"`useImage()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.release","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.release","package":"expo-modules-core"}},{"name":"removeAllListeners","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeAllListeners","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes all listeners for the given event name."}]},"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.removeAllListeners","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.removeAllListeners","package":"expo-modules-core"}},{"name":"removeListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.removeListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.removeListener","package":"expo-modules-core"}},{"name":"replace","variant":"declaration","kind":2048,"signatures":[{"name":"replace","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Replaces the current source with a new one.\n\n> On iOS, this method loads the asset data synchronously on the UI thread and can block it for extended periods of time.\n> Use "},{"kind":"code","text":"`replaceAsync`"},{"kind":"text","text":" to load the asset asynchronously and avoid UI lags.\n\n> This method will be deprecated in the future."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"disableWarning","variant":"param","kind":32768,"flags":{"isOptional":true},"type":{"type":"intrinsic","name":"boolean"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"replaceAsync","variant":"declaration","kind":2048,"signatures":[{"name":"replaceAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Replaces the current source with a new one, while offloading loading of the asset to a different thread.\n\n> On Android and Web, this method is equivalent to "},{"kind":"code","text":"`replace`"},{"kind":"text","text":"."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoSource","package":"expo-video"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"replay","variant":"declaration","kind":2048,"signatures":[{"name":"replay","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Seeks the playback to the beginning."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"seekBy","variant":"declaration","kind":2048,"signatures":[{"name":"seekBy","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Seeks the playback by the given number of seconds. The time to which the player seeks may differ from the specified requested time for efficiency,\ndepending on the encoding and what is currently buffered by the player. Use this function to implement playback controls that seek by specific amount of time,\nin which case, the actual time usually does not have to be precise. For frame accurate seeking, use the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property."}]},"parameters":[{"name":"seconds","variant":"param","kind":32768,"type":{"type":"intrinsic","name":"number"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"startObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"startObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the first listener for an event with the given name is added.\nOverride it in a subclass to perform some additional setup once the event started being observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.startObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.startObserving","package":"expo-modules-core"}},{"name":"stopObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"stopObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the last listener for an event with the given name is removed.\nOverride it in a subclass to perform some additional cleanup once the event is no longer observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"typeOperator","operator":"keyof","target":{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedObject.stopObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedObject.stopObserving","package":"expo-modules-core"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedObject.ts","qualifiedName":"SharedObject"},"typeArguments":[{"type":"reference","name":"VideoPlayerEvents","package":"expo-video"}],"name":"SharedObject","package":"expo-modules-core"}]},{"name":"VideoView","variant":"declaration","kind":128,"children":[{"name":"constructor","variant":"declaration","kind":512,"flags":{"isExternal":true},"signatures":[{"name":"VideoView","variant":"signature","kind":16384,"flags":{"isExternal":true},"parameters":[{"name":"props","variant":"param","kind":32768,"flags":{"isExternal":true},"type":{"type":"reference","name":"VideoViewProps","package":"expo-video"}}],"type":{"type":"reference","name":"VideoView","package":"expo-video"},"inheritedFrom":{"type":"reference","name":"PureComponent.constructor","package":"@types/react"}},{"name":"VideoView","variant":"signature","kind":16384,"flags":{"isExternal":true},"parameters":[{"name":"props","variant":"param","kind":32768,"flags":{"isExternal":true},"type":{"type":"reference","name":"VideoViewProps","package":"expo-video"}},{"name":"context","variant":"param","kind":32768,"flags":{"isExternal":true},"comment":{"summary":[{"kind":"text","text":"value of the parent "},{"kind":"inline-tag","tag":"@link","text":"Context"},{"kind":"text","text":" specified\nin "},{"kind":"code","text":"`contextType`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"any"}}],"type":{"type":"reference","name":"VideoView","package":"expo-video"},"inheritedFrom":{"type":"reference","name":"PureComponent.constructor","package":"@types/react"}}],"inheritedFrom":{"type":"reference","name":"PureComponent.constructor","package":"@types/react"}},{"name":"nativeRef","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"A reference to the underlying native view. On web it is a reference to the HTMLVideoElement."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.RefObject"},"typeArguments":[{"type":"intrinsic","name":"any"}],"name":"RefObject","package":"@types/react","qualifiedName":"React.RefObject"},"defaultValue":"..."},{"name":"enterFullscreen","variant":"declaration","kind":2048,"signatures":[{"name":"enterFullscreen","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Enters fullscreen mode."}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"exitFullscreen","variant":"declaration","kind":2048,"signatures":[{"name":"exitFullscreen","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Exits fullscreen mode."}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"render","variant":"declaration","kind":2048,"signatures":[{"name":"render","variant":"signature","kind":4096,"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"ReactNode","package":"@types/react","qualifiedName":"React.ReactNode"},"overwrites":{"type":"reference","name":"PureComponent.render","package":"@types/react"}}],"overwrites":{"type":"reference","name":"PureComponent.render","package":"@types/react"}},{"name":"startPictureInPicture","variant":"declaration","kind":2048,"signatures":[{"name":"startPictureInPicture","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Enters Picture in Picture (PiP) mode. Throws an exception if the device does not support PiP.\n> **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n\n> **Note:** The "},{"kind":"code","text":"`supportsPictureInPicture`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be configured for the PiP to work."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"stopPictureInPicture","variant":"declaration","kind":2048,"signatures":[{"name":"stopPictureInPicture","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Exits Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]}],"extendedTypes":[{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.PureComponent"},"typeArguments":[{"type":"reference","name":"VideoViewProps","package":"expo-video"}],"name":"PureComponent","package":"@types/react","qualifiedName":"React.PureComponent"}]},{"name":"VideoAirPlayButtonProps","variant":"declaration","kind":256,"children":[{"name":"activeTint","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the button icon while AirPlay sharing is active."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"undefined"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"onBeginPresentingRoutes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback called when the AirPlay route selection popup is about to show."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onEndPresentingRoutes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback called when the AirPlay route selection popup has disappeared."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"prioritizeVideoDevices","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the AirPlay device selection popup should show video outputs first."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"tint","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The color of the button icon while AirPlay sharing is not active."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"undefined"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Omit"},"typeArguments":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/Components/View/ViewPropTypes.d.ts","qualifiedName":"ViewProps"},"name":"ViewProps","package":"react-native"},{"type":"literal","value":"children"}],"name":"Omit","package":"typescript"}]},{"name":"VideoThumbnail","variant":"declaration","kind":256,"comment":{"summary":[{"kind":"text","text":"Represents a video thumbnail that references a native image.\nInstances of this class can be passed as a source to the "},{"kind":"code","text":"`Image`"},{"kind":"text","text":" component from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"actualTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The time in seconds at which the thumbnail was actually generated."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"height","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Height of the created thumbnail."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"nativeRefType","variant":"declaration","kind":1024,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"The type of the native reference."}]},"type":{"type":"intrinsic","name":"string"},"inheritedFrom":{"type":"reference","name":"SharedRef.nativeRefType","package":"expo-modules-core"}},{"name":"requestedTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The time in seconds at which the thumbnail was to be created."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"width","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Width of the created thumbnail."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"addListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"addListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Adds a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"never"},{"type":"intrinsic","name":"never"}],"name":"Record","package":"typescript"}}}],"type":{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/ts-declarations/EventEmitter.ts","qualifiedName":"EventSubscription"},"name":"EventSubscription","package":"expo-modules-core"},"inheritedFrom":{"type":"reference","name":"SharedRef.addListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.addListener","package":"expo-modules-core"}},{"name":"emit","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"emit","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Synchronously calls all the listeners attached to that specific event.\nThe event can include any number of arguments that will be passed to the listeners."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"args","variant":"param","kind":32768,"flags":{"isRest":true},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Parameters"},"typeArguments":[{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"never"},{"type":"intrinsic","name":"never"}],"name":"Record","package":"typescript"}}],"name":"Parameters","package":"typescript"}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.emit","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.emit","package":"expo-modules-core"}},{"name":"listenerCount","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"listenerCount","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Returns a number of listeners added to the given event."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"number"},"inheritedFrom":{"type":"reference","name":"SharedRef.listenerCount","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.listenerCount","package":"expo-modules-core"}},{"name":"release","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"release","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"A function that detaches the JS and native objects to let the native object deallocate\nbefore the JS object gets deallocated by the JS garbage collector. Any subsequent calls to native\nfunctions of the object will throw an error as it is no longer associated with its native counterpart.\n\nIn most cases, you should never need to use this function, except some specific performance-critical cases when\nmanual memory management makes sense and the native object is known to exclusively retain some native memory\n(such as binary data or image bitmap). Before calling this function, you should ensure that nothing else will use\nthis object later on. Shared objects created by React hooks are usually automatically released in the effect's cleanup phase,\nfor example: "},{"kind":"code","text":"`useVideoPlayer()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-video`"},{"kind":"text","text":" and "},{"kind":"code","text":"`useImage()`"},{"kind":"text","text":" from "},{"kind":"code","text":"`expo-image`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.release","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.release","package":"expo-modules-core"}},{"name":"removeAllListeners","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeAllListeners","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes all listeners for the given event name."}]},"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"intrinsic","name":"never"}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.removeAllListeners","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.removeAllListeners","package":"expo-modules-core"}},{"name":"removeListener","variant":"declaration","kind":2048,"flags":{"isInherited":true},"signatures":[{"name":"removeListener","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Removes a listener for the given event name."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}},{"name":"listener","variant":"param","kind":32768,"type":{"type":"indexedAccess","indexType":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true},"objectType":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"never"},{"type":"intrinsic","name":"never"}],"name":"Record","package":"typescript"}}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.removeListener","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.removeListener","package":"expo-modules-core"}},{"name":"startObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"startObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the first listener for an event with the given name is added.\nOverride it in a subclass to perform some additional setup once the event started being observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.startObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.startObserving","package":"expo-modules-core"}},{"name":"stopObserving","variant":"declaration","kind":2048,"flags":{"isOptional":true,"isInherited":true},"signatures":[{"name":"stopObserving","variant":"signature","kind":4096,"flags":{"isInherited":true},"comment":{"summary":[{"kind":"text","text":"Function that is automatically invoked when the last listener for an event with the given name is removed.\nOverride it in a subclass to perform some additional cleanup once the event is no longer observed."}]},"typeParameters":[{"name":"EventName","variant":"typeParam","kind":131072,"type":{"type":"intrinsic","name":"never"}}],"parameters":[{"name":"eventName","variant":"param","kind":32768,"type":{"type":"reference","name":"EventName","package":"expo-modules-core","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"},"inheritedFrom":{"type":"reference","name":"SharedRef.stopObserving","package":"expo-modules-core"}}],"inheritedFrom":{"type":"reference","name":"SharedRef.stopObserving","package":"expo-modules-core"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedRef.ts","qualifiedName":"SharedRef"},"typeArguments":[{"type":"literal","value":"image"}],"name":"SharedRef","package":"expo-modules-core"}]},{"name":"VideoViewProps","variant":"declaration","kind":256,"children":[{"name":"allowsPictureInPicture","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player allows Picture in Picture (PiP) mode.\n> **Note:** The "},{"kind":"code","text":"`supportsPictureInPicture`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be configured for the PiP to work."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"allowsVideoFrameAnalysis","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies whether to perform video frame analysis (Live Text in videos).\nCheck official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios 16.0+"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"buttonOptions","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Configuration for controlling the visibility of player control buttons."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"reference","name":"ButtonOptions","package":"expo-video"}},{"name":"contentFit","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Describes how the video should be scaled to fit in the container.\nOptions are "},{"kind":"code","text":"`'contain'`"},{"kind":"text","text":", "},{"kind":"code","text":"`'cover'`"},{"kind":"text","text":", and "},{"kind":"code","text":"`'fill'`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'contain'"}]}]},"type":{"type":"reference","name":"VideoContentFit","package":"expo-video"}},{"name":"contentPosition","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the position offset of the video inside the container."}],"blockTags":[{"tag":"@default","content":[]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"dx","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"intrinsic","name":"number"}},{"name":"dy","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"intrinsic","name":"number"}}]}}},{"name":"crossOrigin","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\nIf "},{"kind":"code","text":"`undefined`"},{"kind":"text","text":" (default), does not use CORS at all. If set to "},{"kind":"code","text":"`'anonymous'`"},{"kind":"text","text":", the video will be loaded with CORS enabled.\nNote that some videos may not play if CORS is enabled, depending on the CDN settings.\nIf you encounter issues, consider adjusting the "},{"kind":"code","text":"`crossOrigin`"},{"kind":"text","text":" property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"web"}]},{"tag":"@default","content":[{"kind":"text","text":"undefined"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"anonymous"},{"type":"literal","value":"use-credentials"}]}},{"name":"fullscreenOptions","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the fullscreen mode options."}]},"type":{"type":"reference","target":{"packageName":"expo-video","packagePath":"src/VideoView.types.ts","qualifiedName":"FullscreenOptions"},"name":"FullscreenOptions","package":"expo-video"}},{"name":"nativeControls","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether native controls should be displayed or not.\n\n> **Note**: Due to platform limitations, the native controls are always enabled in fullscreen mode."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"onFirstFrameRender","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the mounted "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" has rendered the first frame into the "},{"kind":"code","text":"`VideoView`"},{"kind":"text","text":".\nThis event can be used to hide any cover images that conceal the initial loading of the player.\n> **Note:** This event may also be called during playback when the current video track changes (for example when the player switches video quality)."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onFullscreenEnter","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player enters fullscreen mode."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onFullscreenExit","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player exits fullscreen mode."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPictureInPictureStart","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player enters Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"onPictureInPictureStop","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A callback to call after the video player exits Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"player","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A video player instance. Use ["},{"kind":"code","text":"`useVideoPlayer()`"},{"kind":"text","text":"](#usevideoplayersource-setup) hook to create one."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoPlayer","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"playsInline","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether a video should be played \"inline\", that is, within the element's playback area."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"web"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"requiresLinearPlayback","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player allows the user to skip media content."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showsTimecodes","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the timecodes should be displayed or not."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"startsPictureInPictureAutomatically","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n> **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n\n> **Note:** The "},{"kind":"code","text":"`supportsPictureInPicture`"},{"kind":"text","text":" property of the [config plugin](#configuration-in-app-config)\n> has to be configured for the PiP to work."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android 12+"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"surfaceType","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines the type of the surface used to render the video.\n> This prop should not be changed at runtime."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'surfaceView'"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"reference","name":"SurfaceType","package":"expo-video"}},{"name":"useAudioNodePlayback","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Use Audio Nodes for sound playback. When the same player is playing in multiple video views the audio won't increase in volume\nas the number of players increases.\n\n> **Note**: This property is experimental, when enabled it is known to break audio for some sources. Do not change this property at runtime."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"web"}]}],"modifierTags":["@experimental"]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"useExoShutter","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines whether the player should use the default ExoPlayer shutter that covers the "},{"kind":"code","text":"`VideoView`"},{"kind":"text","text":" before the first video frame is rendered.\nSetting this property to "},{"kind":"code","text":"`false`"},{"kind":"text","text":" makes the Android behavior the same as iOS."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}],"extendedTypes":[{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/Components/View/ViewPropTypes.d.ts","qualifiedName":"ViewProps"},"name":"ViewProps","package":"react-native"}]},{"name":"AudioMixingMode","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies the audio mode that the player should use. Audio mode is set on per-app basis, if there are multiple players playing and\nhave different a "},{"kind":"code","text":"`AudioMode`"},{"kind":"text","text":" specified, the highest priority mode will be used. Priority order: 'doNotMix' > 'auto' > 'duckOthers' > 'mixWithOthers'.\n\n- "},{"kind":"code","text":"`mixWithOthers`"},{"kind":"text","text":": The player will mix its audio output with other apps.\n- "},{"kind":"code","text":"`duckOthers`"},{"kind":"text","text":": The player will lower the volume of other apps if any of the active players is outputting audio.\n- "},{"kind":"code","text":"`auto`"},{"kind":"text","text":": The player will allow other apps to keep playing audio only when it is muted. On iOS it will always interrupt other apps when "},{"kind":"code","text":"`showNowPlayingNotification`"},{"kind":"text","text":" is "},{"kind":"code","text":"`true`"},{"kind":"text","text":" due to system requirements.\n- "},{"kind":"code","text":"`doNotMix`"},{"kind":"text","text":": The player will pause playback in other apps, even when it's muted.\n\n> On iOS, the Now Playing notification is dependent on the audio mode. If the audio mode is different from "},{"kind":"code","text":"`doNotMix`"},{"kind":"text","text":" or "},{"kind":"code","text":"`auto`"},{"kind":"text","text":" this feature will not work."}]},"type":{"type":"union","types":[{"type":"literal","value":"mixWithOthers"},{"type":"literal","value":"duckOthers"},{"type":"literal","value":"auto"},{"type":"literal","value":"doNotMix"}]}},{"name":"AudioTrack","variant":"declaration","kind":2097152,"children":[{"name":"id","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A string used by expo-video to identify the audio track."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"label","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Label of the audio track in the language of the device."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"language","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Language of the audio track. For example, 'en', 'pl', 'de'."}]},"type":{"type":"intrinsic","name":"string"}}]},{"name":"BufferOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies buffer options which will be used by the player when buffering the video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"maxBufferBytes","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"The maximum number of bytes that the player can buffer from the network.\nWhen 0 the player will automatically decide appropriate buffer size."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"minBufferForPlayback","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"Minimum duration of the buffer in seconds required to continue playing after the player has been paused or started buffering.\n\n> This property will be ignored if "},{"kind":"code","text":"`preferredForwardBufferDuration`"},{"kind":"text","text":" is lower."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"2"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"preferredForwardBufferDuration","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"The duration in seconds which determines how much media the player should buffer ahead of the current playback time.\n\nOn iOS when set to "},{"kind":"code","text":"`0`"},{"kind":"text","text":" the player will automatically decide appropriate buffer duration.\n\nEquivalent to ["},{"kind":"code","text":"`AVPlayerItem.preferredForwardBufferDuration`"},{"kind":"text","text":"](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"Android: 20, iOS: 0"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"prioritizeTimeOverSizeThreshold","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"A Boolean value which determines whether the player should prioritize time over size when buffering media."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"waitsToMinimizeStalling","variant":"declaration","kind":1024,"flags":{"isOptional":true,"isReadonly":true},"comment":{"summary":[{"kind":"text","text":"A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.\n\nEquivalent to ["},{"kind":"code","text":"`AVPlayer.automaticallyWaitsToMinimizeStalling`"},{"kind":"text","text":"](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ButtonOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Configuration for controlling the visibility of player control buttons.\n\n> The fullscreen button should be controlled with ["},{"kind":"code","text":"`fullscreenOptions.enable`"},{"kind":"text","text":"](#fullscreenoptions)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"children":[{"name":"showBottomBar","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the bottom control bar (containing time, progress bar, and buttons).\nWhen set to "},{"kind":"code","text":"`false`"},{"kind":"text","text":", the entire bottom bar including the progress bar will be hidden.\n\n> **Note**: The bottom bar is always visible in fullscreen mode to allow users to exit fullscreen."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showNext","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the next button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showPlayPause","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the play/pause button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showPrevious","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the previous button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSeekBackward","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the seek backward button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSeekForward","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the seek forward button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSettings","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the settings button."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"showSubtitles","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show the subtitles button.\n- "},{"kind":"code","text":"`true`"},{"kind":"text","text":": Button is always visible\n- "},{"kind":"code","text":"`false`"},{"kind":"text","text":": Button is never visible\n- "},{"kind":"code","text":"`undefined`"},{"kind":"text","text":": Button is visible only when subtitles are available (default behavior)"}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"undefined"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"boolean"},{"type":"literal","value":null}]}}]},{"name":"ContentType","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies the content type of the source.\n\n- "},{"kind":"code","text":"`auto`"},{"kind":"text","text":": The player will automatically determine the content type of the video.\n- "},{"kind":"code","text":"`progressive`"},{"kind":"text","text":": The player will use progressive download content type. This is the default "},{"kind":"code","text":"`ContentType`"},{"kind":"text","text":" when the uri does not contain an extension.\n- "},{"kind":"code","text":"`hls`"},{"kind":"text","text":": The player will use HLS content type.\n- "},{"kind":"code","text":"`dash`"},{"kind":"text","text":": The player will use DASH content type (Android-only).\n- "},{"kind":"code","text":"`smoothStreaming`"},{"kind":"text","text":": The player will use SmoothStreaming content type (Android-only)."}],"blockTags":[{"tag":"@default","content":[{"kind":"code","text":"`auto`"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"auto"},{"type":"literal","value":"progressive"},{"type":"literal","value":"hls"},{"type":"literal","value":"dash"},{"type":"literal","value":"smoothStreaming"}]}},{"name":"DRMOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies DRM options which will be used by the player while loading the video."}]},"children":[{"name":"base64CertificateData","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the base64 encoded certificate data for the FairPlay DRM.\nWhen this property is set, the "},{"kind":"code","text":"`certificateUrl`"},{"kind":"text","text":" property is ignored."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"certificateUrl","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the certificate URL for the FairPlay DRM."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"contentId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the content ID of the stream."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"headers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Determines headers sent to the license server on license requests."}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"string"},{"type":"intrinsic","name":"string"}],"name":"Record","package":"typescript"}},{"name":"licenseServer","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines the license server URL."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"multiKey","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies whether the DRM is a multi-key DRM."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"type","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Determines which type of DRM to use."}]},"type":{"type":"reference","name":"DRMType","package":"expo-video"}}]},{"name":"DRMType","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies which type of DRM to use:\n- Android supports ClearKey, PlayReady and Widevine.\n- iOS supports FairPlay."}]},"type":{"type":"union","types":[{"type":"literal","value":"clearkey"},{"type":"literal","value":"fairplay"},{"type":"literal","value":"playready"},{"type":"literal","value":"widevine"}]}},{"name":"IsExternalPlaybackActiveChangeEventPayload","variant":"declaration","kind":2097152,"children":[{"name":"isExternalPlaybackActive","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The current external playback status."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"oldIsExternalPlaybackActive","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The previous external playback status."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"MutedChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`mutedChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"muted","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently muted."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"oldMuted","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`isMuted`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"PlaybackRateChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`playbackRateChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"oldPlaybackRate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`playbackRate`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"playbackRate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current playback speed of the player."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"PlayerBuilderOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Options to apply to the player builder before the native constructor is invoked"}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"children":[{"name":"seekBackwardIncrement","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Seek backward increment in seconds.\nValues will be clamped between 0.001 and 999 seconds."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"seekForwardIncrement","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Seek forward increment in seconds.\nValues will be clamped between 0.001 and 999 seconds."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"PlayerError","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Contains information about any errors that the player encountered during the playback"}]},"children":[{"name":"message","variant":"declaration","kind":1024,"type":{"type":"intrinsic","name":"string"}}]},{"name":"PlayingChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`playingChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"isPlaying","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Boolean value whether the player is currently playing."}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"oldIsPlaying","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`isPlaying`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"ScrubbingModeOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Defines scrubbing mode options used by a ["},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":"](#videoplayer)."}]},"children":[{"name":"allowSkippingMediaCodecFlush","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Sets whether to avoid flushing the decoder (where possible) in scrubbing mode.\nWhen "},{"kind":"code","text":"`true`"},{"kind":"text","text":", avoids flushing the decoder when a new seek starts decoding from a key-frame in compatible content."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"enableDynamicScheduling","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Sets whether ExoPlayer's dynamic scheduling should be enabled in scrubbing mode.\nThis can result in available output buffers being handled more quickly when seeking."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"increaseCodecOperatingRate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the codec operating rate should be increased in scrubbing mode."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"scrubbingModeEnabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the codec operating rate should be increased in scrubbing mode.\n\nYou should only enable this when the player is receiving a large number of seeks in a short period of time. For less frequent seeks, fine-tuning the ["},{"kind":"code","text":"`SeekTolerance`"},{"kind":"text","text":"](#seektolerance-1) may be sufficient.\n\nOn Android, the player may consume more resources in this mode, so it should only be used for short periods of time in response to user interaction (for example, dragging on a progress bar UI element).\n\nOn Android, when "},{"kind":"code","text":"`scrubbingModeEnabled`"},{"kind":"text","text":" is "},{"kind":"code","text":"`true`"},{"kind":"text","text":", the playback is suppressed. You should set this property back to "},{"kind":"code","text":"`false`"},{"kind":"text","text":" when the user interaction ends to allow the playback to resume.\nFor best results, on iOS you should pause the playback when scrubbing.\n\n> For best scrubbing performance, consider also increasing the seeking tolerance using the ["},{"kind":"code","text":"`SeekTolerance`"},{"kind":"text","text":"](#seektolerance-1) property.\n\n> Other scrubbing mode options will have no effect when this is "},{"kind":"code","text":"`false`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"useDecodeOnlyFlag","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Sets whether to use "},{"kind":"code","text":"`MediaCodec.BUFFER_FLAG_DECODE_ONLY`"},{"kind":"text","text":" in scrubbing mode.\nWhen playback is using MediaCodec on API 34+, this flag can speed up seeking by signalling that the decoded output of buffers between the previous keyframe and the target frame is not needed by the player."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"SeekTolerance","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Determines the time that the actual position seeked to may precede or exceed the requested seek position.\nLarger tolerance will usually result in faster seeking.\nThis property affects the precision of setting the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property and the ["},{"kind":"code","text":"`seekBy`"},{"kind":"text","text":"](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n\n> If you are trying to optimize for scrubbing (many frequent seeks), also see ["},{"kind":"code","text":"`ScrubbingModeOptions`"},{"kind":"text","text":"](#scrubbingmodeoptions-1)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"toleranceAfter","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The maximum time that the actual position seeked to may exceed the requested seek position, in seconds. Must be non-negative."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"toleranceBefore","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The maximum time that the actual position seeked to may precede the requested seek position, in seconds. Must be non-negative."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"0"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"SourceChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`sourceChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"oldSource","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous source of the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"source","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New source of the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}}]},{"name":"SourceLoadEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`sourceLoad`"},{"kind":"text","text":"](#videoplayerevents) event, contains information about the video source that has finished loading."}]},"children":[{"name":"availableAudioTracks","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Audio tracks available for the loaded video source."}]},"type":{"type":"array","elementType":{"type":"reference","name":"AudioTrack","package":"expo-video"}}},{"name":"availableSubtitleTracks","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Subtitle tracks available for the loaded video source."}]},"type":{"type":"array","elementType":{"type":"reference","name":"SubtitleTrack","package":"expo-video"}}},{"name":"availableVideoTracks","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Video tracks available for the loaded video source.\n\n> On iOS, when using a HLS source, make sure that the uri contains "},{"kind":"code","text":"`.m3u8`"},{"kind":"text","text":" extension or that the ["},{"kind":"code","text":"`contentType`"},{"kind":"text","text":"](#contenttype) property of the ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource) has been set to "},{"kind":"code","text":"`'hls'`"},{"kind":"text","text":". Otherwise, the video tracks will not be available."}]},"type":{"type":"array","elementType":{"type":"reference","name":"VideoTrack","package":"expo-video"}}},{"name":"duration","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Duration of the video source in seconds."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"videoSource","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The video source that has been loaded."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoSource","package":"expo-video"},{"type":"literal","value":null}]}}]},{"name":"StatusChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`statusChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"error","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Error object containing information about the error that occurred."}]},"type":{"type":"reference","name":"PlayerError","package":"expo-video"}},{"name":"oldStatus","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous status of the player."}]},"type":{"type":"reference","name":"VideoPlayerStatus","package":"expo-video"}},{"name":"status","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New status of the player."}]},"type":{"type":"reference","name":"VideoPlayerStatus","package":"expo-video"}}]},{"name":"SubtitleTrack","variant":"declaration","kind":2097152,"children":[{"name":"id","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A string used by "},{"kind":"code","text":"`expo-video`"},{"kind":"text","text":" to identify the subtitle track."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"label","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Label of the subtitle track in the language of the device."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"language","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Language of the subtitle track. For example, "},{"kind":"code","text":"`en`"},{"kind":"text","text":", "},{"kind":"code","text":"`pl`"},{"kind":"text","text":", "},{"kind":"code","text":"`de`"},{"kind":"text","text":"."}]},"type":{"type":"intrinsic","name":"string"}}]},{"name":"SubtitleTrackChangeEventPayload","variant":"declaration","kind":2097152,"children":[{"name":"oldSubtitleTrack","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous subtitle track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"SubtitleTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"subtitleTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New subtitle track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"SubtitleTrack","package":"expo-video"},{"type":"literal","value":null}]}}]},{"name":"SurfaceType","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Describes the type of the surface used to render the video.\n- "},{"kind":"code","text":"`surfaceView`"},{"kind":"text","text":": Uses the "},{"kind":"code","text":"`SurfaceView`"},{"kind":"text","text":" to render the video. This value should be used in the majority of cases. Provides significantly lower power consumption, better performance, and more features.\n- "},{"kind":"code","text":"`textureView`"},{"kind":"text","text":": Uses the "},{"kind":"code","text":"`TextureView`"},{"kind":"text","text":" to render the video. Should be used in cases where the SurfaceView is not supported or causes issues (for example, overlapping video views).\n\nYou can learn more about surface types in the official [ExoPlayer documentation](https://developer.android.com/media/media3/ui/playerview#surfacetype)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"textureView"},{"type":"literal","value":"surfaceView"}]}},{"name":"TimeUpdateEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`timeUpdate`"},{"kind":"text","text":"](#videoplayerevents) event, contains information about the current playback progress."}]},"children":[{"name":"bufferedPosition","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating how far the player has buffered the video in seconds.\nSame as the ["},{"kind":"code","text":"`bufferedPosition`"},{"kind":"text","text":"](#bufferedPosition) property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"currentLiveTimestamp","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The exact timestamp when the currently displayed video frame was sent from the server,\nbased on the "},{"kind":"code","text":"`EXT-X-PROGRAM-DATE-TIME`"},{"kind":"text","text":" tag in the livestream metadata.\nSame as the ["},{"kind":"code","text":"`currentLiveTimestamp`"},{"kind":"text","text":"](#currentlivetimestamp) property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentOffsetFromLive","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the latency of the live stream in seconds.\nSame as the ["},{"kind":"code","text":"`currentOffsetFromLive`"},{"kind":"text","text":"](#currentoffsetfromlive) property."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"currentTime","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current playback time in seconds. Same as the ["},{"kind":"code","text":"`currentTime`"},{"kind":"text","text":"](#currenttime) property."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"VideoContentFit","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Describes how a video should be scaled to fit in a container.\n- "},{"kind":"code","text":"`contain`"},{"kind":"text","text":": The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n- "},{"kind":"code","text":"`cover`"},{"kind":"text","text":": The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n- "},{"kind":"code","text":"`fill`"},{"kind":"text","text":": The video stretches/squeezes to completely fill the container, potentially causing distortion."}]},"type":{"type":"union","types":[{"type":"literal","value":"contain"},{"type":"literal","value":"cover"},{"type":"literal","value":"fill"}]}},{"name":"VideoMetadata","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Contains information that will be displayed in the now playing notification when the video is playing."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"artist","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Secondary text that will be displayed under the title."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"artwork","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The uri of the video artwork."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"title","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The title of the video."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"string"}}]},{"name":"VideoPlayerEvents","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Handlers for events which can be emitted by the player."}]},"children":[{"name":"audioTrackChange","variant":"declaration","kind":2048,"signatures":[{"name":"audioTrackChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current audio track changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"expo-video","packagePath":"src/VideoPlayerEvents.types.ts","qualifiedName":"AudioTrackChangeEventPayload"},"name":"AudioTrackChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"availableAudioTracksChange","variant":"declaration","kind":2048,"signatures":[{"name":"availableAudioTracksChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the available audio tracks change."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"expo-video","packagePath":"src/VideoPlayerEvents.types.ts","qualifiedName":"AvailableAudioTracksChangeEventPayload"},"name":"AvailableAudioTracksChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"availableSubtitleTracksChange","variant":"declaration","kind":2048,"signatures":[{"name":"availableSubtitleTracksChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the available subtitle tracks change."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"AvailableSubtitleTracksChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"isExternalPlaybackActiveChange","variant":"declaration","kind":2048,"signatures":[{"name":"isExternalPlaybackActiveChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the video player starts or stops sharing the video via AirPlay."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"IsExternalPlaybackActiveChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"mutedChange","variant":"declaration","kind":2048,"signatures":[{"name":"mutedChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the "},{"kind":"code","text":"`muted`"},{"kind":"text","text":" property of the player changes"}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"MutedChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"playbackRateChange","variant":"declaration","kind":2048,"signatures":[{"name":"playbackRateChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the "},{"kind":"code","text":"`playbackRate`"},{"kind":"text","text":" property of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"PlaybackRateChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"playingChange","variant":"declaration","kind":2048,"signatures":[{"name":"playingChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the player starts or stops playback."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"PlayingChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"playToEnd","variant":"declaration","kind":2048,"signatures":[{"name":"playToEnd","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the player plays to the end of the current source."}]},"type":{"type":"intrinsic","name":"void"}}]},{"name":"sourceChange","variant":"declaration","kind":2048,"signatures":[{"name":"sourceChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current media source of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"SourceChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"sourceLoad","variant":"declaration","kind":2048,"signatures":[{"name":"sourceLoad","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the player has finished loading metadata for the current video source.\nThis event is emitted when the player has finished metadata for a ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource), but it doesn't mean that there is enough data buffered to start the playback."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"SourceLoadEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"statusChange","variant":"declaration","kind":2048,"signatures":[{"name":"statusChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the status of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"StatusChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"subtitleTrackChange","variant":"declaration","kind":2048,"signatures":[{"name":"subtitleTrackChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current subtitle track changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"SubtitleTrackChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"timeUpdate","variant":"declaration","kind":2048,"signatures":[{"name":"timeUpdate","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted in a given interval specified by the "},{"kind":"code","text":"`timeUpdateEventInterval`"},{"kind":"text","text":"."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"TimeUpdateEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"videoTrackChange","variant":"declaration","kind":2048,"signatures":[{"name":"videoTrackChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the current video track changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoTrackChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]},{"name":"volumeChange","variant":"declaration","kind":2048,"signatures":[{"name":"volumeChange","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Handler for an event emitted when the "},{"kind":"code","text":"`volume`"},{"kind":"text","text":" of "},{"kind":"code","text":"`muted`"},{"kind":"text","text":" property of the player changes."}]},"parameters":[{"name":"payload","variant":"param","kind":32768,"type":{"type":"reference","name":"VolumeChangeEventPayload","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]}]},{"name":"VideoPlayerStatus","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Describes the current status of the player.\n- "},{"kind":"code","text":"`idle`"},{"kind":"text","text":": The player is not playing or loading any videos.\n- "},{"kind":"code","text":"`loading`"},{"kind":"text","text":": The player is loading video data from the provided source\n- "},{"kind":"code","text":"`readyToPlay`"},{"kind":"text","text":": The player has loaded enough data to start playing or to continue playback.\n- "},{"kind":"code","text":"`error`"},{"kind":"text","text":": The player has encountered an error while loading or playing the video."}]},"type":{"type":"union","types":[{"type":"literal","value":"idle"},{"type":"literal","value":"loading"},{"type":"literal","value":"readyToPlay"},{"type":"literal","value":"error"}]}},{"name":"VideoSize","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies the size of a video track."}]},"children":[{"name":"height","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Height of the video track in pixels."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"width","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Width of the video track in pixels."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"VideoSource","variant":"declaration","kind":2097152,"type":{"type":"union","types":[{"type":"intrinsic","name":"string"},{"type":"intrinsic","name":"number"},{"type":"literal","value":null},{"type":"reference","name":"VideoSourceObject","package":"expo-video"}]}},{"name":"VideoSourceObject","variant":"declaration","kind":2097152,"children":[{"name":"assetId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The asset ID of a local video asset, acquired with the "},{"kind":"code","text":"`require`"},{"kind":"text","text":" function.\nThis property is exclusive with the "},{"kind":"code","text":"`uri`"},{"kind":"text","text":" property. When both are present, the "},{"kind":"code","text":"`assetId`"},{"kind":"text","text":" will be ignored."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"contentType","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the content type of the video source. When set to "},{"kind":"code","text":"`'auto'`"},{"kind":"text","text":", the player will try to automatically determine the content type.\n\nYou should use this property when playing HLS, SmoothStreaming or DASH videos from an uri, which does not contain a standardized extension for the corresponding media type."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"'auto'"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"ContentType","package":"expo-video"}},{"name":"drm","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies the DRM options which will be used by the player while loading the video."}]},"type":{"type":"reference","name":"DRMOptions","package":"expo-video"}},{"name":"headers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies headers sent with the video request.\n> For DRM license headers use the "},{"kind":"code","text":"`headers`"},{"kind":"text","text":" field of ["},{"kind":"code","text":"`DRMOptions`"},{"kind":"text","text":"](#drmoptions)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Record"},"typeArguments":[{"type":"intrinsic","name":"string"},{"type":"intrinsic","name":"string"}],"name":"Record","package":"typescript"}},{"name":"metadata","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies information which will be displayed in the now playing notification.\nWhen undefined the player will display information contained in the video metadata."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","name":"VideoMetadata","package":"expo-video"}},{"name":"uri","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The URI of the video.\n\nOn iOS, "},{"kind":"code","text":"`PHAsset`"},{"kind":"text","text":" URIs are supported, but can only be loaded using the ["},{"kind":"code","text":"`replaceAsync`"},{"kind":"text","text":"](#replaceasyncsource) method or the default ["},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":"](#videoplayer) constructor.\n\nThis property is exclusive with the "},{"kind":"code","text":"`assetId`"},{"kind":"text","text":" property. When both are present, the "},{"kind":"code","text":"`assetId`"},{"kind":"text","text":" will be ignored."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"useCaching","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Specifies whether the player should use caching for the video.\n> Due to platform limitations, the cache cannot be used with HLS video sources on iOS. Caching DRM-protected videos is not supported on Android and iOS."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"VideoThumbnailOptions","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Additional options for video thumbnails generation."}]},"children":[{"name":"maxHeight","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"If provided, the generated thumbnail will not exceed this height in pixels, preserving its aspect ratio."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"maxWidth","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"If provided, the generated thumbnail will not exceed this width in pixels, preserving its aspect ratio."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"VideoTrack","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Specifies a VideoTrack loaded from a ["},{"kind":"code","text":"`VideoSource`"},{"kind":"text","text":"](#videosource)."}]},"children":[{"name":"averageBitrate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the average bitrate in bits per second or null if the value is unknown."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"bitrate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate if known, or else null."}],"blockTags":[{"tag":"@deprecated","content":[{"kind":"text","text":"Use "},{"kind":"code","text":"`peakBitrate`"},{"kind":"text","text":" or "},{"kind":"code","text":"`averageBitrate`"},{"kind":"text","text":" instead."}]}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"frameRate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the frame rate of the video track in frames per second."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"id","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The id of the video track.\n\n> This field is platform-specific and may return different depending on the operating system."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"isSupported","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Indicates whether the video track format is supported by the device."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"mimeType","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"MimeType of the video track or null if unknown."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"string"},{"type":"literal","value":null}]}},{"name":"peakBitrate","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Specifies the average bitrate in bits per second or null if the value is unknown."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"number"},{"type":"literal","value":null}]}},{"name":"size","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Size of the video track."}]},"type":{"type":"reference","name":"VideoSize","package":"expo-video"}},{"name":"url","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The URL of the "},{"kind":"code","text":"`VideoTrack`"},{"kind":"text","text":" for HLS video sources. "},{"kind":"code","text":"`null`"},{"kind":"text","text":" for other source types."}]},"type":{"type":"union","types":[{"type":"intrinsic","name":"string"},{"type":"literal","value":null}]}}]},{"name":"VideoTrackChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`videoTrackChange`"},{"kind":"text","text":"](#videoplayerevents) event, contains information about the video track which is currently being played."}]},"children":[{"name":"oldVideoTrack","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous video track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoTrack","package":"expo-video"},{"type":"literal","value":null}]}},{"name":"videoTrack","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"New video track of the player."}]},"type":{"type":"union","types":[{"type":"reference","name":"VideoTrack","package":"expo-video"},{"type":"literal","value":null}]}}]},{"name":"VolumeChangeEventPayload","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Data delivered with the ["},{"kind":"code","text":"`volumeChange`"},{"kind":"text","text":"](#videoplayerevents) event."}]},"children":[{"name":"oldVolume","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Previous value of the "},{"kind":"code","text":"`volume`"},{"kind":"text","text":" property."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"volume","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Float value indicating the current volume of the player."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"clearVideoCacheAsync","variant":"declaration","kind":64,"signatures":[{"name":"clearVideoCacheAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Clears all video cache.\n> This function can be called only if there are no existing "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" instances."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that fulfills after the cache has been cleaned."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"createVideoPlayer","variant":"declaration","kind":64,"signatures":[{"name":"createVideoPlayer","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates a direct instance of "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" that doesn't release automatically.\n\n> **info** For most use cases you should use the ["},{"kind":"code","text":"`useVideoPlayer`"},{"kind":"text","text":"](#usevideoplayer) hook instead. See the [Using the VideoPlayer Directly](#using-the-videoplayer-directly) section for more details."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"comment":{"summary":[{"kind":"text","text":"A video source that is used to initialize the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"playerBuilderOptions","variant":"param","kind":32768,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Options to apply to the Android player builder before the native constructor is invoked."}]},"type":{"type":"reference","name":"PlayerBuilderOptions","package":"expo-video"}}],"type":{"type":"reference","name":"VideoPlayer","package":"expo-video"}}]},{"name":"getCurrentVideoCacheSize","variant":"declaration","kind":64,"signatures":[{"name":"getCurrentVideoCacheSize","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Returns the space currently occupied by the video cache in bytes."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"isPictureInPictureSupported","variant":"declaration","kind":64,"signatures":[{"name":"isPictureInPictureSupported","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Returns whether the current device supports Picture in Picture (PiP) mode."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A "},{"kind":"code","text":"`boolean`"},{"kind":"text","text":" which is "},{"kind":"code","text":"`true`"},{"kind":"text","text":" if the device supports PiP mode, and "},{"kind":"code","text":"`false`"},{"kind":"text","text":" otherwise."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"setVideoCacheSizeAsync","variant":"declaration","kind":64,"signatures":[{"name":"setVideoCacheSizeAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Sets desired video cache size in bytes. The default video cache size is 1GB. Value set by this function is persistent.\nThe cache size is not guaranteed to be exact and the actual cache size may be slightly larger. The cache is evicted on a least-recently-used basis.\n> This function can be called only if there are no existing "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":" instances."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that fulfills after the cache size has been set."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"sizeBytes","variant":"param","kind":32768,"type":{"type":"intrinsic","name":"number"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"intrinsic","name":"void"}],"name":"Promise","package":"typescript"}}]},{"name":"useVideoPlayer","variant":"declaration","kind":64,"signatures":[{"name":"useVideoPlayer","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates a "},{"kind":"code","text":"`VideoPlayer`"},{"kind":"text","text":", which will be automatically cleaned up when the component is unmounted."}]},"parameters":[{"name":"source","variant":"param","kind":32768,"comment":{"summary":[{"kind":"text","text":"A video source that is used to initialize the player."}]},"type":{"type":"reference","name":"VideoSource","package":"expo-video"}},{"name":"setup","variant":"param","kind":32768,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"A function that allows setting up the player. It will run after the player is created."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"parameters":[{"name":"player","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoPlayer","package":"expo-video"}}],"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"playerBuilderOptions","variant":"param","kind":32768,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Options to apply to the Android player builder before the native constructor is invoked."}]},"type":{"type":"reference","name":"PlayerBuilderOptions","package":"expo-video"}}],"type":{"type":"reference","name":"VideoPlayer","package":"expo-video"}}]},{"name":"VideoAirPlayButton","variant":"declaration","kind":64,"signatures":[{"name":"VideoAirPlayButton","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A view displaying the ["},{"kind":"code","text":"`AVRoutePickerView`"},{"kind":"text","text":"](https://developer.apple.com/documentation/avkit/avroutepickerview). Shows a button, when pressed, an AirPlay device picker shows up, allowing users to stream the currently playing video\nto any available AirPlay sink.\n\n> When using this view, make sure that the ["},{"kind":"code","text":"`allowsExternalPlayback`"},{"kind":"text","text":"](#allowsexternalplayback) player property is set to "},{"kind":"code","text":"`true`"},{"kind":"text","text":"."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"VideoAirPlayButtonProps","package":"expo-video"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"React.JSX.Element"}}]}],"packageName":"expo-video"}
\ No newline at end of file
diff --git a/docs/yarn.lock b/docs/yarn.lock
index a462ec65644a47..d08929b38ddde2 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -1006,9 +1006,9 @@ __metadata:
languageName: node
linkType: hard
-"@expo/styleguide-search-ui@npm:^3.3.1":
- version: 3.3.1
- resolution: "@expo/styleguide-search-ui@npm:3.3.1"
+"@expo/styleguide-search-ui@npm:^3.3.3":
+ version: 3.3.3
+ resolution: "@expo/styleguide-search-ui@npm:3.3.3"
dependencies:
"@expo/styleguide": "npm:^9.3.1"
"@expo/styleguide-icons": "npm:^2.3.4"
@@ -1024,7 +1024,7 @@ __metadata:
peerDependencies:
next: ">= 13"
react: ">= 19"
- checksum: 10c0/ff149cf84459e50fe7bc33134db043e3d4afcb5e287e89d6c9cdab7f2d2c57c4bdfc93f454d98a64ee075551c3889c5cdf79740fc9cb56ea4701ccceb845ca0e
+ checksum: 10c0/fa48338c1ff3fb6be6de7e08a0fa7b6ec005e290771af7443fc35628c120d41f2bf24c761031fc95b83a94ce7d3e5b7ff6e803d1ba2ad7aa3e3145c4e7c3b25a
languageName: node
linkType: hard
@@ -7959,7 +7959,7 @@ __metadata:
"@expo/styleguide": "npm:^9.3.1"
"@expo/styleguide-base": "npm:^2.0.5"
"@expo/styleguide-icons": "npm:^2.3.4"
- "@expo/styleguide-search-ui": "npm:^3.3.1"
+ "@expo/styleguide-search-ui": "npm:^3.3.3"
"@kapaai/react-sdk": "npm:^0.9.0"
"@mdx-js/loader": "npm:^3.1.1"
"@mdx-js/mdx": "npm:^3.1.1"
diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md
index 47341274030e8c..83240729f34012 100644
--- a/packages/@expo/cli/CHANGELOG.md
+++ b/packages/@expo/cli/CHANGELOG.md
@@ -72,6 +72,7 @@ _This version does not introduce any user-facing changes._
- Fix loader URL resolution for nested `/index` paths ([#42629](https://github.com/expo/expo/pull/42629) by [@hassankhan](https://github.com/hassankhan))
- [web] Ensure `` component re-renders when focus changes ([#42681](https://github.com/expo/expo/pull/42681) by [@hassankhan](https://github.com/hassankhan))
- Mark `expo-router` as optional peer to prevent auto-installation ([#42728](https://github.com/expo/expo/pull/42728) by [@kitten](https://github.com/kitten))
+- [android] Support `debugOptimized` build variant for active architecture filtering ([#42766](https://github.com/expo/expo/pull/42766) by [@janicduplessis](https://github.com/janicduplessis))
### 💡 Others
diff --git a/packages/@expo/cli/src/run/android/__tests__/resolveGradlePropsAsync-test.ts b/packages/@expo/cli/src/run/android/__tests__/resolveGradlePropsAsync-test.ts
index ca39f1e91e06e1..5788c55a5e0528 100644
--- a/packages/@expo/cli/src/run/android/__tests__/resolveGradlePropsAsync-test.ts
+++ b/packages/@expo/cli/src/run/android/__tests__/resolveGradlePropsAsync-test.ts
@@ -76,10 +76,13 @@ describe(resolveGradlePropsAsync, () => {
});
it('returns with highly custom variant "firstSecondThird"', async () => {
+ // See: https://android.googlesource.com/platform/tools/base/+/e3b89c93d5e8238d6163f4e606d4ae9c4d229ee4/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/variant/VariantPathHelper.kt?autodive=0%2F%2F%2F%2F%2F#137
+ // See: https://github.com/zawn/android-gradle-plugin/blob/c5d0ab91fac5a4acbe1845d3743b5ea0f896983d/builder/src/com/android/builder/core/VariantConfiguration.java#L402-L409
+ // The flavor should only be one directory, rather than multiple
expect(
await resolveGradlePropsAsync('/', { variant: 'firstSecondThird', allArch: true }, testDevice)
).toEqual({
- apkVariantDirectory: '/android/app/build/outputs/apk/first/second/third',
+ apkVariantDirectory: '/android/app/build/outputs/apk/firstSecond/third',
appName: 'app',
buildType: 'third',
flavors: ['first', 'second'],
diff --git a/packages/@expo/cli/src/run/android/__tests__/resolveOptions-test.ts b/packages/@expo/cli/src/run/android/__tests__/resolveOptions-test.ts
index a9b03ca40831c0..a7a78c57572482 100644
--- a/packages/@expo/cli/src/run/android/__tests__/resolveOptions-test.ts
+++ b/packages/@expo/cli/src/run/android/__tests__/resolveOptions-test.ts
@@ -64,7 +64,7 @@ describe(resolveOptionsAsync, () => {
appId: 'dev.expo.test',
})
).toEqual({
- apkVariantDirectory: '/android/app/build/outputs/apk/first/second/third',
+ apkVariantDirectory: '/android/app/build/outputs/apk/firstSecond/third',
appName: 'app',
buildCache: true,
buildType: 'third',
diff --git a/packages/@expo/cli/src/run/android/resolveGradlePropsAsync.ts b/packages/@expo/cli/src/run/android/resolveGradlePropsAsync.ts
index 0710fa1563839e..605ee5a9fac2f6 100644
--- a/packages/@expo/cli/src/run/android/resolveGradlePropsAsync.ts
+++ b/packages/@expo/cli/src/run/android/resolveGradlePropsAsync.ts
@@ -37,22 +37,34 @@ export async function resolveGradlePropsAsync(
const apkDirectory = path.join(projectRoot, 'android', appName, 'build', 'outputs', 'apk');
- // buildDeveloperTrust -> buildtype: trust, flavors: build, developer
+ // buildDeveloperTrust -> buildtype: trust, flavors: buildDeveloper
// developmentDebug -> buildType: debug, flavors: development
// productionRelease -> buildType: release, flavors: production
- // This won't work for non-standard flavor names like "myFlavor" would be treated as "my", "flavor".
- const flavors = variant.split(/(?=[A-Z])/).map((v) => v.toLowerCase());
- const buildType = flavors.pop() ?? 'debug';
+ // previewDebugOptimized -> buildType: debugOptimized, flavors: preview
+ const parts = variant.split(/(?=[A-Z])/);
- const apkVariantDirectory = path.join(apkDirectory, ...flavors, buildType);
- const architectures = await getConnectedDeviceABIS(buildType, device, options.allArch);
+ // Special case: merge 'Optimized' suffix with preceding part, e.g. into 'debugOptimized'
+ let buildType = parts.pop() ?? 'debug';
+ if (parts.length > 0 && buildType === 'Optimized') {
+ buildType = parts.pop()!.toLowerCase() + buildType;
+ } else {
+ buildType = buildType.toLowerCase();
+ }
+
+ let apkVariantDirectory: string;
+ if (parts.length > 0) {
+ const flavorPath = parts[0].toLowerCase() + parts.slice(1).join('');
+ apkVariantDirectory = path.join(apkDirectory, flavorPath, buildType);
+ } else {
+ apkVariantDirectory = path.join(apkDirectory, buildType);
+ }
return {
appName,
buildType,
- flavors,
+ flavors: parts.map((v) => v.toLowerCase()),
apkVariantDirectory,
- architectures,
+ architectures: await getConnectedDeviceABIS(buildType, device, options.allArch),
};
}
@@ -62,7 +74,9 @@ async function getConnectedDeviceABIS(
allArch?: boolean
): Promise {
// Follow the same behavior as iOS, only enable this for debug builds
- if (allArch || buildType !== 'debug') {
+ // Support both 'debug' and 'debugOptimized' build types
+ const isDebugBuild = buildType === 'debug' || buildType === 'debugOptimized';
+ if (allArch || !isDebugBuild) {
return '';
}
diff --git a/packages/expo-brownfield/CHANGELOG.md b/packages/expo-brownfield/CHANGELOG.md
index 4fcfa6a60b441d..73ccf9bdb27e92 100644
--- a/packages/expo-brownfield/CHANGELOG.md
+++ b/packages/expo-brownfield/CHANGELOG.md
@@ -23,6 +23,7 @@
### 💡 Others
- [test] add compilation verification and optimize brownfield workflow in [#42894](https://github.com/expo/expo/pull/42894) by [@pmleczek](https://github.com/pmleczek)
+- [cli] cli refactor ([#42921](https://github.com/expo/expo/pull/42921) by [@pmleczek](https://github.com/pmleczek))
## 55.0.7 — 2026-02-08
diff --git a/packages/expo-brownfield/cli/build/commands/android/build.d.ts b/packages/expo-brownfield/cli/build/commands/android/build.d.ts
deleted file mode 100644
index 9b34b840ecf5fc..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/android/build.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-declare const action: () => Promise;
-export default action;
diff --git a/packages/expo-brownfield/cli/build/commands/android/build.js b/packages/expo-brownfield/cli/build/commands/android/build.js
deleted file mode 100644
index 37d635b944ed08..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/android/build.js
+++ /dev/null
@@ -1,68 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-const path_1 = __importDefault(require("path"));
-const constants_1 = require("../../constants");
-const utils_1 = require("../../utils");
-const action = async () => {
- const args = (0, utils_1.parseArgs)({
- spec: constants_1.Args.Android,
- // Skip first three args:
- // expo-brownfield build:android
- argv: process.argv.slice(3),
- stopAtPositional: true,
- });
- if ((0, utils_1.getCommand)(args)) {
- return constants_1.Errors.additionalCommand('build:android');
- }
- // Only resolve --help and --verbose options
- const basicConfig = (0, utils_1.getCommonConfig)(args);
- if (basicConfig.help) {
- console.log(constants_1.Help.Android);
- return process.exit(0);
- }
- await (0, utils_1.ensurePrebuild)('android');
- const config = await (0, utils_1.getAndroidConfig)(args);
- (0, utils_1.printConfig)(config);
- let tasks = [];
- if (config.tasks.length > 0) {
- tasks = config.tasks;
- }
- else if (config.repositories.length > 0) {
- for (const repository of config.repositories) {
- const task = constructTask(config.buildType, repository);
- tasks.push(task);
- }
- }
- else {
- constants_1.Errors.missingTasksOrRepositories();
- }
- for (const task of tasks) {
- if (!config.dryRun) {
- await runTask(task, config.verbose);
- }
- else {
- console.log(`./gradlew ${task}`);
- }
- }
-};
-exports.default = action;
-const constructTask = (buildType, repository) => {
- const buildTypeCapitalized = buildType[0].toUpperCase() + buildType.slice(1);
- const repositorySuffixed = repository === 'MavenLocal' ? repository : `${repository}Repository`;
- return `publishBrownfield${buildTypeCapitalized}PublicationTo${repositorySuffixed}`;
-};
-const runTask = async (task, verbose) => {
- return (0, utils_1.withSpinner)({
- operation: () => (0, utils_1.runCommand)('./gradlew', [task], {
- cwd: path_1.default.join(process.cwd(), 'android'),
- verbose,
- }),
- loaderMessage: 'Running task: ' + task,
- successMessage: 'Running task: ' + task + ' succeeded',
- errorMessage: 'Running task: ' + task + ' failed',
- verbose,
- });
-};
diff --git a/packages/expo-brownfield/cli/build/commands/android/index.d.ts b/packages/expo-brownfield/cli/build/commands/android/index.d.ts
deleted file mode 100644
index 867e7d814fde67..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/android/index.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as runBuildAndroid } from './build';
-export { default as runTasksAndroid } from './tasks';
diff --git a/packages/expo-brownfield/cli/build/commands/android/index.js b/packages/expo-brownfield/cli/build/commands/android/index.js
deleted file mode 100644
index 9c076969b8bcbe..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/android/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.runTasksAndroid = exports.runBuildAndroid = void 0;
-var build_1 = require("./build");
-Object.defineProperty(exports, "runBuildAndroid", { enumerable: true, get: function () { return __importDefault(build_1).default; } });
-var tasks_1 = require("./tasks");
-Object.defineProperty(exports, "runTasksAndroid", { enumerable: true, get: function () { return __importDefault(tasks_1).default; } });
diff --git a/packages/expo-brownfield/cli/build/commands/android/tasks.d.ts b/packages/expo-brownfield/cli/build/commands/android/tasks.d.ts
deleted file mode 100644
index 9b34b840ecf5fc..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/android/tasks.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-declare const action: () => Promise;
-export default action;
diff --git a/packages/expo-brownfield/cli/build/commands/android/tasks.js b/packages/expo-brownfield/cli/build/commands/android/tasks.js
deleted file mode 100644
index 58c9b23d492564..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/android/tasks.js
+++ /dev/null
@@ -1,63 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-const chalk_1 = __importDefault(require("chalk"));
-const path_1 = __importDefault(require("path"));
-const constants_1 = require("../../constants");
-const utils_1 = require("../../utils");
-const action = async () => {
- const args = (0, utils_1.parseArgs)({
- spec: constants_1.Args.TasksAndroid,
- // Skip first three args:
- // expo-brownfield tasks:android
- argv: process.argv.slice(3),
- stopAtPositional: true,
- });
- if ((0, utils_1.getCommand)(args)) {
- return constants_1.Errors.additionalCommand('tasks:android');
- }
- const config = await (0, utils_1.getTasksAndroidConfig)(args);
- if (config.help) {
- console.log(constants_1.Help.TasksAndroid);
- return process.exit(0);
- }
- const { stdout } = await (0, utils_1.withSpinner)({
- operation: () => (0, utils_1.runCommand)('./gradlew', [`${config.libraryName}:tasks`, '--group', 'publishing'], {
- cwd: path_1.default.join(process.cwd(), 'android'),
- verbose: config.verbose,
- }),
- loaderMessage: 'Reading publish tasks from the android project...',
- successMessage: 'Successfully read publish tasks from the android project\n',
- errorMessage: 'Failed to read publish tasks from the android project',
- verbose: config.verbose,
- });
- if (config.verbose) {
- // stdout is already printed to the console
- return;
- }
- const regex = /^publishBrownfield[a-zA-Z0-9_-]*/i;
- const publishTasks = stdout
- .split('\n')
- .map((line) => regex.exec(line)?.[0])
- // Remove duplicate maven local tasks
- .filter((task) => task && !task.includes('MavenLocalRepository'));
- console.log(chalk_1.default.bold('Publish tasks:'));
- publishTasks.forEach((task) => {
- console.log(`- ${task}`);
- });
- const splitRegex = /^publishBrownfield(?:All|Debug|Release)PublicationTo(.+?)(?:Repository)?$/;
- const repositories = [
- ...new Set(publishTasks
- .map((task) => {
- return splitRegex.exec(task)?.[1];
- })
- .filter((repo) => repo)),
- ];
- console.log(chalk_1.default.bold('\nRepositories:'));
- repositories.forEach((repo) => {
- console.log(`- ${repo}`);
- });
-};
-exports.default = action;
diff --git a/packages/expo-brownfield/cli/build/commands/build-android.d.ts b/packages/expo-brownfield/cli/build/commands/build-android.d.ts
new file mode 100644
index 00000000000000..79f1691d728677
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/commands/build-android.d.ts
@@ -0,0 +1,3 @@
+import type { Command } from 'commander';
+declare const buildAndroid: (command: Command) => Promise;
+export default buildAndroid;
diff --git a/packages/expo-brownfield/cli/build/commands/build-android.js b/packages/expo-brownfield/cli/build/commands/build-android.js
new file mode 100644
index 00000000000000..296e978f31b43f
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/commands/build-android.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const utils_1 = require("../utils");
+const buildAndroid = async (command) => {
+ await (0, utils_1.validatePrebuild)('android');
+ const config = (0, utils_1.resolveBuildConfigAndroid)(command.opts());
+ if (!config.tasks.length) {
+ utils_1.CLIError.handle('android-task-repo');
+ }
+ (0, utils_1.printAndroidConfig)(config);
+ for (const task of config.tasks) {
+ await (0, utils_1.runTask)(task, config.verbose, config.dryRun);
+ }
+};
+exports.default = buildAndroid;
diff --git a/packages/expo-brownfield/cli/build/commands/build-ios.d.ts b/packages/expo-brownfield/cli/build/commands/build-ios.d.ts
new file mode 100644
index 00000000000000..c90045126f74d4
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/commands/build-ios.d.ts
@@ -0,0 +1,3 @@
+import type { Command } from 'commander';
+declare const buildIos: (command: Command) => Promise;
+export default buildIos;
diff --git a/packages/expo-brownfield/cli/build/commands/build-ios.js b/packages/expo-brownfield/cli/build/commands/build-ios.js
new file mode 100644
index 00000000000000..548ba5b7084fa0
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/commands/build-ios.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const utils_1 = require("../utils");
+const buildIos = async (command) => {
+ await (0, utils_1.validatePrebuild)('ios');
+ const config = (0, utils_1.resolveBuildConfigIos)(command.opts());
+ (0, utils_1.printIosConfig)(config);
+ await (0, utils_1.cleanUpArtifacts)(config);
+ (0, utils_1.makeArtifactsDirectory)(config);
+ await (0, utils_1.buildFramework)(config);
+ await (0, utils_1.createXcframework)(config);
+ await (0, utils_1.copyHermesXcframework)(config);
+};
+exports.default = buildIos;
diff --git a/packages/expo-brownfield/cli/build/commands/commands.d.ts b/packages/expo-brownfield/cli/build/commands/commands.d.ts
deleted file mode 100644
index f68d6294139cf8..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/commands.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { CommandsMap } from './types';
-export declare const Commands: CommandsMap;
diff --git a/packages/expo-brownfield/cli/build/commands/commands.js b/packages/expo-brownfield/cli/build/commands/commands.js
deleted file mode 100644
index 12c7dabd890f96..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/commands.js
+++ /dev/null
@@ -1,23 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.Commands = void 0;
-const android_1 = require("./android");
-const general_1 = require("./general");
-const ios_1 = require("./ios");
-exports.Commands = {
- 'build:android': {
- run: android_1.runBuildAndroid,
- },
- 'build:ios': {
- run: ios_1.runBuildIos,
- },
- help: {
- run: general_1.runHelp,
- },
- 'tasks:android': {
- run: android_1.runTasksAndroid,
- },
- version: {
- run: general_1.runVersion,
- },
-};
diff --git a/packages/expo-brownfield/cli/build/commands/general/help.d.ts b/packages/expo-brownfield/cli/build/commands/general/help.d.ts
deleted file mode 100644
index 93f238819b6b01..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/general/help.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * Prints the help message for the CLI.
- */
-declare const helpAction: () => Promise;
-export default helpAction;
diff --git a/packages/expo-brownfield/cli/build/commands/general/help.js b/packages/expo-brownfield/cli/build/commands/general/help.js
deleted file mode 100644
index 9bdaab2d3a76b9..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/general/help.js
+++ /dev/null
@@ -1,10 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-const constants_1 = require("../../constants");
-/**
- * Prints the help message for the CLI.
- */
-const helpAction = async () => {
- console.log(constants_1.Help.General);
-};
-exports.default = helpAction;
diff --git a/packages/expo-brownfield/cli/build/commands/general/index.d.ts b/packages/expo-brownfield/cli/build/commands/general/index.d.ts
deleted file mode 100644
index 3d8eb9fed89bd4..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/general/index.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as runHelp } from './help';
-export { default as runVersion } from './version';
diff --git a/packages/expo-brownfield/cli/build/commands/general/index.js b/packages/expo-brownfield/cli/build/commands/general/index.js
deleted file mode 100644
index f61366d02d3c6c..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/general/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.runVersion = exports.runHelp = void 0;
-var help_1 = require("./help");
-Object.defineProperty(exports, "runHelp", { enumerable: true, get: function () { return __importDefault(help_1).default; } });
-var version_1 = require("./version");
-Object.defineProperty(exports, "runVersion", { enumerable: true, get: function () { return __importDefault(version_1).default; } });
diff --git a/packages/expo-brownfield/cli/build/commands/general/version.d.ts b/packages/expo-brownfield/cli/build/commands/general/version.d.ts
deleted file mode 100644
index 90ce498b5d78a0..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/general/version.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * Prints the version of the CLI (= the version of the package).
- */
-declare const action: () => Promise;
-export default action;
diff --git a/packages/expo-brownfield/cli/build/commands/general/version.js b/packages/expo-brownfield/cli/build/commands/general/version.js
deleted file mode 100644
index ad5ef47a6118c6..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/general/version.js
+++ /dev/null
@@ -1,11 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-// @ts-expect-error - the directory structure is different after building
-const package_json_1 = require("../../../../package.json");
-/**
- * Prints the version of the CLI (= the version of the package).
- */
-const action = async () => {
- console.log(package_json_1.version);
-};
-exports.default = action;
diff --git a/packages/expo-brownfield/cli/build/commands/index.d.ts b/packages/expo-brownfield/cli/build/commands/index.d.ts
index 0739a373502d94..3ad397d285bfe7 100644
--- a/packages/expo-brownfield/cli/build/commands/index.d.ts
+++ b/packages/expo-brownfield/cli/build/commands/index.d.ts
@@ -1,3 +1,3 @@
-export * from './commands';
-export * from './resolve';
-export type * from './types';
+export { default as buildAndroid } from './build-android';
+export { default as buildIos } from './build-ios';
+export { default as tasksAndroid } from './tasks-android';
diff --git a/packages/expo-brownfield/cli/build/commands/index.js b/packages/expo-brownfield/cli/build/commands/index.js
index 8c66c1b23548c6..a5e84ea2f9e537 100644
--- a/packages/expo-brownfield/cli/build/commands/index.js
+++ b/packages/expo-brownfield/cli/build/commands/index.js
@@ -1,18 +1,12 @@
"use strict";
-var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- var desc = Object.getOwnPropertyDescriptor(m, k);
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
- desc = { enumerable: true, get: function() { return m[k]; } };
- }
- Object.defineProperty(o, k2, desc);
-}) : (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- o[k2] = m[k];
-}));
-var __exportStar = (this && this.__exportStar) || function(m, exports) {
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
-__exportStar(require("./commands"), exports);
-__exportStar(require("./resolve"), exports);
+exports.tasksAndroid = exports.buildIos = exports.buildAndroid = void 0;
+var build_android_1 = require("./build-android");
+Object.defineProperty(exports, "buildAndroid", { enumerable: true, get: function () { return __importDefault(build_android_1).default; } });
+var build_ios_1 = require("./build-ios");
+Object.defineProperty(exports, "buildIos", { enumerable: true, get: function () { return __importDefault(build_ios_1).default; } });
+var tasks_android_1 = require("./tasks-android");
+Object.defineProperty(exports, "tasksAndroid", { enumerable: true, get: function () { return __importDefault(tasks_android_1).default; } });
diff --git a/packages/expo-brownfield/cli/build/commands/ios/build.d.ts b/packages/expo-brownfield/cli/build/commands/ios/build.d.ts
deleted file mode 100644
index 9b34b840ecf5fc..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/ios/build.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-declare const action: () => Promise;
-export default action;
diff --git a/packages/expo-brownfield/cli/build/commands/ios/build.js b/packages/expo-brownfield/cli/build/commands/ios/build.js
deleted file mode 100644
index 7787fa09a1017c..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/ios/build.js
+++ /dev/null
@@ -1,125 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-const promises_1 = __importDefault(require("node:fs/promises"));
-const constants_1 = require("../../constants");
-const utils_1 = require("../../utils");
-const action = async () => {
- const args = (0, utils_1.parseArgs)({
- spec: constants_1.Args.IOS,
- // Skip first three args:
- // expo-brownfield build:ios
- argv: process.argv.slice(3),
- stopAtPositional: true,
- });
- if ((0, utils_1.getCommand)(args)) {
- return constants_1.Errors.additionalCommand('build:ios');
- }
- // Only resolve --help and --verbose options
- const basicConfig = (0, utils_1.getCommonConfig)(args);
- if (basicConfig.help) {
- console.log(constants_1.Help.IOS);
- return process.exit(0);
- }
- await (0, utils_1.ensurePrebuild)('ios');
- const config = await (0, utils_1.getIosConfig)(args);
- (0, utils_1.printConfig)(config);
- await cleanUpArtifacts(config);
- await runBuild(config);
- await packageFrameworks(config);
- await copyHermesFramework(config);
-};
-exports.default = action;
-const cleanUpArtifacts = async (config) => {
- if (config.dryRun) {
- console.log('Cleaning up previous artifacts');
- return;
- }
- return (0, utils_1.withSpinner)({
- operation: async () => {
- try {
- await promises_1.default.access(config.artifacts);
- }
- catch (error) {
- return;
- }
- const artifacts = (await promises_1.default.readdir(config.artifacts)).filter((artifact) => artifact.endsWith('.xcframework'));
- for (const artifact of artifacts) {
- await promises_1.default.rm(`${config.artifacts}/${artifact}`, {
- recursive: true,
- force: true,
- });
- }
- },
- loaderMessage: 'Cleaning up previous artifacts...',
- successMessage: 'Cleaning up previous artifacts succeeded',
- errorMessage: 'Cleaning up previous artifacts failed',
- });
-};
-const runBuild = async (config) => {
- const args = [
- '-workspace',
- config.workspace,
- '-scheme',
- config.scheme,
- '-derivedDataPath',
- config.derivedDataPath,
- '-destination',
- 'generic/platform=iphoneos',
- '-destination',
- 'generic/platform=iphonesimulator',
- '-configuration',
- config.buildType.charAt(0).toUpperCase() + config.buildType.slice(1),
- ];
- if (config.dryRun) {
- console.log(`xcodebuild ${args.join(' ')}`);
- return;
- }
- return (0, utils_1.withSpinner)({
- operation: () => (0, utils_1.runCommand)('xcodebuild', args, { verbose: config.verbose }),
- loaderMessage: 'Compiling framework...',
- successMessage: 'Compiling framework succeeded',
- errorMessage: 'Compiling framework failed',
- verbose: config.verbose,
- });
-};
-const packageFrameworks = async (config) => {
- const args = [
- '-create-xcframework',
- '-framework',
- `${config.device}/${config.scheme}.framework`,
- '-framework',
- `${config.simulator}/${config.scheme}.framework`,
- '-output',
- `${config.artifacts}/${config.scheme}.xcframework`,
- ];
- if (config.dryRun) {
- console.log(`xcodebuild ${args.join(' ')}`);
- return;
- }
- return (0, utils_1.withSpinner)({
- operation: () => (0, utils_1.runCommand)('xcodebuild', args, { verbose: config.verbose }),
- loaderMessage: 'Packaging framework into an XCFramework...',
- successMessage: 'Packaging framework into an XCFramework succeeded',
- errorMessage: 'Packaging framework into an XCFramework failed',
- verbose: config.verbose,
- });
-};
-const copyHermesFramework = async (config) => {
- if (config.dryRun) {
- console.log(`Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermesvm.xcframework`);
- return;
- }
- return (0, utils_1.withSpinner)({
- operation: () => promises_1.default.cp(`./ios/${config.hermesFrameworkPath}`, `${config.artifacts}/hermesvm.xcframework`, {
- force: true,
- recursive: true,
- }),
- loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...',
- successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded',
- errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed',
- verbose: config.verbose,
- });
-};
diff --git a/packages/expo-brownfield/cli/build/commands/ios/index.d.ts b/packages/expo-brownfield/cli/build/commands/ios/index.d.ts
deleted file mode 100644
index 49066d174ec4b9..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/ios/index.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as runBuildIos } from './build';
diff --git a/packages/expo-brownfield/cli/build/commands/ios/index.js b/packages/expo-brownfield/cli/build/commands/ios/index.js
deleted file mode 100644
index 3c4d7743b46bb5..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/ios/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.runBuildIos = void 0;
-var build_1 = require("./build");
-Object.defineProperty(exports, "runBuildIos", { enumerable: true, get: function () { return __importDefault(build_1).default; } });
diff --git a/packages/expo-brownfield/cli/build/commands/resolve.d.ts b/packages/expo-brownfield/cli/build/commands/resolve.d.ts
deleted file mode 100644
index 6ccc26c502aa7b..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/resolve.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import type { AndroidCommand, CommandEntry } from './types';
-export declare const resolveCommand: () => CommandEntry;
-export declare const resolveAndroid: (command: AndroidCommand) => CommandEntry;
-export declare const resolveIos: () => CommandEntry;
diff --git a/packages/expo-brownfield/cli/build/commands/resolve.js b/packages/expo-brownfield/cli/build/commands/resolve.js
deleted file mode 100644
index 3ebd51be318513..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/resolve.js
+++ /dev/null
@@ -1,35 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.resolveIos = exports.resolveAndroid = exports.resolveCommand = void 0;
-const constants_1 = require("../constants");
-const utils_1 = require("../utils");
-const commands_1 = require("./commands");
-const resolveCommand = () => {
- const args = (0, utils_1.parseArgs)({ spec: constants_1.Args.General, stopAtPositional: true });
- if (args['--help']) {
- return commands_1.Commands.help;
- }
- if (args['--version']) {
- return commands_1.Commands.version;
- }
- if (!args['_']?.length) {
- return commands_1.Commands.help;
- }
- const command = (0, utils_1.getCommand)(args);
- if (command === 'build:android' || command === 'tasks:android') {
- return (0, exports.resolveAndroid)(command);
- }
- if (command === 'build:ios') {
- return (0, exports.resolveIos)();
- }
- return constants_1.Errors.unknownCommand();
-};
-exports.resolveCommand = resolveCommand;
-const resolveAndroid = (command) => {
- return commands_1.Commands[command];
-};
-exports.resolveAndroid = resolveAndroid;
-const resolveIos = () => {
- return commands_1.Commands['build:ios'];
-};
-exports.resolveIos = resolveIos;
diff --git a/packages/expo-brownfield/cli/build/commands/tasks-android.d.ts b/packages/expo-brownfield/cli/build/commands/tasks-android.d.ts
new file mode 100644
index 00000000000000..662fb87f08c83e
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/commands/tasks-android.d.ts
@@ -0,0 +1,3 @@
+import type { Command } from 'commander';
+declare const tasksAndroid: (command: Command) => Promise;
+export default tasksAndroid;
diff --git a/packages/expo-brownfield/cli/build/commands/tasks-android.js b/packages/expo-brownfield/cli/build/commands/tasks-android.js
new file mode 100644
index 00000000000000..a7bd9859bf2050
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/commands/tasks-android.js
@@ -0,0 +1,36 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const chalk_1 = __importDefault(require("chalk"));
+const node_path_1 = __importDefault(require("node:path"));
+const utils_1 = require("../utils");
+const tasksAndroid = async (command) => {
+ await (0, utils_1.validatePrebuild)('android');
+ const config = (0, utils_1.resolveTasksConfigAndroid)(command.opts());
+ const { stdout } = await (0, utils_1.withSpinner)({
+ operation: () => (0, utils_1.runCommand)('./gradlew', [`${config.library}:tasks`, '--group', 'publishing'], {
+ cwd: node_path_1.default.join(process.cwd(), 'android'),
+ verbose: config.verbose,
+ }),
+ loaderMessage: 'Reading publish tasks from the android project...',
+ successMessage: 'Successfully read publish tasks from the android project\n',
+ errorMessage: 'Failed to read publish tasks from the android project',
+ verbose: config.verbose,
+ });
+ // Forwarded stdout already contains the tasks
+ if (config.verbose) {
+ return;
+ }
+ console.log(chalk_1.default.bold('Publishing tasks'));
+ const tasks = (0, utils_1.processTasks)(stdout);
+ tasks.forEach((task) => {
+ console.log(` - ${chalk_1.default.blue(task)}`);
+ });
+ console.log(chalk_1.default.bold('Repositories'));
+ (0, utils_1.processRepositories)(tasks).forEach((repository) => {
+ console.log(` - ${chalk_1.default.blue(repository)}`);
+ });
+};
+exports.default = tasksAndroid;
diff --git a/packages/expo-brownfield/cli/build/commands/types.d.ts b/packages/expo-brownfield/cli/build/commands/types.d.ts
deleted file mode 100644
index a27e7d32f164ee..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/types.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type GeneralCommand = 'help' | 'version';
-export type AndroidCommand = 'build:android' | 'tasks:android';
-export type IosCommand = 'build:ios';
-export type Command = GeneralCommand | AndroidCommand | IosCommand;
-export interface CommandEntry {
- run: () => Promise;
-}
-export type CommandsMap = Record;
diff --git a/packages/expo-brownfield/cli/build/commands/types.js b/packages/expo-brownfield/cli/build/commands/types.js
deleted file mode 100644
index c8ad2e549bdc68..00000000000000
--- a/packages/expo-brownfield/cli/build/commands/types.js
+++ /dev/null
@@ -1,2 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
diff --git a/packages/expo-brownfield/cli/build/constants/args.d.ts b/packages/expo-brownfield/cli/build/constants/args.d.ts
deleted file mode 100644
index c64f971143968c..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/args.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { type Spec } from 'arg';
-/**
- * CLI arguments
- */
-export declare const Args: Record;
diff --git a/packages/expo-brownfield/cli/build/constants/args.js b/packages/expo-brownfield/cli/build/constants/args.js
deleted file mode 100644
index 4040d5358ac4cd..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/args.js
+++ /dev/null
@@ -1,86 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.Args = void 0;
-const arg_1 = __importDefault(require("arg"));
-/**
- * General CLI arguments
- */
-const generalArgs = {
- // Types
- '--help': arg_1.default.COUNT,
- '--version': arg_1.default.COUNT,
- // Aliases
- '-h': '--help',
- '-v': '--version',
-};
-/**
- * Common build arguments shared by Android and iOS
- */
-const buildCommonArgs = {
- // Types
- '--debug': arg_1.default.COUNT,
- '--dry-run': arg_1.default.COUNT,
- '--help': arg_1.default.COUNT,
- '--release': arg_1.default.COUNT,
- '--verbose': arg_1.default.COUNT,
- // Aliases
- '-d': '--debug',
- '-h': '--help',
- '-r': '--release',
-};
-/**
- * Android build arguments
- */
-const buildAndroidArgs = {
- // Inherited
- ...buildCommonArgs,
- // Types
- '--all': arg_1.default.COUNT,
- '--library': String,
- '--repository': [String],
- '--task': [String],
- // Aliases
- '-a': '--all',
- '-l': '--library',
- '--repo': '--repository',
- '-t': '--task',
-};
-/**
- * Android tasks arguments
- */
-const tasksAndroidArgs = {
- // Types
- '--help': arg_1.default.COUNT,
- '--library': String,
- '--verbose': arg_1.default.COUNT,
- // Aliases
- '-h': '--help',
- '-l': '--library',
-};
-/**
- * iOS build arguments
- */
-const buildIosArgs = {
- // Inherited
- ...buildCommonArgs,
- // Types
- '--artifacts': String,
- '--scheme': String,
- '--xcworkspace': String,
- // Aliases
- '-a': '--artifacts',
- '-s': '--scheme',
- '-x': '--xcworkspace',
-};
-/**
- * CLI arguments
- */
-exports.Args = {
- Android: buildAndroidArgs,
- General: generalArgs,
- IOS: buildIosArgs,
- TasksAndroid: tasksAndroidArgs,
-};
diff --git a/packages/expo-brownfield/cli/build/constants/defaults.d.ts b/packages/expo-brownfield/cli/build/constants/defaults.d.ts
deleted file mode 100644
index 5fb1f6977bfdeb..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/defaults.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export declare const Defaults: {
- readonly artifactsPath: "./artifacts";
- readonly hermesFrameworkPath: "Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework";
- readonly libraryName: "brownfield";
-};
diff --git a/packages/expo-brownfield/cli/build/constants/defaults.js b/packages/expo-brownfield/cli/build/constants/defaults.js
deleted file mode 100644
index f41c1cec1a41c4..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/defaults.js
+++ /dev/null
@@ -1,8 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.Defaults = void 0;
-exports.Defaults = {
- artifactsPath: './artifacts',
- hermesFrameworkPath: 'Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework',
- libraryName: 'brownfield',
-};
diff --git a/packages/expo-brownfield/cli/build/constants/errors.d.ts b/packages/expo-brownfield/cli/build/constants/errors.d.ts
deleted file mode 100644
index feea8b6ba5c92b..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/errors.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { ArgError } from 'arg';
-export declare const Errors: {
- readonly additionalCommand: (command: string) => never;
- readonly generic: (error: unknown) => never;
- readonly inference: (valueName: string) => never;
- readonly missingTasksOrRepositories: () => never;
- readonly parseArgs: () => never;
- readonly unknownCommand: () => never;
- readonly unknownOption: (argError: ArgError) => never;
-};
diff --git a/packages/expo-brownfield/cli/build/constants/errors.js b/packages/expo-brownfield/cli/build/constants/errors.js
deleted file mode 100644
index a1f9e320eb0938..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/errors.js
+++ /dev/null
@@ -1,71 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.Errors = void 0;
-const additionalCommandError = (command) => {
- console.error(`Error: Command ${command} doesn't support additional commands
-For all available options please use the help command:
-npx expo-brownfield ${command} --help`);
- return process.exit(1);
-};
-/**
- * Prints a generic error message.
- *
- * @param error - The error object.
- */
-const genericError = (error) => {
- if (error instanceof Error) {
- console.error(`Error: ${error.message}`);
- }
- else {
- console.error('Error: An unknown error occurred');
- }
- return process.exit(1);
-};
-/**
- * Prints the error message for failed inference.
- */
-const inferenceError = (valueName) => {
- console.error(`Error: Value of ${valueName} could not be inferred from the project`);
- return process.exit(1);
-};
-/**
- * Prints the error message for missing tasks or repositories.
- */
-const missingTasksOrRepositoriesError = () => {
- console.error('Error: At least one task or repository must be specified');
- return process.exit(1);
-};
-/**
- * Prints the error message for failed argument parsing.
- */
-const parseArgsError = () => {
- console.error('Error: failed to parse arguments');
- return process.exit(1);
-};
-/**
- * Prints the error message for an unknown command.
- */
-const unknownCommandError = () => {
- console.error(`Error: unknown command
-Supported commands: build:android, build:ios, tasks:android`);
- return process.exit(1);
-};
-/**
- * Prints the error message for an unknown option.
- *
- * @param argError - The error object.
- */
-const unkownOptionError = (argError) => {
- const message = argError.message.replace('ArgError: ', '');
- console.error(`Error: ${message}`);
- return process.exit(1);
-};
-exports.Errors = {
- additionalCommand: additionalCommandError,
- generic: genericError,
- inference: inferenceError,
- missingTasksOrRepositories: missingTasksOrRepositoriesError,
- parseArgs: parseArgsError,
- unknownCommand: unknownCommandError,
- unknownOption: unkownOptionError,
-};
diff --git a/packages/expo-brownfield/cli/build/constants/help.d.ts b/packages/expo-brownfield/cli/build/constants/help.d.ts
deleted file mode 100644
index 3c8f80ce4db7ff..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/help.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Help messages
- */
-export declare const Help: {
- readonly Android: string;
- readonly General: string;
- readonly IOS: string;
- readonly TasksAndroid: string;
-};
diff --git a/packages/expo-brownfield/cli/build/constants/help.js b/packages/expo-brownfield/cli/build/constants/help.js
deleted file mode 100644
index 9c42d4701b952f..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/help.js
+++ /dev/null
@@ -1,142 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.Help = void 0;
-const utils_1 = require("../utils");
-/**
- * Helper function to create a help option for a command.
- * @param command - The command to display help for.
- * @returns The help option.
- */
-const helpOption = (command = 'command') => ({
- description: `display help for ${command}`,
- option: '--help',
- short: '-h',
-});
-/**
- * Common build options for Android and iOS.
- */
-const commonBuildOptions = [
- {
- description: 'build in debug configuration',
- option: '--debug',
- short: '-d',
- },
- {
- description: 'build in release configuration',
- option: '--release',
- short: '-r',
- },
- {
- description: 'forward all output to the terminal',
- option: '--verbose',
- },
-];
-/**
- * General help message
- */
-const generalHelp = (0, utils_1.helpMessage)({
- commands: [
- {
- command: 'build:android',
- description: 'build and publish Android brownfield artifacts',
- hasOptions: true,
- },
- {
- command: 'build:ios',
- description: 'build iOS brownfield artifacts',
- hasOptions: true,
- },
- {
- command: 'tasks:android',
- description: 'list available publishing tasks and repositories for android',
- hasOptions: true,
- },
- ],
- options: [
- {
- description: 'output the version number',
- option: '--version',
- short: '-v',
- },
- helpOption(),
- ],
-});
-/**
- * Help message for 'build:android' command
- */
-const buildAndroidHelp = (0, utils_1.helpMessage)({
- promptCommand: 'build:android',
- options: [
- helpOption("'build:android'"),
- ...commonBuildOptions,
- {
- description: 'build both debug and release configurations',
- option: '--all',
- short: '-a',
- },
- {
- description: 'maven repository for publishing artifacts (multiple can be passed)',
- option: '--repository',
- short: '--repo',
- },
- {
- description: 'publishing task to be run (multiple can be passed)',
- option: '--task',
- short: '-t',
- },
- {
- description: 'name of the brownfield library',
- option: '--library',
- short: '-l',
- },
- ],
-});
-const tasksAndroidHelp = (0, utils_1.helpMessage)({
- promptCommand: 'tasks:android',
- options: [
- helpOption("'tasks:android'"),
- {
- description: 'output all subcommands output to the terminal',
- option: '--verbose',
- },
- {
- description: 'name of the brownfield library',
- option: '--library',
- short: '-l',
- },
- ],
-});
-/**
- * Help message for 'build:ios' command
- */
-const buildIosHelp = (0, utils_1.helpMessage)({
- promptCommand: 'build:ios',
- options: [
- helpOption("'build:ios'"),
- ...commonBuildOptions,
- {
- description: 'path to artifacts directory',
- option: '--artifacts',
- short: '-a',
- },
- {
- description: 'scheme to be built',
- option: '--scheme',
- short: '-s',
- },
- {
- description: 'path to Xcode workspace (.xcworkspace)',
- option: '--xcworkspace',
- short: '-x',
- },
- ],
-});
-/**
- * Help messages
- */
-exports.Help = {
- Android: buildAndroidHelp,
- General: generalHelp,
- IOS: buildIosHelp,
- TasksAndroid: tasksAndroidHelp,
-};
diff --git a/packages/expo-brownfield/cli/build/constants/index.d.ts b/packages/expo-brownfield/cli/build/constants/index.d.ts
deleted file mode 100644
index 1530f9ad068a95..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/index.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './args';
-export * from './defaults';
-export * from './errors';
-export * from './help';
-export * from './output';
diff --git a/packages/expo-brownfield/cli/build/constants/index.js b/packages/expo-brownfield/cli/build/constants/index.js
deleted file mode 100644
index 1afae57c73bf2f..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-"use strict";
-var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- var desc = Object.getOwnPropertyDescriptor(m, k);
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
- desc = { enumerable: true, get: function() { return m[k]; } };
- }
- Object.defineProperty(o, k2, desc);
-}) : (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- o[k2] = m[k];
-}));
-var __exportStar = (this && this.__exportStar) || function(m, exports) {
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-__exportStar(require("./args"), exports);
-__exportStar(require("./defaults"), exports);
-__exportStar(require("./errors"), exports);
-__exportStar(require("./help"), exports);
-__exportStar(require("./output"), exports);
diff --git a/packages/expo-brownfield/cli/build/constants/output.d.ts b/packages/expo-brownfield/cli/build/constants/output.d.ts
deleted file mode 100644
index 0de2805ebf9763..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/output.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Constants for output formatting.
- */
-export declare const Output: {
- /**
- * The spacing between the beginning of the help line and the description
- */
- readonly HelpSpacing: 30;
-};
diff --git a/packages/expo-brownfield/cli/build/constants/output.js b/packages/expo-brownfield/cli/build/constants/output.js
deleted file mode 100644
index 3bbd5875831c1f..00000000000000
--- a/packages/expo-brownfield/cli/build/constants/output.js
+++ /dev/null
@@ -1,12 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.Output = void 0;
-/**
- * Constants for output formatting.
- */
-exports.Output = {
- /**
- * The spacing between the beginning of the help line and the description
- */
- HelpSpacing: 30,
-};
diff --git a/packages/expo-brownfield/cli/build/index.js b/packages/expo-brownfield/cli/build/index.js
index 08e71c4f5e5a81..01bffcdb826353 100644
--- a/packages/expo-brownfield/cli/build/index.js
+++ b/packages/expo-brownfield/cli/build/index.js
@@ -1,8 +1,52 @@
#!/usr/bin/env node
"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
Object.defineProperty(exports, "__esModule", { value: true });
-const resolve_1 = require("./commands/resolve");
-(async () => {
- const command = (0, resolve_1.resolveCommand)();
- await command.run();
-})();
+const commander_1 = require("commander");
+const commands_1 = require("./commands");
+const package_json_1 = __importDefault(require("../../package.json"));
+const program = new commander_1.Command();
+// main program
+program.name('expo-brownfield').version(package_json_1.default.version, '-v, --version');
+// build:android
+program
+ .command('build:android')
+ .description('Build and publish Android brownfield artifacts')
+ .option('-d, --debug', 'build debug variant')
+ .option('-r, --release', 'build release variant')
+ .option('-a, --all', 'build both debug and release variants')
+ .option('--verbose', 'forward all output to the terminal')
+ .option('-l, --library ', 'name of the brownfield library')
+ .option('-t, --task ', 'publishing task to be run (multiple can be passed)')
+ .option('--repo, --repository ', 'repository to publish to (multiple can be passed)')
+ .option('--dry-run', 'only print the commands without executing them')
+ .action(async function () {
+ await (0, commands_1.buildAndroid)(this);
+});
+// build:ios
+program
+ .command('build:ios')
+ .description('Build and publish iOS brownfield artifacts')
+ .option('-d, --debug', 'build debug configuration')
+ .option('-r, --release', 'build release configuration')
+ .option('--verbose', 'forward all output to the terminal')
+ .option('-s, --scheme ', 'name of the iOS scheme')
+ .option('-x, --xcworkspace ', 'path to the Xcode workspace (.xcworkspace)')
+ .option('-a, --artifacts ', 'path to the artifacts directory')
+ .option('--dry-run', 'only print the commands without executing them')
+ .action(async function () {
+ await (0, commands_1.buildIos)(this);
+});
+// tasks:android
+program
+ .command('tasks:android')
+ .description('List available publishing tasks and repositories for Android')
+ .option('--verbose', 'forward all output to the terminal')
+ .option('-l, --library ', 'name of the brownfield library')
+ .option('--dry-run', 'only print the commands without executing them')
+ .action(async function () {
+ await (0, commands_1.tasksAndroid)(this);
+});
+program.parse();
diff --git a/packages/expo-brownfield/cli/build/utils/android.d.ts b/packages/expo-brownfield/cli/build/utils/android.d.ts
new file mode 100644
index 00000000000000..9afe73a97a81dd
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/android.d.ts
@@ -0,0 +1,7 @@
+import type { AndroidConfig, BuildVariant } from './types';
+export declare const buildPublishingTask: (variant: BuildVariant, repository: string) => string;
+export declare const findBrownfieldLibrary: () => string | undefined;
+export declare const printAndroidConfig: (config: AndroidConfig) => void;
+export declare const processRepositories: (tasks: string[]) => string[];
+export declare const processTasks: (stdout: string) => string[];
+export declare const runTask: (task: string, verbose: boolean, dryRun: boolean) => Promise;
diff --git a/packages/expo-brownfield/cli/build/utils/android.js b/packages/expo-brownfield/cli/build/utils/android.js
new file mode 100644
index 00000000000000..f4dda4a5c1ec7c
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/android.js
@@ -0,0 +1,91 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.runTask = exports.processTasks = exports.processRepositories = exports.printAndroidConfig = exports.findBrownfieldLibrary = exports.buildPublishingTask = void 0;
+const chalk_1 = __importDefault(require("chalk"));
+const node_fs_1 = __importDefault(require("node:fs"));
+const node_path_1 = __importDefault(require("node:path"));
+const commands_1 = require("./commands");
+const error_1 = __importDefault(require("./error"));
+const spinner_1 = require("./spinner");
+const buildPublishingTask = (variant, repository) => {
+ const repositoryName = repository === 'MavenLocal' ? repository : `${repository}Repository`;
+ return `publishBrownfield${variant}PublicationTo${repositoryName}`;
+};
+exports.buildPublishingTask = buildPublishingTask;
+const findBrownfieldLibrary = () => {
+ try {
+ const androidPath = node_path_1.default.join(process.cwd(), 'android');
+ if (!node_fs_1.default.existsSync(androidPath)) {
+ error_1.default.handle('android-directory-not-found');
+ }
+ const subdirectories = node_fs_1.default
+ .readdirSync(androidPath, { withFileTypes: true })
+ .filter((item) => item.isDirectory());
+ const brownfieldLibrary = subdirectories.find((directory) => {
+ const directoryPath = node_path_1.default.join(androidPath, directory.name);
+ const files = node_fs_1.default.readdirSync(directoryPath, { recursive: true });
+ return files.some((file) => typeof file === 'string' && file.endsWith('ReactNativeHostManager.kt'));
+ });
+ if (brownfieldLibrary) {
+ return brownfieldLibrary.name;
+ }
+ error_1.default.handle('android-library-not-found');
+ }
+ catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ error_1.default.handle('android-library-unknown-error', errorMessage);
+ }
+};
+exports.findBrownfieldLibrary = findBrownfieldLibrary;
+const printAndroidConfig = (config) => {
+ console.log(chalk_1.default.bold('Resolved build configuration'));
+ console.log(` - Build variant: ${chalk_1.default.blue(config.variant)}`);
+ console.log(` - Library: ${chalk_1.default.blue(config.library)}`);
+ console.log(` - Verbose: ${chalk_1.default.blue(config.verbose)}`);
+ console.log(` - Dry run: ${chalk_1.default.blue(config.dryRun)}`);
+ console.log(` - Tasks:`);
+ config.tasks.forEach((task) => {
+ console.log(` - ${chalk_1.default.blue(task)}`);
+ });
+ console.log();
+};
+exports.printAndroidConfig = printAndroidConfig;
+const processRepositories = (tasks) => {
+ const splitRegex = /^publishBrownfield(?:All|Debug|Release)PublicationTo(.+?)(?:Repository)?$/;
+ return Array.from(new Set(tasks
+ .map((task) => {
+ return splitRegex.exec(task)?.[1];
+ })
+ .filter((repo) => repo !== undefined)));
+};
+exports.processRepositories = processRepositories;
+const processTasks = (stdout) => {
+ const regex = /^publishBrownfield[a-zA-Z0-9_-]*/i;
+ return (stdout
+ .split('\n')
+ .map((line) => regex.exec(line)?.[0])
+ // Remove duplicate maven local tasks
+ .filter((task) => task !== undefined)
+ .filter((task) => !task.includes('MavenLocalRepository')));
+};
+exports.processTasks = processTasks;
+const runTask = async (task, verbose, dryRun) => {
+ if (dryRun) {
+ console.log(`./gradlew ${task}`);
+ return;
+ }
+ return (0, spinner_1.withSpinner)({
+ operation: () => (0, commands_1.runCommand)('./gradlew', [task], {
+ cwd: node_path_1.default.join(process.cwd(), 'android'),
+ verbose,
+ }),
+ loaderMessage: 'Running task: ' + task,
+ successMessage: 'Running task: ' + task + ' succeeded',
+ errorMessage: 'Running task: ' + task + ' failed',
+ verbose,
+ });
+};
+exports.runTask = runTask;
diff --git a/packages/expo-brownfield/cli/build/utils/args.d.ts b/packages/expo-brownfield/cli/build/utils/args.d.ts
deleted file mode 100644
index 1f7f4610d4fa2f..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/args.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import arg, { type Result, type Spec } from 'arg';
-import type { ParseArgsParams } from './types';
-export declare const parseArgs: ({ spec, argv, stopAtPositional }: ParseArgsParams) => arg.Result;
-export declare const getCommand: (args: Result) => string;
diff --git a/packages/expo-brownfield/cli/build/utils/args.js b/packages/expo-brownfield/cli/build/utils/args.js
deleted file mode 100644
index 8090d534746324..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/args.js
+++ /dev/null
@@ -1,28 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.getCommand = exports.parseArgs = void 0;
-const arg_1 = __importDefault(require("arg"));
-const constants_1 = require("../constants");
-const parseArgs = ({ spec, argv, stopAtPositional }) => {
- try {
- const parsed = (0, arg_1.default)(spec, { argv, stopAtPositional });
- return parsed;
- }
- catch (error) {
- if (error instanceof arg_1.default.ArgError) {
- return constants_1.Errors.unknownOption(error);
- }
- return constants_1.Errors.generic(error);
- }
-};
-exports.parseArgs = parseArgs;
-const getCommand = (args) => {
- if ('_' in args && args['_'].length > 0) {
- return args['_'][0];
- }
- return '';
-};
-exports.getCommand = getCommand;
diff --git a/packages/expo-brownfield/cli/build/utils/build.d.ts b/packages/expo-brownfield/cli/build/utils/build.d.ts
deleted file mode 100644
index c5eb39bc990142..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/build.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { BuildConfigAndroid, BuildConfigIos, WithSpinnerParams } from './types';
-export declare const printConfig: (config: BuildConfigAndroid | BuildConfigIos) => void;
-export declare const withSpinner: ({ operation, loaderMessage, successMessage, errorMessage, onError, verbose, }: WithSpinnerParams) => Promise;
-export declare const checkPrebuild: (platform: "android" | "ios") => Promise;
-export declare const maybeRunPrebuild: (platform: "android" | "ios") => Promise;
-export declare const ensurePrebuild: (platform: "android" | "ios") => Promise;
diff --git a/packages/expo-brownfield/cli/build/utils/build.js b/packages/expo-brownfield/cli/build/utils/build.js
deleted file mode 100644
index 1c10228395047e..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/build.js
+++ /dev/null
@@ -1,99 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.ensurePrebuild = exports.maybeRunPrebuild = exports.checkPrebuild = exports.withSpinner = exports.printConfig = void 0;
-const chalk_1 = __importDefault(require("chalk"));
-const promises_1 = __importDefault(require("node:fs/promises"));
-const ora_1 = __importDefault(require("ora"));
-const path_1 = __importDefault(require("path"));
-const prompts_1 = __importDefault(require("prompts"));
-const commands_1 = require("./commands");
-const constants_1 = require("../constants");
-const isBuildConfigAndroid = (config) => {
- return 'libraryName' in config;
-};
-const printConfig = (config) => {
- console.log(chalk_1.default.bold('Build configuration:'));
- console.log(`- Verbose: ${config.verbose}`);
- if (isBuildConfigAndroid(config)) {
- console.log(`- Build type: ${config.buildType.charAt(0).toUpperCase() + config.buildType.slice(1)}`);
- console.log(`- Brownfield library: ${config.libraryName}`);
- console.log(`- Repositories: ${config.repositories.length > 0 ? config.repositories.join(', ') : '[]'}`);
- console.log(`- Tasks: ${config.tasks.length > 0 ? config.tasks.join(', ') : '[]'}`);
- }
- else {
- console.log(`- Artifacts directory: ${config.artifacts}`);
- console.log(`- Build type: ${config.buildType.charAt(0).toUpperCase() + config.buildType.slice(1)}`);
- console.log(`- Xcode Scheme: ${config.scheme}`);
- console.log(`- Xcode Workspace: ${config.workspace}`);
- }
- console.log('');
-};
-exports.printConfig = printConfig;
-const withSpinner = async ({ operation, loaderMessage, successMessage, errorMessage, onError = 'error', verbose = false, }) => {
- let spinner;
- try {
- if (!verbose) {
- spinner = (0, ora_1.default)(loaderMessage).start();
- }
- const result = await operation();
- if (!verbose) {
- spinner?.succeed(successMessage);
- }
- return result;
- }
- catch (error) {
- if (!verbose) {
- onError === 'error' ? spinner?.fail(errorMessage) : spinner?.warn(errorMessage);
- }
- return constants_1.Errors.generic(error);
- }
- finally {
- if (!verbose && spinner?.isSpinning) {
- spinner?.stop();
- }
- }
-};
-exports.withSpinner = withSpinner;
-const checkPrebuild = async (platform) => {
- const nativeProjectPath = path_1.default.join(process.cwd(), platform);
- try {
- await promises_1.default.access(nativeProjectPath);
- }
- catch (error) {
- return false;
- }
- return true;
-};
-exports.checkPrebuild = checkPrebuild;
-const maybeRunPrebuild = async (platform) => {
- console.info(`${chalk_1.default.yellow('⚠')} Prebuild for platform: ${platform} is missing`);
- const response = await (0, prompts_1.default)({
- type: 'confirm',
- name: 'shouldRunPrebuild',
- message: 'Do you want to run the prebuild now?',
- initial: false,
- });
- if (response.shouldRunPrebuild) {
- return (0, exports.withSpinner)({
- operation: () => (0, commands_1.runCommand)('npx', ['expo', 'prebuild', '--platform', platform]),
- loaderMessage: `Running 'npx expo prebuild' for platform: ${platform}...`,
- successMessage: `Prebuild for ${platform} completed\n`,
- errorMessage: `Prebuild for ${platform} failed`,
- verbose: false,
- });
- }
- else {
- console.error(`${chalk_1.default.red('✖')} Brownfield cannot be built without prebuild`);
- return process.exit(1);
- }
-};
-exports.maybeRunPrebuild = maybeRunPrebuild;
-const ensurePrebuild = async (platform) => {
- if (!(await (0, exports.checkPrebuild)(platform))) {
- await (0, exports.maybeRunPrebuild)(platform);
- }
-};
-exports.ensurePrebuild = ensurePrebuild;
diff --git a/packages/expo-brownfield/cli/build/utils/config.d.ts b/packages/expo-brownfield/cli/build/utils/config.d.ts
index 6023cf50a6f8fe..611e0a801c6c9f 100644
--- a/packages/expo-brownfield/cli/build/utils/config.d.ts
+++ b/packages/expo-brownfield/cli/build/utils/config.d.ts
@@ -1,13 +1,5 @@
-import type { Result, Spec } from 'arg';
-import type { BuildConfigAndroid, BuildConfigCommon, BuildConfigIos, BuildTypeAndroid, BuildTypeCommon } from './types';
-export declare const getCommonConfig: (args: Result) => BuildConfigCommon;
-export declare const getAndroidConfig: (args: Result) => Promise;
-export declare const getIosConfig: (args: Result) => Promise;
-export declare const getTasksAndroidConfig: (args: Result) => Promise<{
- libraryName: string;
- dryRun: boolean;
- help: boolean;
- verbose: boolean;
-}>;
-export declare const getBuildTypeCommon: (args: Result) => BuildTypeCommon;
-export declare const getBuildTypeAndroid: (args: Result) => BuildTypeAndroid;
+import type { OptionValues } from 'commander';
+import type { AndroidConfig, IosConfig, TasksConfigAndroid } from './types';
+export declare const resolveBuildConfigAndroid: (options: OptionValues) => AndroidConfig;
+export declare const resolveBuildConfigIos: (options: OptionValues) => IosConfig;
+export declare const resolveTasksConfigAndroid: (options: OptionValues) => TasksConfigAndroid;
diff --git a/packages/expo-brownfield/cli/build/utils/config.js b/packages/expo-brownfield/cli/build/utils/config.js
index 1c866dd11d68c2..f71b0f16fb8b41 100644
--- a/packages/expo-brownfield/cli/build/utils/config.js
+++ b/packages/expo-brownfield/cli/build/utils/config.js
@@ -3,62 +3,89 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
-exports.getBuildTypeAndroid = exports.getBuildTypeCommon = exports.getTasksAndroidConfig = exports.getIosConfig = exports.getAndroidConfig = exports.getCommonConfig = void 0;
-const path_1 = __importDefault(require("path"));
-const constants_1 = require("../constants");
-const infer_1 = require("./infer");
-const getCommonConfig = (args) => {
+exports.resolveTasksConfigAndroid = exports.resolveBuildConfigIos = exports.resolveBuildConfigAndroid = void 0;
+const node_path_1 = __importDefault(require("node:path"));
+const android_1 = require("./android");
+const ios_1 = require("./ios");
+const resolveBuildConfigAndroid = (options) => {
+ const variant = resolveVariant(options);
return {
- dryRun: !!args['--dry-run'],
- help: !!args['--help'],
- verbose: !!args['--verbose'],
+ ...resolveCommonConfig(options),
+ library: resolveLibrary(options),
+ tasks: resolveTaskArray(options, variant),
+ variant,
};
};
-exports.getCommonConfig = getCommonConfig;
-const getAndroidConfig = async (args) => {
+exports.resolveBuildConfigAndroid = resolveBuildConfigAndroid;
+const resolveBuildConfigIos = (options) => {
+ let artifacts = options.artifacts || './artifacts';
+ if (!node_path_1.default.isAbsolute(artifacts)) {
+ artifacts = node_path_1.default.join(process.cwd(), artifacts);
+ }
+ const derivedDataPath = node_path_1.default.join(process.cwd(), 'ios/build');
+ const buildProductsPath = node_path_1.default.join(derivedDataPath, 'Build/Products');
+ const buildConfiguration = resolveBuildConfiguration(options);
+ const device = node_path_1.default.join(buildProductsPath, `${buildConfiguration.toLowerCase()}-iphoneos`);
+ const simulator = node_path_1.default.join(buildProductsPath, `${buildConfiguration.toLowerCase()}-iphonesimulator`);
+ const hermesFrameworkPath = 'Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework';
return {
- ...(0, exports.getCommonConfig)(args),
- buildType: (0, exports.getBuildTypeAndroid)(args),
- libraryName: args['--library'] || (await (0, infer_1.inferAndroidLibrary)()),
- repositories: args['--repository'] || [],
- tasks: args['--task'] || [],
+ ...resolveCommonConfig(options),
+ artifacts,
+ buildConfiguration,
+ derivedDataPath,
+ device,
+ simulator,
+ hermesFrameworkPath,
+ scheme: resolveScheme(options),
+ workspace: resolveWorkspace(options),
};
};
-exports.getAndroidConfig = getAndroidConfig;
-const getIosConfig = async (args) => {
- const buildType = (0, exports.getBuildTypeCommon)(args);
- const derivedDataPath = path_1.default.join(process.cwd(), 'ios/build');
- const buildProductsPath = path_1.default.join(derivedDataPath, 'Build/Products');
+exports.resolveBuildConfigIos = resolveBuildConfigIos;
+const resolveTasksConfigAndroid = (options) => {
return {
- ...(0, exports.getCommonConfig)(args),
- artifacts: path_1.default.join(process.cwd(), args['--artifacts'] || constants_1.Defaults.artifactsPath),
- buildType,
- derivedDataPath,
- device: path_1.default.join(buildProductsPath, `${buildType}-iphoneos`),
- simulator: path_1.default.join(buildProductsPath, `${buildType}-iphonesimulator`),
- hermesFrameworkPath: args['--hermes-framework'] || constants_1.Defaults.hermesFrameworkPath,
- scheme: args['--scheme'] || (await (0, infer_1.inferScheme)()),
- workspace: args['--xcworkspace'] || (await (0, infer_1.inferXCWorkspace)()),
+ ...resolveCommonConfig(options),
+ library: resolveLibrary(options),
};
};
-exports.getIosConfig = getIosConfig;
-const getTasksAndroidConfig = async (args) => {
- const commonConfig = (0, exports.getCommonConfig)(args);
- const libraryName = !commonConfig.help ? args['--library'] || (await (0, infer_1.inferAndroidLibrary)()) : '';
+exports.resolveTasksConfigAndroid = resolveTasksConfigAndroid;
+const resolveCommonConfig = (options) => {
return {
- ...commonConfig,
- libraryName,
+ dryRun: !!options.dryRun,
+ verbose: !!options.verbose,
};
};
-exports.getTasksAndroidConfig = getTasksAndroidConfig;
-const getBuildTypeCommon = (args) => {
- return !args['--release'] && args['--debug'] ? 'debug' : 'release';
+// SECTION: Android Helpers
+const resolveLibrary = (options) => {
+ return options.library || (0, android_1.findBrownfieldLibrary)();
};
-exports.getBuildTypeCommon = getBuildTypeCommon;
-const getBuildTypeAndroid = (args) => {
- if ((args['--debug'] && args['--release']) || (!args['--debug'] && !args['--release'])) {
- return 'all';
+const resolveTaskArray = (options, variant) => {
+ const tasks = options.task ?? [];
+ const repoTasks = (options.repository ?? []).map((repo) => (0, android_1.buildPublishingTask)(variant, repo));
+ return Array.from(new Set([...tasks, ...repoTasks]));
+};
+const resolveVariant = (options) => {
+ let variant = 'All';
+ if (options.release && !options.debug) {
+ variant = 'Release';
+ }
+ if (options.debug && !options.release) {
+ variant = 'Debug';
}
- return (0, exports.getBuildTypeCommon)(args);
+ return variant;
+};
+// END SECTION: Android Helpers
+// SECTION: iOS Helpers
+const resolveBuildConfiguration = (options) => {
+ let buildConfiguration = 'Release';
+ if (options.debug && !options.release) {
+ buildConfiguration = 'Debug';
+ }
+ return buildConfiguration;
+};
+const resolveScheme = (options) => {
+ return options.scheme || (0, ios_1.findScheme)();
+};
+const resolveWorkspace = (options) => {
+ return options.xcworkspace || (0, ios_1.findWorkspace)();
};
-exports.getBuildTypeAndroid = getBuildTypeAndroid;
+// END SECTION: iOS Helpers
diff --git a/packages/expo-brownfield/cli/build/utils/error.d.ts b/packages/expo-brownfield/cli/build/utils/error.d.ts
new file mode 100644
index 00000000000000..0fd7bfb1565da1
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/error.d.ts
@@ -0,0 +1,8 @@
+type ErrorType = 'android-task-repo' | 'android-directory-not-found' | 'android-library-unknown-error' | 'android-library-not-found' | 'ios-artifacts-directory-unknown-error' | 'ios-directory-not-found' | 'ios-directory-unknown-error' | 'ios-hermes-framework-not-found' | 'ios-scheme-not-found' | 'ios-workspace-not-found' | 'ios-workspace-unknown-error' | 'prebuild-cancelled';
+declare class CLIError {
+ private static readonly errorSymbol;
+ private static readonly errorMessages;
+ static handle(error: ErrorType, errorMessage?: string, fatal?: boolean): void;
+ private static handleInternal;
+}
+export default CLIError;
diff --git a/packages/expo-brownfield/cli/build/utils/error.js b/packages/expo-brownfield/cli/build/utils/error.js
new file mode 100644
index 00000000000000..590c5429b1c49a
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/error.js
@@ -0,0 +1,32 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class CLIError {
+ static errorSymbol = '✖';
+ static errorMessages = {
+ 'android-task-repo': 'At least one task or repository must be specified',
+ 'android-directory-not-found': 'Cannot find `android` directory in the project',
+ 'android-library-unknown-error': 'Unknown error occurred while finding brownfield library',
+ 'android-library-not-found': 'Could not find brownfield library in the project',
+ 'ios-artifacts-directory-unknown-error': 'Unknown error occurred while creating artifacts directory',
+ 'ios-directory-not-found': 'Cannot find `ios` directory in the project',
+ 'ios-directory-unknown-error': 'Unknown error occurred while finding brownfield iOS scheme',
+ 'ios-hermes-framework-not-found': 'Could not find hermes framework in the project at path',
+ 'ios-scheme-not-found': 'Could not find brownfield iOS scheme',
+ 'ios-workspace-not-found': 'Could not find brownfield iOS workspace',
+ 'ios-workspace-unknown-error': 'Unknown error occurred while finding brownfield iOS workspace',
+ 'prebuild-cancelled': 'Brownfield cannot be built without prebuilding the native project',
+ };
+ static handle(error, errorMessage = '', fatal = true) {
+ const message = errorMessage
+ ? `${this.errorMessages[error]}: ${errorMessage}`
+ : this.errorMessages[error];
+ this.handleInternal(message, fatal);
+ }
+ static handleInternal(message, fatal = true) {
+ console.error(`${this.errorSymbol} Error: ${message}`);
+ if (fatal) {
+ process.exit(1);
+ }
+ }
+}
+exports.default = CLIError;
diff --git a/packages/expo-brownfield/cli/build/utils/help.d.ts b/packages/expo-brownfield/cli/build/utils/help.d.ts
deleted file mode 100644
index f593dcaed08571..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/help.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import type { HelpMessageParams, HelpMessageSectionParams } from './types';
-export declare const helpMessage: ({ commands, options, promptCommand, promptOptions, }: HelpMessageParams) => string;
-export declare const helpMessageSection: ({ items, left, right, title, }: HelpMessageSectionParams) => string;
diff --git a/packages/expo-brownfield/cli/build/utils/help.js b/packages/expo-brownfield/cli/build/utils/help.js
deleted file mode 100644
index cf922b1c5051db..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/help.js
+++ /dev/null
@@ -1,38 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.helpMessageSection = exports.helpMessage = void 0;
-const chalk_1 = __importDefault(require("chalk"));
-const output_1 = require("../constants/output");
-const helpMessage = ({ commands, options, promptCommand = '', promptOptions = '', }) => {
- const optionsSection = (0, exports.helpMessageSection)({
- items: options,
- left: ({ option, short }) => `${option}${short ? `, ${short}` : ''}`,
- right: ({ description }) => description,
- title: 'Options:',
- });
- const commandsSection = (0, exports.helpMessageSection)({
- items: commands,
- left: ({ command, hasOptions }) => `${command}${hasOptions ? ` [${promptOptions}]` : ''}`,
- right: ({ description }) => description,
- title: 'Commands:',
- });
- const usageSection = `${chalk_1.default.bold('Usage:')} expo-brownfield ${promptCommand} [${promptOptions}]`;
- return `\n${usageSection}${optionsSection}${commandsSection}\n`;
-};
-exports.helpMessage = helpMessage;
-const helpMessageSection = ({ items, left, right, title, }) => {
- if (!items) {
- return '';
- }
- const content = items.reduce((acc, item) => {
- const ls = left(item);
- const rs = right(item);
- const spacing = ' '.repeat(output_1.Output.HelpSpacing - ls.length);
- return `${acc}\n ${ls}${spacing}${rs}`;
- }, '');
- return `\n\n${chalk_1.default.bold(title)}${content}`;
-};
-exports.helpMessageSection = helpMessageSection;
diff --git a/packages/expo-brownfield/cli/build/utils/index.d.ts b/packages/expo-brownfield/cli/build/utils/index.d.ts
index 58abe64a5ab08f..d47b08a1c809ce 100644
--- a/packages/expo-brownfield/cli/build/utils/index.d.ts
+++ b/packages/expo-brownfield/cli/build/utils/index.d.ts
@@ -1,6 +1,8 @@
-export * from './args';
-export * from './build';
+export * from './android';
export * from './commands';
export * from './config';
-export * from './help';
+export { default as CLIError } from './error';
+export * from './ios';
+export * from './prebuild';
+export * from './spinner';
export type * from './types';
diff --git a/packages/expo-brownfield/cli/build/utils/index.js b/packages/expo-brownfield/cli/build/utils/index.js
index 96ce4b2ee037c2..af6047af8cd081 100644
--- a/packages/expo-brownfield/cli/build/utils/index.js
+++ b/packages/expo-brownfield/cli/build/utils/index.js
@@ -13,9 +13,16 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
Object.defineProperty(exports, "__esModule", { value: true });
-__exportStar(require("./args"), exports);
-__exportStar(require("./build"), exports);
+exports.CLIError = void 0;
+__exportStar(require("./android"), exports);
__exportStar(require("./commands"), exports);
__exportStar(require("./config"), exports);
-__exportStar(require("./help"), exports);
+var error_1 = require("./error");
+Object.defineProperty(exports, "CLIError", { enumerable: true, get: function () { return __importDefault(error_1).default; } });
+__exportStar(require("./ios"), exports);
+__exportStar(require("./prebuild"), exports);
+__exportStar(require("./spinner"), exports);
diff --git a/packages/expo-brownfield/cli/build/utils/infer.d.ts b/packages/expo-brownfield/cli/build/utils/infer.d.ts
deleted file mode 100644
index 24a30dfb51f556..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/infer.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export declare const inferAndroidLibrary: () => Promise;
-export declare const inferXCWorkspace: () => Promise;
-export declare const inferScheme: () => Promise;
diff --git a/packages/expo-brownfield/cli/build/utils/infer.js b/packages/expo-brownfield/cli/build/utils/infer.js
deleted file mode 100644
index 93340a26fc90b4..00000000000000
--- a/packages/expo-brownfield/cli/build/utils/infer.js
+++ /dev/null
@@ -1,81 +0,0 @@
-"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.inferScheme = exports.inferXCWorkspace = exports.inferAndroidLibrary = void 0;
-const promises_1 = __importDefault(require("node:fs/promises"));
-const path_1 = __importDefault(require("path"));
-const constants_1 = require("../constants");
-const inferAndroidLibrary = async () => {
- const files = ['ReactNativeFragment.kt', 'ReactNativeHostManager.kt'];
- try {
- const androidPath = path_1.default.join(process.cwd(), 'android');
- await promises_1.default.access(androidPath);
- const android = await promises_1.default.readdir(androidPath, { withFileTypes: true });
- const directories = android.filter((item) => item.isDirectory());
- if (directories.length === 0) {
- throw new Error('No directories found in android/ folder');
- }
- for (const directory of directories) {
- const libraryPath = path_1.default.join(androidPath, directory.name);
- try {
- const contents = await promises_1.default.readdir(libraryPath, {
- recursive: true,
- });
- const hasAllFiles = files.every((file) => contents.some((item) => item.includes(file)));
- if (hasAllFiles) {
- return directory.name;
- }
- }
- catch (readError) {
- continue;
- }
- }
- throw new Error('Unable to find brownfield Android library');
- }
- catch (error) {
- const message = error instanceof Error ? error.message : 'Unknown error';
- return constants_1.Errors.inference('Android library name: ' + message);
- }
-};
-exports.inferAndroidLibrary = inferAndroidLibrary;
-const inferXCWorkspace = async () => {
- try {
- const iosPath = path_1.default.join(process.cwd(), 'ios');
- await promises_1.default.access(iosPath);
- const xcworkspace = (await promises_1.default.readdir(iosPath, { withFileTypes: true })).find((item) => item.name.endsWith('.xcworkspace'));
- if (xcworkspace) {
- return path_1.default.join(iosPath, xcworkspace.name);
- }
- throw new Error('Unable to find brownfield iOS Workspace (.xcworkspace)');
- }
- catch (error) {
- return constants_1.Errors.inference('iOS Workspace (.xcworkspace)');
- }
-};
-exports.inferXCWorkspace = inferXCWorkspace;
-const inferScheme = async () => {
- try {
- const iosPath = path_1.default.join(process.cwd(), 'ios');
- await promises_1.default.access(iosPath);
- const subDirs = (await promises_1.default.readdir(iosPath, { withFileTypes: true })).filter((item) => item.isDirectory());
- for (const subDir of subDirs) {
- try {
- const subDirPath = path_1.default.join(iosPath, subDir.name);
- const contents = await promises_1.default.readdir(subDirPath);
- if (contents.includes('ReactNativeHostManager.swift')) {
- return subDir.name;
- }
- }
- catch (readError) {
- continue;
- }
- }
- throw new Error('Unable to find brownfield iOS group');
- }
- catch (error) {
- return constants_1.Errors.inference('iOS Scheme');
- }
-};
-exports.inferScheme = inferScheme;
diff --git a/packages/expo-brownfield/cli/build/utils/ios.d.ts b/packages/expo-brownfield/cli/build/utils/ios.d.ts
new file mode 100644
index 00000000000000..cd0680538c0e16
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/ios.d.ts
@@ -0,0 +1,9 @@
+import { IosConfig } from './types';
+export declare const cleanUpArtifacts: (config: IosConfig) => Promise;
+export declare const buildFramework: (config: IosConfig) => Promise;
+export declare const copyHermesXcframework: (config: IosConfig) => Promise;
+export declare const createXcframework: (config: IosConfig) => Promise;
+export declare const findScheme: () => string | undefined;
+export declare const findWorkspace: () => string | undefined;
+export declare const makeArtifactsDirectory: (config: IosConfig) => void;
+export declare const printIosConfig: (config: IosConfig) => void;
diff --git a/packages/expo-brownfield/cli/build/utils/ios.js b/packages/expo-brownfield/cli/build/utils/ios.js
new file mode 100644
index 00000000000000..0d91ba448b6c26
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/ios.js
@@ -0,0 +1,175 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.printIosConfig = exports.makeArtifactsDirectory = exports.findWorkspace = exports.findScheme = exports.createXcframework = exports.copyHermesXcframework = exports.buildFramework = exports.cleanUpArtifacts = void 0;
+const chalk_1 = __importDefault(require("chalk"));
+const node_fs_1 = __importDefault(require("node:fs"));
+const node_path_1 = __importDefault(require("node:path"));
+const commands_1 = require("./commands");
+const error_1 = __importDefault(require("./error"));
+const spinner_1 = require("./spinner");
+const cleanUpArtifacts = async (config) => {
+ if (config.dryRun) {
+ console.log('Cleaning up previous artifacts');
+ return;
+ }
+ return (0, spinner_1.withSpinner)({
+ operation: async () => {
+ if (!node_fs_1.default.existsSync(config.artifacts)) {
+ return;
+ }
+ const xcframeworks = node_fs_1.default
+ .readdirSync(config.artifacts)
+ .filter((item) => item.endsWith('.xcframework'));
+ xcframeworks.forEach((item) => {
+ const itemPath = `${config.artifacts}/${item}`;
+ node_fs_1.default.rmSync(itemPath, { recursive: true, force: true });
+ });
+ },
+ loaderMessage: 'Cleaning up previous artifacts...',
+ successMessage: 'Cleaning up previous artifacts succeeded',
+ errorMessage: 'Cleaning up previous artifacts failed',
+ });
+};
+exports.cleanUpArtifacts = cleanUpArtifacts;
+const buildFramework = async (config) => {
+ const args = [
+ '-workspace',
+ config.workspace,
+ '-scheme',
+ config.scheme,
+ '-derivedDataPath',
+ config.derivedDataPath,
+ '-destination',
+ 'generic/platform=iphoneos',
+ '-destination',
+ 'generic/platform=iphonesimulator',
+ '-configuration',
+ config.buildConfiguration,
+ ];
+ if (config.dryRun) {
+ console.log(`xcodebuild ${args.join(' ')}`);
+ return;
+ }
+ return (0, spinner_1.withSpinner)({
+ operation: () => (0, commands_1.runCommand)('xcodebuild', args, { verbose: config.verbose }),
+ loaderMessage: 'Compiling framework...',
+ successMessage: 'Compiling framework succeeded',
+ errorMessage: 'Compiling framework failed',
+ verbose: config.verbose,
+ });
+};
+exports.buildFramework = buildFramework;
+const copyHermesXcframework = async (config) => {
+ if (config.dryRun) {
+ console.log(`Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermes.xcframework`);
+ return;
+ }
+ const sourcePath = `./ios/${config.hermesFrameworkPath}`;
+ if (!node_fs_1.default.existsSync(sourcePath)) {
+ error_1.default.handle('ios-hermes-framework-not-found', sourcePath);
+ }
+ return (0, spinner_1.withSpinner)({
+ operation: async () => node_fs_1.default.cpSync(sourcePath, `${config.artifacts}/hermesvm.xcframework`, {
+ force: true,
+ recursive: true,
+ }),
+ loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...',
+ successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded',
+ errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed',
+ verbose: config.verbose,
+ });
+};
+exports.copyHermesXcframework = copyHermesXcframework;
+const createXcframework = async (config) => {
+ const args = [
+ '-create-xcframework',
+ '-framework',
+ `${config.device}/${config.scheme}.framework`,
+ '-framework',
+ `${config.simulator}/${config.scheme}.framework`,
+ '-output',
+ `${config.artifacts}/${config.scheme}.xcframework`,
+ ];
+ if (config.dryRun) {
+ console.log(`xcodebuild ${args.join(' ')}`);
+ return;
+ }
+ return (0, spinner_1.withSpinner)({
+ operation: () => (0, commands_1.runCommand)('xcodebuild', args, { verbose: config.verbose }),
+ loaderMessage: 'Packaging framework into an XCFramework...',
+ successMessage: 'Packaging framework into an XCFramework succeeded',
+ errorMessage: 'Packaging framework into an XCFramework failed',
+ verbose: config.verbose,
+ });
+};
+exports.createXcframework = createXcframework;
+const findScheme = () => {
+ try {
+ const iosPath = node_path_1.default.join(process.cwd(), 'ios');
+ if (!node_fs_1.default.existsSync(iosPath)) {
+ error_1.default.handle('ios-directory-not-found');
+ }
+ const subdirectories = node_fs_1.default
+ .readdirSync(iosPath, { withFileTypes: true })
+ .filter((item) => item.isDirectory());
+ const scheme = subdirectories.find((directory) => {
+ const directoryPath = node_path_1.default.join(iosPath, directory.name);
+ const files = node_fs_1.default.readdirSync(directoryPath, { recursive: true });
+ return files.some((file) => typeof file === 'string' && file.endsWith('ReactNativeHostManager.swift'));
+ });
+ if (scheme) {
+ return scheme.name;
+ }
+ error_1.default.handle('ios-scheme-not-found');
+ }
+ catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ error_1.default.handle('ios-directory-unknown-error', errorMessage);
+ }
+};
+exports.findScheme = findScheme;
+const findWorkspace = () => {
+ try {
+ const iosPath = node_path_1.default.join(process.cwd(), 'ios');
+ if (!node_fs_1.default.existsSync(iosPath)) {
+ error_1.default.handle('ios-directory-not-found');
+ }
+ const items = node_fs_1.default.readdirSync(iosPath, { withFileTypes: true });
+ const workspace = items.find((item) => item.name.endsWith('.xcworkspace'));
+ if (workspace) {
+ return node_path_1.default.join(iosPath, workspace.name);
+ }
+ error_1.default.handle('ios-workspace-not-found');
+ }
+ catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ error_1.default.handle('ios-workspace-unknown-error', errorMessage);
+ }
+};
+exports.findWorkspace = findWorkspace;
+const makeArtifactsDirectory = (config) => {
+ try {
+ if (!node_fs_1.default.existsSync(config.artifacts)) {
+ node_fs_1.default.mkdirSync(config.artifacts, { recursive: true });
+ }
+ }
+ catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ error_1.default.handle('ios-artifacts-directory-unknown-error', errorMessage);
+ }
+};
+exports.makeArtifactsDirectory = makeArtifactsDirectory;
+const printIosConfig = (config) => {
+ console.log(chalk_1.default.bold('Resolved build configuration'));
+ console.log(` - Build configuration: ${chalk_1.default.blue(config.buildConfiguration)}`);
+ console.log(` - Scheme: ${chalk_1.default.blue(config.scheme)}`);
+ console.log(` - Workspace: ${chalk_1.default.blue(config.workspace)}`);
+ console.log(` - Dry run: ${chalk_1.default.blue(config.dryRun)}`);
+ console.log(` - Verbose: ${chalk_1.default.blue(config.verbose)}`);
+ console.log(` - Artifacts path: ${chalk_1.default.blue(config.artifacts)}`);
+ console.log();
+};
+exports.printIosConfig = printIosConfig;
diff --git a/packages/expo-brownfield/cli/build/utils/prebuild.d.ts b/packages/expo-brownfield/cli/build/utils/prebuild.d.ts
new file mode 100644
index 00000000000000..839b333d295858
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/prebuild.d.ts
@@ -0,0 +1,2 @@
+import type { Platform } from './types';
+export declare const validatePrebuild: (platform: Platform) => Promise;
diff --git a/packages/expo-brownfield/cli/build/utils/prebuild.js b/packages/expo-brownfield/cli/build/utils/prebuild.js
new file mode 100644
index 00000000000000..dab172911f0a6d
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/prebuild.js
@@ -0,0 +1,41 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.validatePrebuild = void 0;
+const chalk_1 = __importDefault(require("chalk"));
+const node_fs_1 = __importDefault(require("node:fs"));
+const node_path_1 = __importDefault(require("node:path"));
+const prompts_1 = __importDefault(require("prompts"));
+const commands_1 = require("./commands");
+const error_1 = __importDefault(require("./error"));
+const spinner_1 = require("./spinner");
+const validatePrebuild = async (platform) => {
+ if (!checkPrebuild(platform)) {
+ console.info(`${chalk_1.default.yellow(`⚠ Prebuild for platform: ${platform} is missing`)}`);
+ const response = await (0, prompts_1.default)({
+ type: 'confirm',
+ name: 'shouldRunPrebuild',
+ message: 'Do you want to run the prebuild now?',
+ initial: false,
+ });
+ if (response.shouldRunPrebuild) {
+ await (0, spinner_1.withSpinner)({
+ operation: () => (0, commands_1.runCommand)('npx', ['expo', 'prebuild', '--platform', platform]),
+ loaderMessage: `Running 'npx expo prebuild' for platform: ${platform}...`,
+ successMessage: `Prebuild for ${platform} completed\n`,
+ errorMessage: `Prebuild for ${platform} failed`,
+ verbose: false,
+ });
+ }
+ else {
+ error_1.default.handle('prebuild-cancelled');
+ }
+ }
+};
+exports.validatePrebuild = validatePrebuild;
+const checkPrebuild = (platform) => {
+ const nativeDirectory = node_path_1.default.join(process.cwd(), platform);
+ return node_fs_1.default.existsSync(nativeDirectory);
+};
diff --git a/packages/expo-brownfield/cli/build/utils/spinner.d.ts b/packages/expo-brownfield/cli/build/utils/spinner.d.ts
new file mode 100644
index 00000000000000..c96ce244f45f23
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/spinner.d.ts
@@ -0,0 +1,2 @@
+import { WithSpinnerParams } from './types';
+export declare const withSpinner: ({ operation, loaderMessage, successMessage, errorMessage, onError, verbose, }: WithSpinnerParams) => Promise;
diff --git a/packages/expo-brownfield/cli/build/utils/spinner.js b/packages/expo-brownfield/cli/build/utils/spinner.js
new file mode 100644
index 00000000000000..e8294be65a89be
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/spinner.js
@@ -0,0 +1,32 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.withSpinner = void 0;
+const ora_1 = __importDefault(require("ora"));
+const withSpinner = async ({ operation, loaderMessage, successMessage, errorMessage, onError = 'error', verbose = false, }) => {
+ let spinner;
+ try {
+ if (!verbose) {
+ spinner = (0, ora_1.default)(loaderMessage).start();
+ }
+ const result = await operation();
+ if (!verbose) {
+ spinner?.succeed(successMessage);
+ }
+ return result;
+ }
+ catch (error) {
+ if (!verbose) {
+ onError === 'error' ? spinner?.fail(errorMessage) : spinner?.warn(errorMessage);
+ }
+ throw new Error(errorMessage);
+ }
+ finally {
+ if (!verbose && spinner?.isSpinning) {
+ spinner?.stop();
+ }
+ }
+};
+exports.withSpinner = withSpinner;
diff --git a/packages/expo-brownfield/cli/build/utils/types.d.ts b/packages/expo-brownfield/cli/build/utils/types.d.ts
index 1dcce8e7263498..2c4b92d3cf6a92 100644
--- a/packages/expo-brownfield/cli/build/utils/types.d.ts
+++ b/packages/expo-brownfield/cli/build/utils/types.d.ts
@@ -1,47 +1,34 @@
-import type { Spec } from 'arg';
-export interface HelpMessageCommand {
- command: string;
- description: string;
- hasOptions?: boolean;
-}
-export interface HelpMessageOption {
- description: string;
- option: string;
- short?: string;
-}
-export interface HelpMessageParams {
- commands?: HelpMessageCommand[];
- options?: HelpMessageOption[];
- promptCommand?: string;
- promptOptions?: string;
+export type Platform = 'android' | 'ios';
+export interface RunCommandOptions {
+ cwd?: string;
+ env?: Record;
+ verbose?: boolean;
}
-export interface HelpMessageSectionParams {
- items?: T[];
- left: (item: T) => string;
- right: (item: T) => string;
- title: string;
+export interface RunCommandResult {
+ stdout: string;
}
-export interface ParseArgsParams {
- spec: Spec;
- argv?: string[];
- stopAtPositional?: boolean;
+export interface WithSpinnerParams {
+ operation: () => Promise;
+ loaderMessage: string;
+ successMessage?: string;
+ errorMessage?: string;
+ onError?: 'error' | 'warn';
+ verbose?: boolean;
}
-export type BuildTypeCommon = 'debug' | 'release';
-export type BuildTypeAndroid = BuildTypeCommon | 'all';
-export interface BuildConfigCommon {
+export interface CommonConfig {
dryRun: boolean;
- help: boolean;
verbose: boolean;
}
-export interface BuildConfigAndroid extends BuildConfigCommon {
- buildType: BuildTypeAndroid;
- libraryName: string;
- repositories: string[];
+export type BuildVariant = 'All' | 'Debug' | 'Release';
+export interface AndroidConfig extends CommonConfig {
+ library: string;
tasks: string[];
+ variant: BuildVariant;
}
-export interface BuildConfigIos extends BuildConfigCommon {
+export type BuildConfiguration = 'Debug' | 'Release';
+export interface IosConfig extends CommonConfig {
artifacts: string;
- buildType: BuildTypeCommon;
+ buildConfiguration: BuildConfiguration;
derivedDataPath: string;
device: string;
hermesFrameworkPath: string;
@@ -49,19 +36,6 @@ export interface BuildConfigIos extends BuildConfigCommon {
simulator: string;
workspace: string;
}
-export interface RunCommandOptions {
- cwd?: string;
- env?: Record;
- verbose?: boolean;
-}
-export interface RunCommandResult {
- stdout: string;
-}
-export interface WithSpinnerParams {
- operation: () => Promise;
- loaderMessage: string;
- successMessage?: string;
- errorMessage?: string;
- onError?: 'error' | 'warn';
- verbose?: boolean;
+export interface TasksConfigAndroid extends CommonConfig {
+ library: string;
}
diff --git a/packages/expo-brownfield/cli/src/commands/android/build.ts b/packages/expo-brownfield/cli/src/commands/android/build.ts
deleted file mode 100644
index 44ba7dc59b2a3e..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/android/build.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import path from 'path';
-
-import { Args, Errors, Help } from '../../constants';
-import {
- BuildTypeAndroid,
- ensurePrebuild,
- getAndroidConfig,
- getCommand,
- getCommonConfig,
- parseArgs,
- printConfig,
- runCommand,
- withSpinner,
-} from '../../utils';
-
-const action = async () => {
- const args = parseArgs({
- spec: Args.Android,
- // Skip first three args:
- // expo-brownfield build:android
- argv: process.argv.slice(3),
- stopAtPositional: true,
- });
-
- if (getCommand(args)) {
- return Errors.additionalCommand('build:android');
- }
-
- // Only resolve --help and --verbose options
- const basicConfig = getCommonConfig(args);
- if (basicConfig.help) {
- console.log(Help.Android);
- return process.exit(0);
- }
-
- await ensurePrebuild('android');
-
- const config = await getAndroidConfig(args);
- printConfig(config);
-
- let tasks = [];
- if (config.tasks.length > 0) {
- tasks = config.tasks;
- } else if (config.repositories.length > 0) {
- for (const repository of config.repositories) {
- const task = constructTask(config.buildType, repository);
- tasks.push(task);
- }
- } else {
- Errors.missingTasksOrRepositories();
- }
-
- for (const task of tasks) {
- if (!config.dryRun) {
- await runTask(task, config.verbose);
- } else {
- console.log(`./gradlew ${task}`);
- }
- }
-};
-
-export default action;
-
-const constructTask = (buildType: BuildTypeAndroid, repository: string): string => {
- const buildTypeCapitalized = buildType[0].toUpperCase() + buildType.slice(1);
- const repositorySuffixed = repository === 'MavenLocal' ? repository : `${repository}Repository`;
- return `publishBrownfield${buildTypeCapitalized}PublicationTo${repositorySuffixed}`;
-};
-
-const runTask = async (task: string, verbose: boolean) => {
- return withSpinner({
- operation: () =>
- runCommand('./gradlew', [task], {
- cwd: path.join(process.cwd(), 'android'),
- verbose,
- }),
- loaderMessage: 'Running task: ' + task,
- successMessage: 'Running task: ' + task + ' succeeded',
- errorMessage: 'Running task: ' + task + ' failed',
- verbose,
- });
-};
diff --git a/packages/expo-brownfield/cli/src/commands/android/index.ts b/packages/expo-brownfield/cli/src/commands/android/index.ts
deleted file mode 100644
index 867e7d814fde67..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/android/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as runBuildAndroid } from './build';
-export { default as runTasksAndroid } from './tasks';
diff --git a/packages/expo-brownfield/cli/src/commands/android/tasks.ts b/packages/expo-brownfield/cli/src/commands/android/tasks.ts
deleted file mode 100644
index ad05bbc528a4b3..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/android/tasks.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import chalk from 'chalk';
-import path from 'path';
-
-import { Args, Errors, Help } from '../../constants';
-import { getCommand, getTasksAndroidConfig, parseArgs, runCommand, withSpinner } from '../../utils';
-
-const action = async () => {
- const args = parseArgs({
- spec: Args.TasksAndroid,
- // Skip first three args:
- // expo-brownfield tasks:android
- argv: process.argv.slice(3),
- stopAtPositional: true,
- });
-
- if (getCommand(args)) {
- return Errors.additionalCommand('tasks:android');
- }
-
- const config = await getTasksAndroidConfig(args);
-
- if (config.help) {
- console.log(Help.TasksAndroid);
- return process.exit(0);
- }
-
- const { stdout } = await withSpinner({
- operation: () =>
- runCommand('./gradlew', [`${config.libraryName}:tasks`, '--group', 'publishing'], {
- cwd: path.join(process.cwd(), 'android'),
- verbose: config.verbose,
- }),
- loaderMessage: 'Reading publish tasks from the android project...',
- successMessage: 'Successfully read publish tasks from the android project\n',
- errorMessage: 'Failed to read publish tasks from the android project',
- verbose: config.verbose,
- });
-
- if (config.verbose) {
- // stdout is already printed to the console
- return;
- }
-
- const regex = /^publishBrownfield[a-zA-Z0-9_-]*/i;
- const publishTasks = stdout
- .split('\n')
- .map((line) => regex.exec(line)?.[0])
- // Remove duplicate maven local tasks
- .filter((task) => task && !task.includes('MavenLocalRepository')) as string[];
-
- console.log(chalk.bold('Publish tasks:'));
- publishTasks.forEach((task) => {
- console.log(`- ${task}`);
- });
-
- const splitRegex = /^publishBrownfield(?:All|Debug|Release)PublicationTo(.+?)(?:Repository)?$/;
- const repositories = [
- ...new Set(
- publishTasks
- .map((task) => {
- return splitRegex.exec(task)?.[1];
- })
- .filter((repo) => repo)
- ),
- ];
-
- console.log(chalk.bold('\nRepositories:'));
- repositories.forEach((repo) => {
- console.log(`- ${repo}`);
- });
-};
-
-export default action;
diff --git a/packages/expo-brownfield/cli/src/commands/build-android.ts b/packages/expo-brownfield/cli/src/commands/build-android.ts
new file mode 100644
index 00000000000000..95534e3e8275a4
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/commands/build-android.ts
@@ -0,0 +1,25 @@
+import type { Command } from 'commander';
+
+import {
+ CLIError,
+ printAndroidConfig,
+ resolveBuildConfigAndroid,
+ runTask,
+ validatePrebuild,
+} from '../utils';
+
+const buildAndroid = async (command: Command) => {
+ await validatePrebuild('android');
+
+ const config = resolveBuildConfigAndroid(command.opts());
+ if (!config.tasks.length) {
+ CLIError.handle('android-task-repo');
+ }
+ printAndroidConfig(config);
+
+ for (const task of config.tasks) {
+ await runTask(task, config.verbose, config.dryRun);
+ }
+};
+
+export default buildAndroid;
diff --git a/packages/expo-brownfield/cli/src/commands/build-ios.ts b/packages/expo-brownfield/cli/src/commands/build-ios.ts
new file mode 100644
index 00000000000000..ca5bed14ba0ee5
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/commands/build-ios.ts
@@ -0,0 +1,27 @@
+import type { Command } from 'commander';
+
+import {
+ buildFramework,
+ cleanUpArtifacts,
+ createXcframework,
+ copyHermesXcframework,
+ makeArtifactsDirectory,
+ printIosConfig,
+ resolveBuildConfigIos,
+ validatePrebuild,
+} from '../utils';
+
+const buildIos = async (command: Command) => {
+ await validatePrebuild('ios');
+
+ const config = resolveBuildConfigIos(command.opts());
+ printIosConfig(config);
+
+ await cleanUpArtifacts(config);
+ makeArtifactsDirectory(config);
+ await buildFramework(config);
+ await createXcframework(config);
+ await copyHermesXcframework(config);
+};
+
+export default buildIos;
diff --git a/packages/expo-brownfield/cli/src/commands/commands.ts b/packages/expo-brownfield/cli/src/commands/commands.ts
deleted file mode 100644
index d8c8a7c1ed9d7d..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/commands.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { runBuildAndroid, runTasksAndroid } from './android';
-import { runHelp, runVersion } from './general';
-import { runBuildIos } from './ios';
-import { CommandsMap } from './types';
-
-export const Commands: CommandsMap = {
- 'build:android': {
- run: runBuildAndroid,
- },
- 'build:ios': {
- run: runBuildIos,
- },
- help: {
- run: runHelp,
- },
- 'tasks:android': {
- run: runTasksAndroid,
- },
- version: {
- run: runVersion,
- },
-};
diff --git a/packages/expo-brownfield/cli/src/commands/general/help.ts b/packages/expo-brownfield/cli/src/commands/general/help.ts
deleted file mode 100644
index 07639d8a713c16..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/general/help.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Help } from '../../constants';
-
-/**
- * Prints the help message for the CLI.
- */
-const helpAction = async () => {
- console.log(Help.General);
-};
-
-export default helpAction;
diff --git a/packages/expo-brownfield/cli/src/commands/general/index.ts b/packages/expo-brownfield/cli/src/commands/general/index.ts
deleted file mode 100644
index 3d8eb9fed89bd4..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/general/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as runHelp } from './help';
-export { default as runVersion } from './version';
diff --git a/packages/expo-brownfield/cli/src/commands/general/version.ts b/packages/expo-brownfield/cli/src/commands/general/version.ts
deleted file mode 100644
index b1e1af579fb1e8..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/general/version.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// @ts-expect-error - the directory structure is different after building
-import { version } from '../../../../package.json';
-
-/**
- * Prints the version of the CLI (= the version of the package).
- */
-const action = async () => {
- console.log(version);
-};
-
-export default action;
diff --git a/packages/expo-brownfield/cli/src/commands/index.ts b/packages/expo-brownfield/cli/src/commands/index.ts
index 0739a373502d94..3ad397d285bfe7 100644
--- a/packages/expo-brownfield/cli/src/commands/index.ts
+++ b/packages/expo-brownfield/cli/src/commands/index.ts
@@ -1,3 +1,3 @@
-export * from './commands';
-export * from './resolve';
-export type * from './types';
+export { default as buildAndroid } from './build-android';
+export { default as buildIos } from './build-ios';
+export { default as tasksAndroid } from './tasks-android';
diff --git a/packages/expo-brownfield/cli/src/commands/ios/build.ts b/packages/expo-brownfield/cli/src/commands/ios/build.ts
deleted file mode 100644
index e2f34e6e6dd6da..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/ios/build.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import fs from 'node:fs/promises';
-
-import { Args, Errors, Help } from '../../constants';
-import {
- BuildConfigIos,
- ensurePrebuild,
- getCommand,
- getCommonConfig,
- getIosConfig,
- parseArgs,
- printConfig,
- runCommand,
- withSpinner,
-} from '../../utils';
-
-const action = async () => {
- const args = parseArgs({
- spec: Args.IOS,
- // Skip first three args:
- // expo-brownfield build:ios
- argv: process.argv.slice(3),
- stopAtPositional: true,
- });
-
- if (getCommand(args)) {
- return Errors.additionalCommand('build:ios');
- }
-
- // Only resolve --help and --verbose options
- const basicConfig = getCommonConfig(args);
- if (basicConfig.help) {
- console.log(Help.IOS);
- return process.exit(0);
- }
-
- await ensurePrebuild('ios');
-
- const config = await getIosConfig(args);
- printConfig(config);
-
- await cleanUpArtifacts(config);
- await runBuild(config);
- await packageFrameworks(config);
- await copyHermesFramework(config);
-};
-
-export default action;
-
-const cleanUpArtifacts = async (config: BuildConfigIos) => {
- if (config.dryRun) {
- console.log('Cleaning up previous artifacts');
- return;
- }
-
- return withSpinner({
- operation: async () => {
- try {
- await fs.access(config.artifacts);
- } catch (error) {
- return;
- }
-
- const artifacts = (await fs.readdir(config.artifacts)).filter((artifact) =>
- artifact.endsWith('.xcframework')
- );
-
- for (const artifact of artifacts) {
- await fs.rm(`${config.artifacts}/${artifact}`, {
- recursive: true,
- force: true,
- });
- }
- },
- loaderMessage: 'Cleaning up previous artifacts...',
- successMessage: 'Cleaning up previous artifacts succeeded',
- errorMessage: 'Cleaning up previous artifacts failed',
- });
-};
-
-const runBuild = async (config: BuildConfigIos) => {
- const args = [
- '-workspace',
- config.workspace,
- '-scheme',
- config.scheme,
- '-derivedDataPath',
- config.derivedDataPath,
- '-destination',
- 'generic/platform=iphoneos',
- '-destination',
- 'generic/platform=iphonesimulator',
- '-configuration',
- config.buildType.charAt(0).toUpperCase() + config.buildType.slice(1),
- ];
-
- if (config.dryRun) {
- console.log(`xcodebuild ${args.join(' ')}`);
- return;
- }
-
- return withSpinner({
- operation: () => runCommand('xcodebuild', args, { verbose: config.verbose }),
- loaderMessage: 'Compiling framework...',
- successMessage: 'Compiling framework succeeded',
- errorMessage: 'Compiling framework failed',
- verbose: config.verbose,
- });
-};
-
-const packageFrameworks = async (config: BuildConfigIos) => {
- const args = [
- '-create-xcframework',
- '-framework',
- `${config.device}/${config.scheme}.framework`,
- '-framework',
- `${config.simulator}/${config.scheme}.framework`,
- '-output',
- `${config.artifacts}/${config.scheme}.xcframework`,
- ];
-
- if (config.dryRun) {
- console.log(`xcodebuild ${args.join(' ')}`);
- return;
- }
-
- return withSpinner({
- operation: () => runCommand('xcodebuild', args, { verbose: config.verbose }),
- loaderMessage: 'Packaging framework into an XCFramework...',
- successMessage: 'Packaging framework into an XCFramework succeeded',
- errorMessage: 'Packaging framework into an XCFramework failed',
- verbose: config.verbose,
- });
-};
-
-const copyHermesFramework = async (config: BuildConfigIos) => {
- if (config.dryRun) {
- console.log(
- `Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermesvm.xcframework`
- );
- return;
- }
-
- return withSpinner({
- operation: () =>
- fs.cp(`./ios/${config.hermesFrameworkPath}`, `${config.artifacts}/hermesvm.xcframework`, {
- force: true,
- recursive: true,
- }),
- loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...',
- successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded',
- errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed',
- verbose: config.verbose,
- });
-};
diff --git a/packages/expo-brownfield/cli/src/commands/ios/index.ts b/packages/expo-brownfield/cli/src/commands/ios/index.ts
deleted file mode 100644
index 49066d174ec4b9..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/ios/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as runBuildIos } from './build';
diff --git a/packages/expo-brownfield/cli/src/commands/resolve.ts b/packages/expo-brownfield/cli/src/commands/resolve.ts
deleted file mode 100644
index edf1d872fecd37..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/resolve.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { AndroidCommand, CommandEntry } from './types';
-import { Args, Errors } from '../constants';
-import { getCommand, parseArgs } from '../utils';
-import { Commands } from './commands';
-
-export const resolveCommand = (): CommandEntry => {
- const args = parseArgs({ spec: Args.General, stopAtPositional: true });
-
- if (args['--help']) {
- return Commands.help;
- }
- if (args['--version']) {
- return Commands.version;
- }
-
- if (!args['_']?.length) {
- return Commands.help;
- }
-
- const command = getCommand(args);
- if (command === 'build:android' || command === 'tasks:android') {
- return resolveAndroid(command);
- }
- if (command === 'build:ios') {
- return resolveIos();
- }
-
- return Errors.unknownCommand();
-};
-
-export const resolveAndroid = (command: AndroidCommand): CommandEntry => {
- return Commands[command];
-};
-
-export const resolveIos = (): CommandEntry => {
- return Commands['build:ios'];
-};
diff --git a/packages/expo-brownfield/cli/src/commands/tasks-android.ts b/packages/expo-brownfield/cli/src/commands/tasks-android.ts
new file mode 100644
index 00000000000000..3e4cbcc65e13a2
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/commands/tasks-android.ts
@@ -0,0 +1,46 @@
+import chalk from 'chalk';
+import type { Command } from 'commander';
+import path from 'node:path';
+
+import {
+ processRepositories,
+ processTasks,
+ resolveTasksConfigAndroid,
+ runCommand,
+ validatePrebuild,
+ withSpinner,
+} from '../utils';
+
+const tasksAndroid = async (command: Command) => {
+ await validatePrebuild('android');
+
+ const config = resolveTasksConfigAndroid(command.opts());
+ const { stdout } = await withSpinner({
+ operation: () =>
+ runCommand('./gradlew', [`${config.library}:tasks`, '--group', 'publishing'], {
+ cwd: path.join(process.cwd(), 'android'),
+ verbose: config.verbose,
+ }),
+ loaderMessage: 'Reading publish tasks from the android project...',
+ successMessage: 'Successfully read publish tasks from the android project\n',
+ errorMessage: 'Failed to read publish tasks from the android project',
+ verbose: config.verbose,
+ });
+
+ // Forwarded stdout already contains the tasks
+ if (config.verbose) {
+ return;
+ }
+
+ console.log(chalk.bold('Publishing tasks'));
+ const tasks = processTasks(stdout);
+ tasks.forEach((task) => {
+ console.log(` - ${chalk.blue(task)}`);
+ });
+ console.log(chalk.bold('Repositories'));
+ processRepositories(tasks).forEach((repository) => {
+ console.log(` - ${chalk.blue(repository)}`);
+ });
+};
+
+export default tasksAndroid;
diff --git a/packages/expo-brownfield/cli/src/commands/types.ts b/packages/expo-brownfield/cli/src/commands/types.ts
deleted file mode 100644
index ee58ccff4415af..00000000000000
--- a/packages/expo-brownfield/cli/src/commands/types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export type GeneralCommand = 'help' | 'version';
-export type AndroidCommand = 'build:android' | 'tasks:android';
-export type IosCommand = 'build:ios';
-
-export type Command = GeneralCommand | AndroidCommand | IosCommand;
-
-export interface CommandEntry {
- run: () => Promise;
-}
-
-export type CommandsMap = Record;
diff --git a/packages/expo-brownfield/cli/src/constants/args.ts b/packages/expo-brownfield/cli/src/constants/args.ts
deleted file mode 100644
index b0490b62230147..00000000000000
--- a/packages/expo-brownfield/cli/src/constants/args.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import arg, { type Spec } from 'arg';
-
-/**
- * General CLI arguments
- */
-const generalArgs: Spec = {
- // Types
- '--help': arg.COUNT,
- '--version': arg.COUNT,
- // Aliases
- '-h': '--help',
- '-v': '--version',
-} as const;
-
-/**
- * Common build arguments shared by Android and iOS
- */
-const buildCommonArgs: Spec = {
- // Types
- '--debug': arg.COUNT,
- '--dry-run': arg.COUNT,
- '--help': arg.COUNT,
- '--release': arg.COUNT,
- '--verbose': arg.COUNT,
- // Aliases
- '-d': '--debug',
- '-h': '--help',
- '-r': '--release',
-};
-
-/**
- * Android build arguments
- */
-const buildAndroidArgs: Spec = {
- // Inherited
- ...buildCommonArgs,
- // Types
- '--all': arg.COUNT,
- '--library': String,
- '--repository': [String],
- '--task': [String],
- // Aliases
- '-a': '--all',
- '-l': '--library',
- '--repo': '--repository',
- '-t': '--task',
-};
-
-/**
- * Android tasks arguments
- */
-const tasksAndroidArgs: Spec = {
- // Types
- '--help': arg.COUNT,
- '--library': String,
- '--verbose': arg.COUNT,
- // Aliases
- '-h': '--help',
- '-l': '--library',
-};
-
-/**
- * iOS build arguments
- */
-const buildIosArgs: Spec = {
- // Inherited
- ...buildCommonArgs,
- // Types
- '--artifacts': String,
- '--scheme': String,
- '--xcworkspace': String,
- // Aliases
- '-a': '--artifacts',
- '-s': '--scheme',
- '-x': '--xcworkspace',
-};
-
-/**
- * CLI arguments
- */
-export const Args: Record = {
- Android: buildAndroidArgs,
- General: generalArgs,
- IOS: buildIosArgs,
- TasksAndroid: tasksAndroidArgs,
-};
diff --git a/packages/expo-brownfield/cli/src/constants/defaults.ts b/packages/expo-brownfield/cli/src/constants/defaults.ts
deleted file mode 100644
index 01d4f36f21b4bd..00000000000000
--- a/packages/expo-brownfield/cli/src/constants/defaults.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const Defaults = {
- artifactsPath: './artifacts',
- hermesFrameworkPath:
- 'Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework',
- libraryName: 'brownfield',
-} as const;
diff --git a/packages/expo-brownfield/cli/src/constants/errors.ts b/packages/expo-brownfield/cli/src/constants/errors.ts
deleted file mode 100644
index 21615c8b8633f0..00000000000000
--- a/packages/expo-brownfield/cli/src/constants/errors.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import type { ArgError } from 'arg';
-
-const additionalCommandError = (command: string) => {
- console.error(`Error: Command ${command} doesn't support additional commands
-For all available options please use the help command:
-npx expo-brownfield ${command} --help`);
- return process.exit(1);
-};
-
-/**
- * Prints a generic error message.
- *
- * @param error - The error object.
- */
-const genericError = (error: unknown) => {
- if (error instanceof Error) {
- console.error(`Error: ${error.message}`);
- } else {
- console.error('Error: An unknown error occurred');
- }
- return process.exit(1);
-};
-
-/**
- * Prints the error message for failed inference.
- */
-const inferenceError = (valueName: string) => {
- console.error(`Error: Value of ${valueName} could not be inferred from the project`);
- return process.exit(1);
-};
-
-/**
- * Prints the error message for missing tasks or repositories.
- */
-const missingTasksOrRepositoriesError = () => {
- console.error('Error: At least one task or repository must be specified');
- return process.exit(1);
-};
-
-/**
- * Prints the error message for failed argument parsing.
- */
-const parseArgsError = () => {
- console.error('Error: failed to parse arguments');
- return process.exit(1);
-};
-
-/**
- * Prints the error message for an unknown command.
- */
-const unknownCommandError = () => {
- console.error(`Error: unknown command
-Supported commands: build:android, build:ios, tasks:android`);
- return process.exit(1);
-};
-
-/**
- * Prints the error message for an unknown option.
- *
- * @param argError - The error object.
- */
-const unkownOptionError = (argError: ArgError) => {
- const message = argError.message.replace('ArgError: ', '');
- console.error(`Error: ${message}`);
- return process.exit(1);
-};
-
-export const Errors = {
- additionalCommand: additionalCommandError,
- generic: genericError,
- inference: inferenceError,
- missingTasksOrRepositories: missingTasksOrRepositoriesError,
- parseArgs: parseArgsError,
- unknownCommand: unknownCommandError,
- unknownOption: unkownOptionError,
-} as const;
diff --git a/packages/expo-brownfield/cli/src/constants/help.ts b/packages/expo-brownfield/cli/src/constants/help.ts
deleted file mode 100644
index 29ebacb508fb50..00000000000000
--- a/packages/expo-brownfield/cli/src/constants/help.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { helpMessage } from '../utils';
-
-/**
- * Helper function to create a help option for a command.
- * @param command - The command to display help for.
- * @returns The help option.
- */
-const helpOption = (command: string = 'command') => ({
- description: `display help for ${command}`,
- option: '--help',
- short: '-h',
-});
-
-/**
- * Common build options for Android and iOS.
- */
-const commonBuildOptions = [
- {
- description: 'build in debug configuration',
- option: '--debug',
- short: '-d',
- },
- {
- description: 'build in release configuration',
- option: '--release',
- short: '-r',
- },
- {
- description: 'forward all output to the terminal',
- option: '--verbose',
- },
-];
-
-/**
- * General help message
- */
-const generalHelp = helpMessage({
- commands: [
- {
- command: 'build:android',
- description: 'build and publish Android brownfield artifacts',
- hasOptions: true,
- },
- {
- command: 'build:ios',
- description: 'build iOS brownfield artifacts',
- hasOptions: true,
- },
- {
- command: 'tasks:android',
- description: 'list available publishing tasks and repositories for android',
- hasOptions: true,
- },
- ],
- options: [
- {
- description: 'output the version number',
- option: '--version',
- short: '-v',
- },
- helpOption(),
- ],
-});
-
-/**
- * Help message for 'build:android' command
- */
-const buildAndroidHelp = helpMessage({
- promptCommand: 'build:android',
- options: [
- helpOption("'build:android'"),
- ...commonBuildOptions,
- {
- description: 'build both debug and release configurations',
- option: '--all',
- short: '-a',
- },
- {
- description: 'maven repository for publishing artifacts (multiple can be passed)',
- option: '--repository',
- short: '--repo',
- },
- {
- description: 'publishing task to be run (multiple can be passed)',
- option: '--task',
- short: '-t',
- },
- {
- description: 'name of the brownfield library',
- option: '--library',
- short: '-l',
- },
- ],
-});
-
-const tasksAndroidHelp = helpMessage({
- promptCommand: 'tasks:android',
- options: [
- helpOption("'tasks:android'"),
- {
- description: 'output all subcommands output to the terminal',
- option: '--verbose',
- },
- {
- description: 'name of the brownfield library',
- option: '--library',
- short: '-l',
- },
- ],
-});
-
-/**
- * Help message for 'build:ios' command
- */
-const buildIosHelp = helpMessage({
- promptCommand: 'build:ios',
- options: [
- helpOption("'build:ios'"),
- ...commonBuildOptions,
- {
- description: 'path to artifacts directory',
- option: '--artifacts',
- short: '-a',
- },
- {
- description: 'scheme to be built',
- option: '--scheme',
- short: '-s',
- },
- {
- description: 'path to Xcode workspace (.xcworkspace)',
- option: '--xcworkspace',
- short: '-x',
- },
- ],
-});
-
-/**
- * Help messages
- */
-export const Help = {
- Android: buildAndroidHelp,
- General: generalHelp,
- IOS: buildIosHelp,
- TasksAndroid: tasksAndroidHelp,
-} as const;
diff --git a/packages/expo-brownfield/cli/src/constants/index.ts b/packages/expo-brownfield/cli/src/constants/index.ts
deleted file mode 100644
index 1530f9ad068a95..00000000000000
--- a/packages/expo-brownfield/cli/src/constants/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './args';
-export * from './defaults';
-export * from './errors';
-export * from './help';
-export * from './output';
diff --git a/packages/expo-brownfield/cli/src/constants/output.ts b/packages/expo-brownfield/cli/src/constants/output.ts
deleted file mode 100644
index 2c5fdeb6d9fb33..00000000000000
--- a/packages/expo-brownfield/cli/src/constants/output.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Constants for output formatting.
- */
-export const Output = {
- /**
- * The spacing between the beginning of the help line and the description
- */
- HelpSpacing: 30,
-} as const;
diff --git a/packages/expo-brownfield/cli/src/index.ts b/packages/expo-brownfield/cli/src/index.ts
index 7bb7fcf1b11bd8..e6c2905e65c814 100644
--- a/packages/expo-brownfield/cli/src/index.ts
+++ b/packages/expo-brownfield/cli/src/index.ts
@@ -1,7 +1,57 @@
#!/usr/bin/env node
-import { resolveCommand } from './commands/resolve';
+import { Command } from 'commander';
-(async () => {
- const command = resolveCommand();
- await command.run();
-})();
+import { buildAndroid, buildIos, tasksAndroid } from './commands';
+import packageJson from '../../package.json';
+
+const program = new Command();
+
+// main program
+program.name('expo-brownfield').version(packageJson.version, '-v, --version');
+
+// build:android
+program
+ .command('build:android')
+ .description('Build and publish Android brownfield artifacts')
+ .option('-d, --debug', 'build debug variant')
+ .option('-r, --release', 'build release variant')
+ .option('-a, --all', 'build both debug and release variants')
+ .option('--verbose', 'forward all output to the terminal')
+ .option('-l, --library ', 'name of the brownfield library')
+ .option('-t, --task ', 'publishing task to be run (multiple can be passed)')
+ .option(
+ '--repo, --repository ',
+ 'repository to publish to (multiple can be passed)'
+ )
+ .option('--dry-run', 'only print the commands without executing them')
+ .action(async function (this: Command) {
+ await buildAndroid(this);
+ });
+
+// build:ios
+program
+ .command('build:ios')
+ .description('Build and publish iOS brownfield artifacts')
+ .option('-d, --debug', 'build debug configuration')
+ .option('-r, --release', 'build release configuration')
+ .option('--verbose', 'forward all output to the terminal')
+ .option('-s, --scheme ', 'name of the iOS scheme')
+ .option('-x, --xcworkspace ', 'path to the Xcode workspace (.xcworkspace)')
+ .option('-a, --artifacts ', 'path to the artifacts directory')
+ .option('--dry-run', 'only print the commands without executing them')
+ .action(async function (this: Command) {
+ await buildIos(this);
+ });
+
+// tasks:android
+program
+ .command('tasks:android')
+ .description('List available publishing tasks and repositories for Android')
+ .option('--verbose', 'forward all output to the terminal')
+ .option('-l, --library ', 'name of the brownfield library')
+ .option('--dry-run', 'only print the commands without executing them')
+ .action(async function (this: Command) {
+ await tasksAndroid(this);
+ });
+
+program.parse();
diff --git a/packages/expo-brownfield/cli/src/utils/android.ts b/packages/expo-brownfield/cli/src/utils/android.ts
new file mode 100644
index 00000000000000..b1ca8ad1191cbe
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/utils/android.ts
@@ -0,0 +1,99 @@
+import chalk from 'chalk';
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { runCommand } from './commands';
+import CLIError from './error';
+import { withSpinner } from './spinner';
+import type { AndroidConfig, BuildVariant } from './types';
+
+export const buildPublishingTask = (variant: BuildVariant, repository: string): string => {
+ const repositoryName = repository === 'MavenLocal' ? repository : `${repository}Repository`;
+ return `publishBrownfield${variant}PublicationTo${repositoryName}`;
+};
+
+export const findBrownfieldLibrary = (): string | undefined => {
+ try {
+ const androidPath = path.join(process.cwd(), 'android');
+ if (!fs.existsSync(androidPath)) {
+ CLIError.handle('android-directory-not-found');
+ }
+
+ const subdirectories = fs
+ .readdirSync(androidPath, { withFileTypes: true })
+ .filter((item) => item.isDirectory());
+ const brownfieldLibrary = subdirectories.find((directory) => {
+ const directoryPath = path.join(androidPath, directory.name);
+ const files = fs.readdirSync(directoryPath, { recursive: true });
+ return files.some(
+ (file) => typeof file === 'string' && file.endsWith('ReactNativeHostManager.kt')
+ );
+ });
+
+ if (brownfieldLibrary) {
+ return brownfieldLibrary.name;
+ }
+
+ CLIError.handle('android-library-not-found');
+ } catch (error: unknown) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ CLIError.handle('android-library-unknown-error', errorMessage);
+ }
+};
+
+export const printAndroidConfig = (config: AndroidConfig) => {
+ console.log(chalk.bold('Resolved build configuration'));
+ console.log(` - Build variant: ${chalk.blue(config.variant)}`);
+ console.log(` - Library: ${chalk.blue(config.library)}`);
+ console.log(` - Verbose: ${chalk.blue(config.verbose)}`);
+ console.log(` - Dry run: ${chalk.blue(config.dryRun)}`);
+ console.log(` - Tasks:`);
+ config.tasks.forEach((task) => {
+ console.log(` - ${chalk.blue(task)}`);
+ });
+ console.log();
+};
+
+export const processRepositories = (tasks: string[]): string[] => {
+ const splitRegex = /^publishBrownfield(?:All|Debug|Release)PublicationTo(.+?)(?:Repository)?$/;
+ return Array.from(
+ new Set(
+ tasks
+ .map((task) => {
+ return splitRegex.exec(task)?.[1];
+ })
+ .filter((repo) => repo !== undefined)
+ )
+ );
+};
+
+export const processTasks = (stdout: string): string[] => {
+ const regex = /^publishBrownfield[a-zA-Z0-9_-]*/i;
+ return (
+ stdout
+ .split('\n')
+ .map((line) => regex.exec(line)?.[0])
+ // Remove duplicate maven local tasks
+ .filter((task) => task !== undefined)
+ .filter((task) => !task.includes('MavenLocalRepository'))
+ );
+};
+
+export const runTask = async (task: string, verbose: boolean, dryRun: boolean) => {
+ if (dryRun) {
+ console.log(`./gradlew ${task}`);
+ return;
+ }
+
+ return withSpinner({
+ operation: () =>
+ runCommand('./gradlew', [task], {
+ cwd: path.join(process.cwd(), 'android'),
+ verbose,
+ }),
+ loaderMessage: 'Running task: ' + task,
+ successMessage: 'Running task: ' + task + ' succeeded',
+ errorMessage: 'Running task: ' + task + ' failed',
+ verbose,
+ });
+};
diff --git a/packages/expo-brownfield/cli/src/utils/args.ts b/packages/expo-brownfield/cli/src/utils/args.ts
deleted file mode 100644
index 0cbd464b96b70f..00000000000000
--- a/packages/expo-brownfield/cli/src/utils/args.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import arg, { type Result, type Spec } from 'arg';
-
-import { Errors } from '../constants';
-import type { ParseArgsParams } from './types';
-
-export const parseArgs = ({ spec, argv, stopAtPositional }: ParseArgsParams) => {
- try {
- const parsed = arg(spec, { argv, stopAtPositional });
- return parsed;
- } catch (error: unknown) {
- if (error instanceof arg.ArgError) {
- return Errors.unknownOption(error);
- }
-
- return Errors.generic(error);
- }
-};
-
-export const getCommand = (args: Result): string => {
- if ('_' in args && args['_'].length > 0) {
- return args['_'][0];
- }
-
- return '';
-};
diff --git a/packages/expo-brownfield/cli/src/utils/build.ts b/packages/expo-brownfield/cli/src/utils/build.ts
deleted file mode 100644
index a660472f336216..00000000000000
--- a/packages/expo-brownfield/cli/src/utils/build.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import chalk from 'chalk';
-import fs from 'node:fs/promises';
-import ora, { type Ora } from 'ora';
-import path from 'path';
-import prompts from 'prompts';
-
-import { runCommand } from './commands';
-import { BuildConfigAndroid, BuildConfigIos, WithSpinnerParams } from './types';
-import { Errors } from '../constants';
-
-const isBuildConfigAndroid = (
- config: BuildConfigAndroid | BuildConfigIos
-): config is BuildConfigAndroid => {
- return 'libraryName' in config;
-};
-
-export const printConfig = (config: BuildConfigAndroid | BuildConfigIos) => {
- console.log(chalk.bold('Build configuration:'));
- console.log(`- Verbose: ${config.verbose}`);
-
- if (isBuildConfigAndroid(config)) {
- console.log(
- `- Build type: ${config.buildType.charAt(0).toUpperCase() + config.buildType.slice(1)}`
- );
- console.log(`- Brownfield library: ${config.libraryName}`);
- console.log(
- `- Repositories: ${config.repositories.length > 0 ? config.repositories.join(', ') : '[]'}`
- );
- console.log(`- Tasks: ${config.tasks.length > 0 ? config.tasks.join(', ') : '[]'}`);
- } else {
- console.log(`- Artifacts directory: ${config.artifacts}`);
- console.log(
- `- Build type: ${config.buildType.charAt(0).toUpperCase() + config.buildType.slice(1)}`
- );
- console.log(`- Xcode Scheme: ${config.scheme}`);
- console.log(`- Xcode Workspace: ${config.workspace}`);
- }
-
- console.log('');
-};
-
-export const withSpinner = async ({
- operation,
- loaderMessage,
- successMessage,
- errorMessage,
- onError = 'error',
- verbose = false,
-}: WithSpinnerParams) => {
- let spinner: Ora | undefined;
-
- try {
- if (!verbose) {
- spinner = ora(loaderMessage).start();
- }
-
- const result = await operation();
-
- if (!verbose) {
- spinner?.succeed(successMessage);
- }
-
- return result;
- } catch (error) {
- if (!verbose) {
- onError === 'error' ? spinner?.fail(errorMessage) : spinner?.warn(errorMessage);
- }
-
- return Errors.generic(error);
- } finally {
- if (!verbose && spinner?.isSpinning) {
- spinner?.stop();
- }
- }
-};
-
-export const checkPrebuild = async (platform: 'android' | 'ios'): Promise => {
- const nativeProjectPath = path.join(process.cwd(), platform);
- try {
- await fs.access(nativeProjectPath);
- } catch (error) {
- return false;
- }
-
- return true;
-};
-
-export const maybeRunPrebuild = async (platform: 'android' | 'ios') => {
- console.info(`${chalk.yellow('⚠')} Prebuild for platform: ${platform} is missing`);
- const response = await prompts({
- type: 'confirm',
- name: 'shouldRunPrebuild',
- message: 'Do you want to run the prebuild now?',
- initial: false,
- });
-
- if (response.shouldRunPrebuild) {
- return withSpinner({
- operation: () => runCommand('npx', ['expo', 'prebuild', '--platform', platform]),
- loaderMessage: `Running 'npx expo prebuild' for platform: ${platform}...`,
- successMessage: `Prebuild for ${platform} completed\n`,
- errorMessage: `Prebuild for ${platform} failed`,
- verbose: false,
- });
- } else {
- console.error(`${chalk.red('✖')} Brownfield cannot be built without prebuild`);
- return process.exit(1);
- }
-};
-
-export const ensurePrebuild = async (platform: 'android' | 'ios') => {
- if (!(await checkPrebuild(platform))) {
- await maybeRunPrebuild(platform);
- }
-};
diff --git a/packages/expo-brownfield/cli/src/utils/config.ts b/packages/expo-brownfield/cli/src/utils/config.ts
index 5f7c4708fe3707..4602725b68b184 100644
--- a/packages/expo-brownfield/cli/src/utils/config.ts
+++ b/packages/expo-brownfield/cli/src/utils/config.ts
@@ -1,70 +1,119 @@
-import type { Result, Spec } from 'arg';
-import path from 'path';
+import type { OptionValues } from 'commander';
+import path from 'node:path';
+import { buildPublishingTask, findBrownfieldLibrary } from './android';
+import { findScheme, findWorkspace } from './ios';
import type {
- BuildConfigAndroid,
- BuildConfigCommon,
- BuildConfigIos,
- BuildTypeAndroid,
- BuildTypeCommon,
+ AndroidConfig,
+ BuildConfiguration,
+ BuildVariant,
+ CommonConfig,
+ IosConfig,
+ TasksConfigAndroid,
} from './types';
-import { Defaults } from '../constants';
-import { inferAndroidLibrary, inferScheme, inferXCWorkspace } from './infer';
-export const getCommonConfig = (args: Result): BuildConfigCommon => {
+export const resolveBuildConfigAndroid = (options: OptionValues): AndroidConfig => {
+ const variant = resolveVariant(options);
return {
- dryRun: !!args['--dry-run'],
- help: !!args['--help'],
- verbose: !!args['--verbose'],
+ ...resolveCommonConfig(options),
+ library: resolveLibrary(options),
+ tasks: resolveTaskArray(options, variant),
+ variant,
};
};
-export const getAndroidConfig = async (args: Result): Promise => {
- return {
- ...getCommonConfig(args),
- buildType: getBuildTypeAndroid(args),
- libraryName: args['--library'] || (await inferAndroidLibrary()),
- repositories: args['--repository'] || [],
- tasks: args['--task'] || [],
- };
-};
+export const resolveBuildConfigIos = (options: OptionValues): IosConfig => {
+ let artifacts = options.artifacts || './artifacts';
+ if (!path.isAbsolute(artifacts)) {
+ artifacts = path.join(process.cwd(), artifacts);
+ }
-export const getIosConfig = async (args: Result): Promise => {
- const buildType = getBuildTypeCommon(args);
const derivedDataPath = path.join(process.cwd(), 'ios/build');
const buildProductsPath = path.join(derivedDataPath, 'Build/Products');
+ const buildConfiguration = resolveBuildConfiguration(options);
+ const device = path.join(buildProductsPath, `${buildConfiguration.toLowerCase()}-iphoneos`);
+ const simulator = path.join(
+ buildProductsPath,
+ `${buildConfiguration.toLowerCase()}-iphonesimulator`
+ );
+
+ const hermesFrameworkPath =
+ 'Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework';
+
return {
- ...getCommonConfig(args),
- artifacts: path.join(process.cwd(), args['--artifacts'] || Defaults.artifactsPath),
- buildType,
+ ...resolveCommonConfig(options),
+ artifacts,
+ buildConfiguration,
derivedDataPath,
- device: path.join(buildProductsPath, `${buildType}-iphoneos`),
- simulator: path.join(buildProductsPath, `${buildType}-iphonesimulator`),
- hermesFrameworkPath: args['--hermes-framework'] || Defaults.hermesFrameworkPath,
- scheme: args['--scheme'] || (await inferScheme()),
- workspace: args['--xcworkspace'] || (await inferXCWorkspace()),
+ device,
+ simulator,
+ hermesFrameworkPath,
+ scheme: resolveScheme(options),
+ workspace: resolveWorkspace(options),
};
};
-export const getTasksAndroidConfig = async (args: Result) => {
- const commonConfig = getCommonConfig(args);
- const libraryName = !commonConfig.help ? args['--library'] || (await inferAndroidLibrary()) : '';
+export const resolveTasksConfigAndroid = (options: OptionValues): TasksConfigAndroid => {
+ return {
+ ...resolveCommonConfig(options),
+ library: resolveLibrary(options),
+ };
+};
+const resolveCommonConfig = (options: OptionValues): CommonConfig => {
return {
- ...commonConfig,
- libraryName,
+ dryRun: !!options.dryRun,
+ verbose: !!options.verbose,
};
};
-export const getBuildTypeCommon = (args: Result): BuildTypeCommon => {
- return !args['--release'] && args['--debug'] ? 'debug' : 'release';
+// SECTION: Android Helpers
+
+const resolveLibrary = (options: OptionValues): string => {
+ return options.library || findBrownfieldLibrary();
+};
+
+const resolveTaskArray = (options: OptionValues, variant: BuildVariant): string[] => {
+ const tasks: string[] = options.task ?? [];
+ const repoTasks = (options.repository ?? []).map((repo: string) =>
+ buildPublishingTask(variant, repo)
+ );
+
+ return Array.from(new Set([...tasks, ...repoTasks]));
};
-export const getBuildTypeAndroid = (args: Result): BuildTypeAndroid => {
- if ((args['--debug'] && args['--release']) || (!args['--debug'] && !args['--release'])) {
- return 'all';
+const resolveVariant = (options: OptionValues): BuildVariant => {
+ let variant: BuildVariant = 'All';
+ if (options.release && !options.debug) {
+ variant = 'Release';
}
+ if (options.debug && !options.release) {
+ variant = 'Debug';
+ }
+
+ return variant;
+};
+
+// END SECTION: Android Helpers
+
+// SECTION: iOS Helpers
+
+const resolveBuildConfiguration = (options: OptionValues): BuildConfiguration => {
+ let buildConfiguration: BuildConfiguration = 'Release';
+ if (options.debug && !options.release) {
+ buildConfiguration = 'Debug';
+ }
+
+ return buildConfiguration;
+};
- return getBuildTypeCommon(args);
+const resolveScheme = (options: OptionValues): string => {
+ return options.scheme || findScheme();
};
+
+const resolveWorkspace = (options: OptionValues): string => {
+ return options.xcworkspace || findWorkspace();
+};
+
+// END SECTION: iOS Helpers
diff --git a/packages/expo-brownfield/cli/src/utils/error.ts b/packages/expo-brownfield/cli/src/utils/error.ts
new file mode 100644
index 00000000000000..85ff15372a4bef
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/utils/error.ts
@@ -0,0 +1,48 @@
+type ErrorType =
+ | 'android-task-repo'
+ | 'android-directory-not-found'
+ | 'android-library-unknown-error'
+ | 'android-library-not-found'
+ | 'ios-artifacts-directory-unknown-error'
+ | 'ios-directory-not-found'
+ | 'ios-directory-unknown-error'
+ | 'ios-hermes-framework-not-found'
+ | 'ios-scheme-not-found'
+ | 'ios-workspace-not-found'
+ | 'ios-workspace-unknown-error'
+ | 'prebuild-cancelled';
+
+class CLIError {
+ private static readonly errorSymbol: string = '✖';
+ private static readonly errorMessages: Record = {
+ 'android-task-repo': 'At least one task or repository must be specified',
+ 'android-directory-not-found': 'Cannot find `android` directory in the project',
+ 'android-library-unknown-error': 'Unknown error occurred while finding brownfield library',
+ 'android-library-not-found': 'Could not find brownfield library in the project',
+ 'ios-artifacts-directory-unknown-error':
+ 'Unknown error occurred while creating artifacts directory',
+ 'ios-directory-not-found': 'Cannot find `ios` directory in the project',
+ 'ios-directory-unknown-error': 'Unknown error occurred while finding brownfield iOS scheme',
+ 'ios-hermes-framework-not-found': 'Could not find hermes framework in the project at path',
+ 'ios-scheme-not-found': 'Could not find brownfield iOS scheme',
+ 'ios-workspace-not-found': 'Could not find brownfield iOS workspace',
+ 'ios-workspace-unknown-error': 'Unknown error occurred while finding brownfield iOS workspace',
+ 'prebuild-cancelled': 'Brownfield cannot be built without prebuilding the native project',
+ };
+
+ public static handle(error: ErrorType, errorMessage: string = '', fatal: boolean = true) {
+ const message = errorMessage
+ ? `${this.errorMessages[error]}: ${errorMessage}`
+ : this.errorMessages[error];
+ this.handleInternal(message, fatal);
+ }
+
+ private static handleInternal(message: string, fatal: boolean = true) {
+ console.error(`${this.errorSymbol} Error: ${message}`);
+ if (fatal) {
+ process.exit(1);
+ }
+ }
+}
+
+export default CLIError;
diff --git a/packages/expo-brownfield/cli/src/utils/help.ts b/packages/expo-brownfield/cli/src/utils/help.ts
deleted file mode 100644
index 6a005144eb9b06..00000000000000
--- a/packages/expo-brownfield/cli/src/utils/help.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import chalk from 'chalk';
-
-import type { HelpMessageParams, HelpMessageSectionParams } from './types';
-import { Output } from '../constants/output';
-
-export const helpMessage = ({
- commands,
- options,
- promptCommand = '',
- promptOptions = '',
-}: HelpMessageParams): string => {
- const optionsSection = helpMessageSection({
- items: options,
- left: ({ option, short }) => `${option}${short ? `, ${short}` : ''}`,
- right: ({ description }) => description,
- title: 'Options:',
- });
-
- const commandsSection = helpMessageSection({
- items: commands,
- left: ({ command, hasOptions }) => `${command}${hasOptions ? ` [${promptOptions}]` : ''}`,
- right: ({ description }) => description,
- title: 'Commands:',
- });
-
- const usageSection = `${chalk.bold('Usage:')} expo-brownfield ${promptCommand} [${promptOptions}]`;
-
- return `\n${usageSection}${optionsSection}${commandsSection}\n`;
-};
-
-export const helpMessageSection = ({
- items,
- left,
- right,
- title,
-}: HelpMessageSectionParams): string => {
- if (!items) {
- return '';
- }
-
- const content = items.reduce((acc, item) => {
- const ls = left(item);
- const rs = right(item);
- const spacing = ' '.repeat(Output.HelpSpacing - ls.length);
- return `${acc}\n ${ls}${spacing}${rs}`;
- }, '');
-
- return `\n\n${chalk.bold(title)}${content}`;
-};
diff --git a/packages/expo-brownfield/cli/src/utils/index.ts b/packages/expo-brownfield/cli/src/utils/index.ts
index 58abe64a5ab08f..d47b08a1c809ce 100644
--- a/packages/expo-brownfield/cli/src/utils/index.ts
+++ b/packages/expo-brownfield/cli/src/utils/index.ts
@@ -1,6 +1,8 @@
-export * from './args';
-export * from './build';
+export * from './android';
export * from './commands';
export * from './config';
-export * from './help';
+export { default as CLIError } from './error';
+export * from './ios';
+export * from './prebuild';
+export * from './spinner';
export type * from './types';
diff --git a/packages/expo-brownfield/cli/src/utils/infer.ts b/packages/expo-brownfield/cli/src/utils/infer.ts
deleted file mode 100644
index 291b3662b0daff..00000000000000
--- a/packages/expo-brownfield/cli/src/utils/infer.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import fs from 'node:fs/promises';
-import path from 'path';
-
-import { Errors } from '../constants';
-
-export const inferAndroidLibrary = async (): Promise => {
- const files = ['ReactNativeFragment.kt', 'ReactNativeHostManager.kt'];
-
- try {
- const androidPath = path.join(process.cwd(), 'android');
- await fs.access(androidPath);
-
- const android = await fs.readdir(androidPath, { withFileTypes: true });
- const directories = android.filter((item) => item.isDirectory());
- if (directories.length === 0) {
- throw new Error('No directories found in android/ folder');
- }
-
- for (const directory of directories) {
- const libraryPath = path.join(androidPath, directory.name);
- try {
- const contents = await fs.readdir(libraryPath, {
- recursive: true,
- });
- const hasAllFiles = files.every((file) => contents.some((item) => item.includes(file)));
- if (hasAllFiles) {
- return directory.name;
- }
- } catch (readError) {
- continue;
- }
- }
-
- throw new Error('Unable to find brownfield Android library');
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Unknown error';
- return Errors.inference('Android library name: ' + message);
- }
-};
-
-export const inferXCWorkspace = async (): Promise => {
- try {
- const iosPath = path.join(process.cwd(), 'ios');
- await fs.access(iosPath);
-
- const xcworkspace = (await fs.readdir(iosPath, { withFileTypes: true })).find((item) =>
- item.name.endsWith('.xcworkspace')
- );
- if (xcworkspace) {
- return path.join(iosPath, xcworkspace.name);
- }
-
- throw new Error('Unable to find brownfield iOS Workspace (.xcworkspace)');
- } catch (error) {
- return Errors.inference('iOS Workspace (.xcworkspace)');
- }
-};
-
-export const inferScheme = async (): Promise => {
- try {
- const iosPath = path.join(process.cwd(), 'ios');
- await fs.access(iosPath);
-
- const subDirs = (await fs.readdir(iosPath, { withFileTypes: true })).filter((item) =>
- item.isDirectory()
- );
-
- for (const subDir of subDirs) {
- try {
- const subDirPath = path.join(iosPath, subDir.name);
- const contents = await fs.readdir(subDirPath);
- if (contents.includes('ReactNativeHostManager.swift')) {
- return subDir.name;
- }
- } catch (readError) {
- continue;
- }
- }
-
- throw new Error('Unable to find brownfield iOS group');
- } catch (error) {
- return Errors.inference('iOS Scheme');
- }
-};
diff --git a/packages/expo-brownfield/cli/src/utils/ios.ts b/packages/expo-brownfield/cli/src/utils/ios.ts
new file mode 100644
index 00000000000000..9d22f44e87ad91
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/utils/ios.ts
@@ -0,0 +1,186 @@
+import chalk from 'chalk';
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { runCommand } from './commands';
+import CLIError from './error';
+import { withSpinner } from './spinner';
+import { IosConfig } from './types';
+
+export const cleanUpArtifacts = async (config: IosConfig) => {
+ if (config.dryRun) {
+ console.log('Cleaning up previous artifacts');
+ return;
+ }
+
+ return withSpinner({
+ operation: async () => {
+ if (!fs.existsSync(config.artifacts)) {
+ return;
+ }
+
+ const xcframeworks = fs
+ .readdirSync(config.artifacts)
+ .filter((item) => item.endsWith('.xcframework'));
+ xcframeworks.forEach((item) => {
+ const itemPath = `${config.artifacts}/${item}`;
+ fs.rmSync(itemPath, { recursive: true, force: true });
+ });
+ },
+ loaderMessage: 'Cleaning up previous artifacts...',
+ successMessage: 'Cleaning up previous artifacts succeeded',
+ errorMessage: 'Cleaning up previous artifacts failed',
+ });
+};
+
+export const buildFramework = async (config: IosConfig) => {
+ const args = [
+ '-workspace',
+ config.workspace,
+ '-scheme',
+ config.scheme,
+ '-derivedDataPath',
+ config.derivedDataPath,
+ '-destination',
+ 'generic/platform=iphoneos',
+ '-destination',
+ 'generic/platform=iphonesimulator',
+ '-configuration',
+ config.buildConfiguration,
+ ];
+
+ if (config.dryRun) {
+ console.log(`xcodebuild ${args.join(' ')}`);
+ return;
+ }
+
+ return withSpinner({
+ operation: () => runCommand('xcodebuild', args, { verbose: config.verbose }),
+ loaderMessage: 'Compiling framework...',
+ successMessage: 'Compiling framework succeeded',
+ errorMessage: 'Compiling framework failed',
+ verbose: config.verbose,
+ });
+};
+
+export const copyHermesXcframework = async (config: IosConfig) => {
+ if (config.dryRun) {
+ console.log(
+ `Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${config.artifacts}/hermes.xcframework`
+ );
+ return;
+ }
+
+ const sourcePath = `./ios/${config.hermesFrameworkPath}`;
+ if (!fs.existsSync(sourcePath)) {
+ CLIError.handle('ios-hermes-framework-not-found', sourcePath);
+ }
+
+ return withSpinner({
+ operation: async () =>
+ fs.cpSync(sourcePath, `${config.artifacts}/hermesvm.xcframework`, {
+ force: true,
+ recursive: true,
+ }),
+ loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...',
+ successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded',
+ errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed',
+ verbose: config.verbose,
+ });
+};
+
+export const createXcframework = async (config: IosConfig) => {
+ const args = [
+ '-create-xcframework',
+ '-framework',
+ `${config.device}/${config.scheme}.framework`,
+ '-framework',
+ `${config.simulator}/${config.scheme}.framework`,
+ '-output',
+ `${config.artifacts}/${config.scheme}.xcframework`,
+ ];
+
+ if (config.dryRun) {
+ console.log(`xcodebuild ${args.join(' ')}`);
+ return;
+ }
+
+ return withSpinner({
+ operation: () => runCommand('xcodebuild', args, { verbose: config.verbose }),
+ loaderMessage: 'Packaging framework into an XCFramework...',
+ successMessage: 'Packaging framework into an XCFramework succeeded',
+ errorMessage: 'Packaging framework into an XCFramework failed',
+ verbose: config.verbose,
+ });
+};
+
+export const findScheme = (): string | undefined => {
+ try {
+ const iosPath = path.join(process.cwd(), 'ios');
+ if (!fs.existsSync(iosPath)) {
+ CLIError.handle('ios-directory-not-found');
+ }
+
+ const subdirectories = fs
+ .readdirSync(iosPath, { withFileTypes: true })
+ .filter((item) => item.isDirectory());
+ const scheme = subdirectories.find((directory) => {
+ const directoryPath = path.join(iosPath, directory.name);
+ const files = fs.readdirSync(directoryPath, { recursive: true });
+ return files.some(
+ (file) => typeof file === 'string' && file.endsWith('ReactNativeHostManager.swift')
+ );
+ });
+
+ if (scheme) {
+ return scheme.name;
+ }
+
+ CLIError.handle('ios-scheme-not-found');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ CLIError.handle('ios-directory-unknown-error', errorMessage);
+ }
+};
+
+export const findWorkspace = (): string | undefined => {
+ try {
+ const iosPath = path.join(process.cwd(), 'ios');
+ if (!fs.existsSync(iosPath)) {
+ CLIError.handle('ios-directory-not-found');
+ }
+
+ const items = fs.readdirSync(iosPath, { withFileTypes: true });
+ const workspace = items.find((item) => item.name.endsWith('.xcworkspace'));
+ if (workspace) {
+ return path.join(iosPath, workspace.name);
+ }
+
+ CLIError.handle('ios-workspace-not-found');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ CLIError.handle('ios-workspace-unknown-error', errorMessage);
+ }
+};
+
+export const makeArtifactsDirectory = (config: IosConfig) => {
+ try {
+ if (!fs.existsSync(config.artifacts)) {
+ fs.mkdirSync(config.artifacts, { recursive: true });
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '';
+ CLIError.handle('ios-artifacts-directory-unknown-error', errorMessage);
+ }
+};
+
+export const printIosConfig = (config: IosConfig) => {
+ console.log(chalk.bold('Resolved build configuration'));
+ console.log(` - Build configuration: ${chalk.blue(config.buildConfiguration)}`);
+ console.log(` - Scheme: ${chalk.blue(config.scheme)}`);
+ console.log(` - Workspace: ${chalk.blue(config.workspace)}`);
+ console.log(` - Dry run: ${chalk.blue(config.dryRun)}`);
+ console.log(` - Verbose: ${chalk.blue(config.verbose)}`);
+ console.log(` - Artifacts path: ${chalk.blue(config.artifacts)}`);
+ console.log();
+};
diff --git a/packages/expo-brownfield/cli/src/utils/prebuild.ts b/packages/expo-brownfield/cli/src/utils/prebuild.ts
new file mode 100644
index 00000000000000..ddd253297edda1
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/utils/prebuild.ts
@@ -0,0 +1,38 @@
+import chalk from 'chalk';
+import fs from 'node:fs';
+import path from 'node:path';
+import prompts from 'prompts';
+
+import { runCommand } from './commands';
+import CLIError from './error';
+import { withSpinner } from './spinner';
+import type { Platform } from './types';
+
+export const validatePrebuild = async (platform: Platform): Promise => {
+ if (!checkPrebuild(platform)) {
+ console.info(`${chalk.yellow(`⚠ Prebuild for platform: ${platform} is missing`)}`);
+ const response = await prompts({
+ type: 'confirm',
+ name: 'shouldRunPrebuild',
+ message: 'Do you want to run the prebuild now?',
+ initial: false,
+ });
+
+ if (response.shouldRunPrebuild) {
+ await withSpinner({
+ operation: () => runCommand('npx', ['expo', 'prebuild', '--platform', platform]),
+ loaderMessage: `Running 'npx expo prebuild' for platform: ${platform}...`,
+ successMessage: `Prebuild for ${platform} completed\n`,
+ errorMessage: `Prebuild for ${platform} failed`,
+ verbose: false,
+ });
+ } else {
+ CLIError.handle('prebuild-cancelled');
+ }
+ }
+};
+
+const checkPrebuild = (platform: Platform): boolean => {
+ const nativeDirectory = path.join(process.cwd(), platform);
+ return fs.existsSync(nativeDirectory);
+};
diff --git a/packages/expo-brownfield/cli/src/utils/spinner.ts b/packages/expo-brownfield/cli/src/utils/spinner.ts
new file mode 100644
index 00000000000000..c5a1b42ff40c63
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/utils/spinner.ts
@@ -0,0 +1,38 @@
+import ora, { type Ora } from 'ora';
+
+import { WithSpinnerParams } from './types';
+
+export const withSpinner = async ({
+ operation,
+ loaderMessage,
+ successMessage,
+ errorMessage,
+ onError = 'error',
+ verbose = false,
+}: WithSpinnerParams) => {
+ let spinner: Ora | undefined;
+
+ try {
+ if (!verbose) {
+ spinner = ora(loaderMessage).start();
+ }
+
+ const result = await operation();
+
+ if (!verbose) {
+ spinner?.succeed(successMessage);
+ }
+
+ return result;
+ } catch (error) {
+ if (!verbose) {
+ onError === 'error' ? spinner?.fail(errorMessage) : spinner?.warn(errorMessage);
+ }
+
+ throw new Error(errorMessage);
+ } finally {
+ if (!verbose && spinner?.isSpinning) {
+ spinner?.stop();
+ }
+ }
+};
diff --git a/packages/expo-brownfield/cli/src/utils/types.ts b/packages/expo-brownfield/cli/src/utils/types.ts
index 2f1498bff2ebf7..84f03968d9a4f2 100644
--- a/packages/expo-brownfield/cli/src/utils/types.ts
+++ b/packages/expo-brownfield/cli/src/utils/types.ts
@@ -1,57 +1,40 @@
-import type { Spec } from 'arg';
+export type Platform = 'android' | 'ios';
-export interface HelpMessageCommand {
- command: string;
- description: string;
- hasOptions?: boolean;
-}
-
-export interface HelpMessageOption {
- description: string;
- option: string;
- short?: string;
-}
-
-export interface HelpMessageParams {
- commands?: HelpMessageCommand[];
- options?: HelpMessageOption[];
- promptCommand?: string;
- promptOptions?: string;
+export interface RunCommandOptions {
+ cwd?: string;
+ env?: Record;
+ verbose?: boolean;
}
-export interface HelpMessageSectionParams {
- items?: T[];
- left: (item: T) => string;
- right: (item: T) => string;
- title: string;
+export interface RunCommandResult {
+ stdout: string;
}
-export interface ParseArgsParams {
- spec: Spec;
- argv?: string[];
- stopAtPositional?: boolean;
+export interface WithSpinnerParams {
+ operation: () => Promise;
+ loaderMessage: string;
+ successMessage?: string;
+ errorMessage?: string;
+ onError?: 'error' | 'warn';
+ verbose?: boolean;
}
-export type BuildTypeCommon = 'debug' | 'release';
-
-export type BuildTypeAndroid = BuildTypeCommon | 'all';
-
-export interface BuildConfigCommon {
+export interface CommonConfig {
dryRun: boolean;
- help: boolean;
verbose: boolean;
}
-export interface BuildConfigAndroid extends BuildConfigCommon {
- buildType: BuildTypeAndroid;
- libraryName: string;
- repositories: string[];
+export type BuildVariant = 'All' | 'Debug' | 'Release';
+export interface AndroidConfig extends CommonConfig {
+ library: string;
tasks: string[];
+ variant: BuildVariant;
}
-export interface BuildConfigIos extends BuildConfigCommon {
+export type BuildConfiguration = 'Debug' | 'Release';
+export interface IosConfig extends CommonConfig {
artifacts: string;
- buildType: BuildTypeCommon;
+ buildConfiguration: BuildConfiguration;
derivedDataPath: string;
device: string;
hermesFrameworkPath: string;
@@ -60,21 +43,6 @@ export interface BuildConfigIos extends BuildConfigCommon {
workspace: string;
}
-export interface RunCommandOptions {
- cwd?: string;
- env?: Record;
- verbose?: boolean;
-}
-
-export interface RunCommandResult {
- stdout: string;
-}
-
-export interface WithSpinnerParams {
- operation: () => Promise;
- loaderMessage: string;
- successMessage?: string;
- errorMessage?: string;
- onError?: 'error' | 'warn';
- verbose?: boolean;
+export interface TasksConfigAndroid extends CommonConfig {
+ library: string;
}
diff --git a/packages/expo-brownfield/cli/tsconfig.json b/packages/expo-brownfield/cli/tsconfig.json
index 354bddb4336eb1..1400b4cffc3e8f 100644
--- a/packages/expo-brownfield/cli/tsconfig.json
+++ b/packages/expo-brownfield/cli/tsconfig.json
@@ -2,7 +2,8 @@
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
- "rootDir": "src"
+ "rootDir": "src",
+ "resolveJsonModule": true
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-android.test.ts.snap b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-android.test.ts.snap
index 6916fb1caf50cb..0f4717e0952c23 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-android.test.ts.snap
+++ b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-android.test.ts.snap
@@ -1,29 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`build:android command without prebuild should display help message for --help/-h option 1`] = `
-"Usage: expo-brownfield build:android []
+exports[`build:android command with prebuild should infer and print build configuration 1`] = `
+"Resolved build configuration
+ - Build variant: All
+ - Library: brownfield
+ - Verbose: false
+ - Dry run: true
+ - Tasks:
+ - someGradleTask
+
+./gradlew someGradleTask"
+`;
+
+exports[`build:android command without prebuild should display help message for --help/-h/help option 1`] = `
+"Usage: expo-brownfield build:android [options]
+
+Build and publish Android brownfield artifacts
+
+Options:
+ -d, --debug build debug variant
+ -r, --release build release variant
+ -a, --all build both debug and release variants
+ --verbose forward all output to the terminal
+ -l, --library name of the brownfield library
+ -t, --task publishing task to be run (multiple can
+ be passed)
+ --repo, --repository repository to publish to (multiple can
+ be passed)
+ --dry-run only print the commands without
+ executing them
+ -h, --help display help for command"
+`;
+
+exports[`build:android command without prebuild should display help message for --help/-h/help option 2`] = `
+"Usage: expo-brownfield build:android [options]
+
+Build and publish Android brownfield artifacts
Options:
- --help, -h display help for 'build:android'
- --debug, -d build in debug configuration
- --release, -r build in release configuration
- --verbose forward all output to the terminal
- --all, -a build both debug and release configurations
- --repository, --repo maven repository for publishing artifacts (multiple can be passed)
- --task, -t publishing task to be run (multiple can be passed)
- --library, -l name of the brownfield library"
+ -d, --debug build debug variant
+ -r, --release build release variant
+ -a, --all build both debug and release variants
+ --verbose forward all output to the terminal
+ -l, --library name of the brownfield library
+ -t, --task publishing task to be run (multiple can
+ be passed)
+ --repo, --repository repository to publish to (multiple can
+ be passed)
+ --dry-run only print the commands without
+ executing them
+ -h, --help display help for command"
`;
-exports[`build:android command without prebuild should display help message for --help/-h option 2`] = `
-"Usage: expo-brownfield build:android []
+exports[`build:android command without prebuild should display help message for --help/-h/help option 3`] = `
+"Usage: expo-brownfield build:android [options]
+
+Build and publish Android brownfield artifacts
Options:
- --help, -h display help for 'build:android'
- --debug, -d build in debug configuration
- --release, -r build in release configuration
- --verbose forward all output to the terminal
- --all, -a build both debug and release configurations
- --repository, --repo maven repository for publishing artifacts (multiple can be passed)
- --task, -t publishing task to be run (multiple can be passed)
- --library, -l name of the brownfield library"
+ -d, --debug build debug variant
+ -r, --release build release variant
+ -a, --all build both debug and release variants
+ --verbose forward all output to the terminal
+ -l, --library name of the brownfield library
+ -t, --task publishing task to be run (multiple can
+ be passed)
+ --repo, --repository repository to publish to (multiple can
+ be passed)
+ --dry-run only print the commands without
+ executing them
+ -h, --help display help for command"
`;
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-ios.test.ts.snap b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-ios.test.ts.snap
index 236b3067bc8dae..0f4d0c8af0774e 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-ios.test.ts.snap
+++ b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/build-ios.test.ts.snap
@@ -1,27 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`build:ios command without prebuild should display help message for --help/-h option 1`] = `
-"Usage: expo-brownfield build:ios []
+exports[`build:ios command without prebuild should display help message for --help/-h/help option 1`] = `
+"Usage: expo-brownfield build:ios [options]
+
+Build and publish iOS brownfield artifacts
Options:
- --help, -h display help for 'build:ios'
- --debug, -d build in debug configuration
- --release, -r build in release configuration
- --verbose forward all output to the terminal
- --artifacts, -a path to artifacts directory
- --scheme, -s scheme to be built
- --xcworkspace, -x path to Xcode workspace (.xcworkspace)"
+ -d, --debug build debug configuration
+ -r, --release build release configuration
+ --verbose forward all output to the terminal
+ -s, --scheme name of the iOS scheme
+ -x, --xcworkspace path to the Xcode workspace (.xcworkspace)
+ -a, --artifacts path to the artifacts directory
+ --dry-run only print the commands without executing
+ them
+ -h, --help display help for command"
`;
-exports[`build:ios command without prebuild should display help message for --help/-h option 2`] = `
-"Usage: expo-brownfield build:ios []
+exports[`build:ios command without prebuild should display help message for --help/-h/help option 2`] = `
+"Usage: expo-brownfield build:ios [options]
+
+Build and publish iOS brownfield artifacts
+
+Options:
+ -d, --debug build debug configuration
+ -r, --release build release configuration
+ --verbose forward all output to the terminal
+ -s, --scheme name of the iOS scheme
+ -x, --xcworkspace path to the Xcode workspace (.xcworkspace)
+ -a, --artifacts path to the artifacts directory
+ --dry-run only print the commands without executing
+ them
+ -h, --help display help for command"
+`;
+
+exports[`build:ios command without prebuild should display help message for --help/-h/help option 3`] = `
+"Usage: expo-brownfield build:ios [options]
+
+Build and publish iOS brownfield artifacts
Options:
- --help, -h display help for 'build:ios'
- --debug, -d build in debug configuration
- --release, -r build in release configuration
- --verbose forward all output to the terminal
- --artifacts, -a path to artifacts directory
- --scheme, -s scheme to be built
- --xcworkspace, -x path to Xcode workspace (.xcworkspace)"
+ -d, --debug build debug configuration
+ -r, --release build release configuration
+ --verbose forward all output to the terminal
+ -s, --scheme name of the iOS scheme
+ -x, --xcworkspace path to the Xcode workspace (.xcworkspace)
+ -a, --artifacts path to the artifacts directory
+ --dry-run only print the commands without executing
+ them
+ -h, --help display help for command"
`;
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/help.test.ts.snap b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/help.test.ts.snap
index 56fb8d7117cc6c..4bbe933aae0aca 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/help.test.ts.snap
+++ b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/help.test.ts.snap
@@ -1,27 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`--help option should return the correct help message 1`] = `
-"Usage: expo-brownfield []
+"Usage: expo-brownfield [options] [command]
Options:
- --version, -v output the version number
- --help, -h display help for command
+ -v, --version output the version number
+ -h, --help display help for command
Commands:
- build:android [] build and publish Android brownfield artifacts
- build:ios [] build iOS brownfield artifacts
- tasks:android [] list available publishing tasks and repositories for android"
+ build:android [options] Build and publish Android brownfield artifacts
+ build:ios [options] Build and publish iOS brownfield artifacts
+ tasks:android [options] List available publishing tasks and repositories for
+ Android
+ help [command] display help for command"
`;
exports[`--help option should support the \`-h\` shorthand 1`] = `
-"Usage: expo-brownfield []
+"Usage: expo-brownfield [options] [command]
Options:
- --version, -v output the version number
- --help, -h display help for command
+ -v, --version output the version number
+ -h, --help display help for command
Commands:
- build:android [] build and publish Android brownfield artifacts
- build:ios [] build iOS brownfield artifacts
- tasks:android [] list available publishing tasks and repositories for android"
+ build:android [options] Build and publish Android brownfield artifacts
+ build:ios [options] Build and publish iOS brownfield artifacts
+ tasks:android [options] List available publishing tasks and repositories for
+ Android
+ help [command] display help for command"
+`;
+
+exports[`--help option should support the \`help\` command 1`] = `
+"Usage: expo-brownfield [options] [command]
+
+Options:
+ -v, --version output the version number
+ -h, --help display help for command
+
+Commands:
+ build:android [options] Build and publish Android brownfield artifacts
+ build:ios [options] Build and publish iOS brownfield artifacts
+ tasks:android [options] List available publishing tasks and repositories for
+ Android
+ help [command] display help for command"
`;
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/tasks-android.test.ts.snap b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/tasks-android.test.ts.snap
index 7b7bc28eeefc00..5d66c3f9b2697f 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/tasks-android.test.ts.snap
+++ b/packages/expo-brownfield/e2e/cli/__tests__/__snapshots__/tasks-android.test.ts.snap
@@ -1,19 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`tasks:android command without prebuild should display help message for --help/-h option 1`] = `
-"Usage: expo-brownfield tasks:android []
+exports[`tasks:android command without prebuild should display help message for --help/-h/help option 1`] = `
+"Usage: expo-brownfield tasks:android [options]
+
+List available publishing tasks and repositories for Android
Options:
- --help, -h display help for 'tasks:android'
- --verbose output all subcommands output to the terminal
- --library, -l name of the brownfield library"
+ --verbose forward all output to the terminal
+ -l, --library name of the brownfield library
+ --dry-run only print the commands without executing them
+ -h, --help display help for command"
`;
-exports[`tasks:android command without prebuild should display help message for --help/-h option 2`] = `
-"Usage: expo-brownfield tasks:android []
+exports[`tasks:android command without prebuild should display help message for --help/-h/help option 2`] = `
+"Usage: expo-brownfield tasks:android [options]
+
+List available publishing tasks and repositories for Android
+
+Options:
+ --verbose forward all output to the terminal
+ -l, --library name of the brownfield library
+ --dry-run only print the commands without executing them
+ -h, --help display help for command"
+`;
+
+exports[`tasks:android command without prebuild should display help message for --help/-h/help option 3`] = `
+"Usage: expo-brownfield tasks:android [options]
+
+List available publishing tasks and repositories for Android
Options:
- --help, -h display help for 'tasks:android'
- --verbose output all subcommands output to the terminal
- --library, -l name of the brownfield library"
+ --verbose forward all output to the terminal
+ -l, --library name of the brownfield library
+ --dry-run only print the commands without executing them
+ -h, --help display help for command"
`;
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/build-android.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/build-android.test.ts
index b91ffb928e0964..4a9cb4ae2fbbcb 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/build-android.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/build-android.test.ts
@@ -1,7 +1,7 @@
import { BUILD, BUILD_ANDROID, ERROR } from '../../utils/output';
-import { executeCommandAsync } from '../../utils/process';
+import { CLI_PATH, executeCommandAsync } from '../../utils/process';
import { cleanUpProject, createTempProject } from '../../utils/project';
-import { buildAndroidTest, expectPrebuild } from '../../utils/test';
+import { buildAndroidTest, buildTestCommon, expectPrebuild } from '../../utils/test';
let TEMP_DIR: string;
let TEMP_DIR_PREBUILD: string;
@@ -27,7 +27,7 @@ describe('build:android command', () => {
* Command: npx expo-brownfield build:android --help/-h
* Expected behavior: The CLI should display the full help message
*/
- it('should display help message for --help/-h option', async () => {
+ it('should display help message for --help/-h/help option', async () => {
// Help message display shouldn't require prebuild
await buildAndroidTest({
directory: TEMP_DIR,
@@ -39,6 +39,12 @@ describe('build:android command', () => {
args: ['-h'],
useSnapshot: true,
});
+ await buildTestCommon({
+ directory: TEMP_DIR,
+ command: 'help',
+ args: ['build:android'],
+ useSnapshot: true,
+ });
});
/**
@@ -67,6 +73,39 @@ describe('build:android command', () => {
});
});
+ /**
+ * Command: npx expo-brownfield build:android --library
+ * Expected behavior: The CLI should display the error message about missing argument
+ * (no need to test for all arguments as it's handled by commander)
+ */
+ it('should fail if argument value is not passed', async () => {
+ await buildAndroidTest({
+ directory: TEMP_DIR,
+ args: ['--library'],
+ successExit: false,
+ stderr: [ERROR.MISSING_ARGUMENT('l', 'library', 'library')],
+ });
+ });
+
+ /**
+ * Command: npx expo-brownfield build:android
+ * Expected behavior: The CLI should fail if prebuild is cancelled
+ */
+ it('should fail if prebuild is cancelled', async () => {
+ // The command fails, because `expo-brownfield` is not added to app.json
+ // But the prebuild should succeed
+ const { exitCode, stdout, stderr } = await executeCommandAsync(
+ TEMP_DIR,
+ 'bash',
+ ['-c', `yes no | node ${CLI_PATH} build:android --repo MavenLocal`],
+ { ignoreErrors: true }
+ );
+ expect(exitCode).not.toBe(0);
+ expect(stdout).toContain(BUILD.PREBUILD_WARNING('android'));
+ expect(stdout).toContain(BUILD.PREBUILD_PROMPT);
+ expect(stderr).toContain(ERROR.MISSING_PREBUILD());
+ });
+
/**
* Command: npx expo-brownfield build:android
* Expected behavior: The CLI should validate and ask for prebuild
@@ -77,21 +116,17 @@ describe('build:android command', () => {
const { exitCode, stdout, stderr } = await executeCommandAsync(
TEMP_DIR,
'bash',
- ['-c', 'yes | npx expo-brownfield build:android'],
+ ['-c', `yes | node ${CLI_PATH} build:android --repo MavenLocal`],
{ ignoreErrors: true }
);
expect(exitCode).not.toBe(0);
expect(stdout).toContain(BUILD.PREBUILD_WARNING('android'));
expect(stdout).toContain(BUILD.PREBUILD_PROMPT);
- // TODO(pmleczek): Refactor CLI error handling
- expect(stderr).toContain(`Error: Value of Android library name`);
- expect(stderr).toContain(`could not be inferred from the project`);
+ expect(stderr).toContain('Could not find brownfield library in the project');
// The android directory should be created and not empty
await expectPrebuild(TEMP_DIR, 'android');
});
-
- // TODO(pmleczek): Verify failure if prebuild is not done
});
/**
@@ -126,7 +161,7 @@ describe('build:android command', () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--task', 'someGradleTask', '--dry-run'],
- stdout: [BUILD_ANDROID.CONFIGURATION],
+ useSnapshot: true,
});
});
@@ -150,17 +185,26 @@ describe('build:android command', () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '--debug'],
- stdout: [BUILD.BUILD_TYPE_DEBUG, `./gradlew publishBrownfieldDebugPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_DEBUG,
+ `./gradlew publishBrownfieldDebugPublicationToMavenLocal`,
+ ],
});
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '--debug'],
- stdout: [BUILD.BUILD_TYPE_DEBUG, `./gradlew publishBrownfieldDebugPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_DEBUG,
+ `./gradlew publishBrownfieldDebugPublicationToMavenLocal`,
+ ],
});
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '-d'],
- stdout: [BUILD.BUILD_TYPE_DEBUG, `./gradlew publishBrownfieldDebugPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_DEBUG,
+ `./gradlew publishBrownfieldDebugPublicationToMavenLocal`,
+ ],
});
});
@@ -174,7 +218,7 @@ describe('build:android command', () => {
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '--release'],
stdout: [
- BUILD.BUILD_TYPE_RELEASE,
+ BUILD_ANDROID.BUILD_VARIANT_RELEASE,
`./gradlew publishBrownfieldReleasePublicationToMavenLocal`,
],
});
@@ -182,7 +226,7 @@ describe('build:android command', () => {
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '-r'],
stdout: [
- BUILD.BUILD_TYPE_RELEASE,
+ BUILD_ANDROID.BUILD_VARIANT_RELEASE,
`./gradlew publishBrownfieldReleasePublicationToMavenLocal`,
],
});
@@ -191,7 +235,7 @@ describe('build:android command', () => {
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '-r'],
stdout: [
- BUILD.BUILD_TYPE_RELEASE,
+ BUILD_ANDROID.BUILD_VARIANT_RELEASE,
`./gradlew publishBrownfieldReleasePublicationToMavenLocal`,
],
});
@@ -207,21 +251,30 @@ describe('build:android command', () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '--all'],
- stdout: [BUILD.BUILD_TYPE_ALL, `./gradlew publishBrownfieldAllPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_ALL,
+ `./gradlew publishBrownfieldAllPublicationToMavenLocal`,
+ ],
});
// Short version: -a
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '-a'],
- stdout: [BUILD.BUILD_TYPE_ALL, `./gradlew publishBrownfieldAllPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_ALL,
+ `./gradlew publishBrownfieldAllPublicationToMavenLocal`,
+ ],
});
// Combination of the two flags: --release/-r + --debug/-d
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--dry-run', '--release', '-d'],
- stdout: [BUILD.BUILD_TYPE_ALL, `./gradlew publishBrownfieldAllPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_ALL,
+ `./gradlew publishBrownfieldAllPublicationToMavenLocal`,
+ ],
});
});
@@ -252,8 +305,8 @@ describe('build:android command', () => {
it('should properly handle --task/-t option(s)', async () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
- args: ['--task', 'task1', '-t', 'task2', '--task', 'task3', '--dry-run'],
- stdout: [BUILD_ANDROID.TASKS, `./gradlew task1`, `./gradlew task2`, `./gradlew task3`],
+ args: ['--task', 'task1', '-t', 'task2', 'task3', '--dry-run'],
+ stdout: [...BUILD_ANDROID.TASKS, `./gradlew task1`, `./gradlew task2`, `./gradlew task3`],
});
});
@@ -264,11 +317,11 @@ describe('build:android command', () => {
it('should properly handle --repo/--repository option(s)', async () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
- args: ['--repo', 'MavenLocal', '--repository', 'CustomLocal', '--dry-run'],
+ args: ['--repo', 'MavenLocal', '--repository', 'CustomLocal', 'RemotePublic', '--dry-run'],
stdout: [
- BUILD_ANDROID.REPOSTORIES,
`./gradlew publishBrownfieldAllPublicationToMavenLocal`,
`./gradlew publishBrownfieldAllPublicationToCustomLocalRepository`,
+ `./gradlew publishBrownfieldAllPublicationToRemotePublicRepository`,
],
});
});
@@ -277,11 +330,15 @@ describe('build:android command', () => {
* Command: npx expo-brownfield build:android --repo MavenLocal --task task1 --dry-run
* Expected behavior: Tasks should take precedence over repositories. Correct task should be executed
*/
- it('tasks should take precedence over repositories', async () => {
+ it('should resolve both tasks and repositories', async () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--task', 'task1', '--dry-run'],
- stdout: [BUILD_ANDROID.TASK, `./gradlew task1`],
+ stdout: [
+ ...BUILD_ANDROID.TASK,
+ `./gradlew task1`,
+ `./gradlew publishBrownfieldAllPublicationToMavenLocal`,
+ ],
});
});
@@ -293,13 +350,16 @@ describe('build:android command', () => {
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'MavenLocal', '--debug', '--dry-run'],
- stdout: [BUILD.BUILD_TYPE_DEBUG, `./gradlew publishBrownfieldDebugPublicationToMavenLocal`],
+ stdout: [
+ BUILD_ANDROID.BUILD_VARIANT_DEBUG,
+ `./gradlew publishBrownfieldDebugPublicationToMavenLocal`,
+ ],
});
await buildAndroidTest({
directory: TEMP_DIR_PREBUILD,
args: ['--repo', 'CustomDir', '--repository', 'CustomLocal', '--release', '--dry-run'],
stdout: [
- BUILD.BUILD_TYPE_RELEASE,
+ BUILD_ANDROID.BUILD_VARIANT_RELEASE,
`./gradlew publishBrownfieldReleasePublicationToCustomDirRepository`,
`./gradlew publishBrownfieldReleasePublicationToCustomLocalRepository`,
],
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts
index 56a2b88ffd87cc..f8ff35ada5df95 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts
@@ -1,9 +1,9 @@
import path from 'path';
import { BUILD, BUILD_IOS, ERROR } from '../../utils/output';
-import { executeCommandAsync } from '../../utils/process';
+import { CLI_PATH, executeCommandAsync } from '../../utils/process';
import { cleanUpProject, createTempProject } from '../../utils/project';
-import { buildIosTest, expectPrebuild } from '../../utils/test';
+import { buildIosTest, buildTestCommon, expectPrebuild } from '../../utils/test';
let TEMP_DIR: string;
let TEMP_DIR_PREBUILD: string;
@@ -30,7 +30,7 @@ describe('build:ios command', () => {
* Command: npx expo-brownfield build:android --help/-h
* Expected behavior: The CLI should display the full help message
*/
- it('should display help message for --help/-h option', async () => {
+ it('should display help message for --help/-h/help option', async () => {
// Help message display shouldn't require prebuild
await buildIosTest({
directory: TEMP_DIR,
@@ -42,6 +42,12 @@ describe('build:ios command', () => {
args: ['-h'],
useSnapshot: true,
});
+ await buildTestCommon({
+ directory: TEMP_DIR,
+ command: 'help',
+ args: ['build:ios'],
+ useSnapshot: true,
+ });
});
/**
@@ -70,6 +76,39 @@ describe('build:ios command', () => {
});
});
+ /**
+ * Command: npx expo-brownfield build:ios --scheme
+ * Expected behavior: The CLI should display the error message about missing argument
+ * (no need to test for all arguments as it's handled by commander)
+ */
+ it('should fail if argument value is not passed', async () => {
+ await buildIosTest({
+ directory: TEMP_DIR,
+ args: ['--scheme'],
+ successExit: false,
+ stderr: [ERROR.MISSING_ARGUMENT('s', 'scheme', 'scheme')],
+ });
+ });
+
+ /**
+ * Command: npx expo-brownfield build:ios
+ * Expected behavior: The CLI should fail if prebuild is cancelled
+ */
+ it('should fail if prebuild is cancelled', async () => {
+ // The command fails, because `expo-brownfield` is not added to app.json
+ // But the prebuild should succeed
+ const { exitCode, stdout, stderr } = await executeCommandAsync(
+ TEMP_DIR,
+ 'bash',
+ ['-c', `yes no | node ${CLI_PATH} build:ios`],
+ { ignoreErrors: true }
+ );
+ expect(exitCode).not.toBe(0);
+ expect(stdout).toContain(BUILD.PREBUILD_WARNING('ios'));
+ expect(stdout).toContain(BUILD.PREBUILD_PROMPT);
+ expect(stderr).toContain(ERROR.MISSING_PREBUILD());
+ });
+
/**
* Command: npx expo-brownfield build:ios
* Expected behavior: The CLI should validate and ask for prebuild
@@ -80,21 +119,17 @@ describe('build:ios command', () => {
const { exitCode, stdout, stderr } = await executeCommandAsync(
TEMP_DIR,
'bash',
- ['-c', 'yes | npx expo-brownfield build:ios'],
+ ['-c', `yes | node ${CLI_PATH} build:ios`],
{ ignoreErrors: true }
);
expect(exitCode).not.toBe(0);
expect(stdout).toContain(BUILD.PREBUILD_WARNING('ios'));
expect(stdout).toContain(BUILD.PREBUILD_PROMPT);
- // TODO(pmleczek): Refactor CLI error handling
- expect(stderr).toContain(`Error: Value of iOS Scheme`);
- expect(stderr).toContain(`could not be inferred from the project`);
+ expect(stderr).toContain(`Could not find brownfield iOS scheme`);
// The android directory should be created and not empty
await expectPrebuild(TEMP_DIR, 'ios');
});
-
- // TODO(pmleczek): Verify failure if prebuild is not done
});
/**
@@ -134,7 +169,7 @@ describe('build:ios command', () => {
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run'],
- stdout: [BUILD_IOS.CONFIGURATION(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME)],
+ stdout: BUILD_IOS.CONFIGURATION(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME),
});
});
@@ -158,7 +193,7 @@ describe('build:ios command', () => {
const expectedOutput = [
...BUILD_IOS.BUILD_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Debug'),
...BUILD_IOS.PACKAGE_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Debug'),
- BUILD.BUILD_TYPE_DEBUG,
+ BUILD_IOS.BUILD_TYPE_DEBUG,
];
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
@@ -181,7 +216,7 @@ describe('build:ios command', () => {
const expectedOutput = [
...BUILD_IOS.BUILD_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Release'),
...BUILD_IOS.PACKAGE_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Release'),
- BUILD.BUILD_TYPE_RELEASE,
+ BUILD_IOS.BUILD_TYPE_RELEASE,
];
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
@@ -202,7 +237,7 @@ describe('build:ios command', () => {
it('--release option should take precedence over --debug option', async () => {
const expectedOutput = [
...BUILD_IOS.BUILD_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Release'),
- BUILD.BUILD_TYPE_RELEASE,
+ BUILD_IOS.BUILD_TYPE_RELEASE,
];
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
@@ -219,12 +254,12 @@ describe('build:ios command', () => {
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run', '-x', 'someworkspace.xcworkspace'],
- stdout: [`- Xcode Workspace: someworkspace.xcworkspace`],
+ stdout: [`- Workspace: someworkspace.xcworkspace`],
});
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run', '--xcworkspace', 'someworkspace.xcworkspace'],
- stdout: [`- Xcode Workspace: someworkspace.xcworkspace`],
+ stdout: [`- Workspace: someworkspace.xcworkspace`],
});
});
@@ -236,12 +271,12 @@ describe('build:ios command', () => {
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run', '-s', 'somescheme'],
- stdout: [`- Xcode Scheme: somescheme`],
+ stdout: [`- Scheme: somescheme`],
});
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run', '--scheme', 'somescheme'],
- stdout: [`- Xcode Scheme: somescheme`],
+ stdout: [`- Scheme: somescheme`],
});
});
@@ -254,12 +289,12 @@ describe('build:ios command', () => {
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run', '-a', '../artifacts'],
- stdout: [`- Artifacts directory: ${expectedPath}`],
+ stdout: [`- Artifacts path: ${expectedPath}`],
});
await buildIosTest({
directory: TEMP_DIR_PREBUILD,
args: ['--dry-run', '--artifacts', '../artifacts'],
- stdout: [`- Artifacts directory: ${expectedPath}`],
+ stdout: [`- Artifacts path: ${expectedPath}`],
});
});
});
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/help.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/help.test.ts
index 757d256608f39b..cb4e6602cecb07 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/help.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/help.test.ts
@@ -1,4 +1,4 @@
-import { HELP_MESSAGE } from '../../utils/output';
+import { HELP_MESSAGE, VERSION } from '../../utils/output';
import { executeCLIASync } from '../../utils/process';
import { createTempProject, cleanUpProject } from '../../utils/project';
@@ -37,6 +37,16 @@ describe('--help option', () => {
expect(stdout).toMatchSnapshot();
});
+ /**
+ * Command: npx expo-brownfield help
+ * Expected behavior: The CLI should support the `help` command
+ */
+ it('should support the `help` command', async () => {
+ const { stdout, exitCode } = await executeCLIASync(TEMP_DIR, ['help']);
+ expect(exitCode).toBe(0);
+ expect(stdout).toMatchSnapshot();
+ });
+
/**
* Command: npx expo-brownfield tasks:android --help
* Expected behavior: The CLI should not print general help after a command is used
@@ -50,12 +60,12 @@ describe('--help option', () => {
/**
* Command: npx expo-brownfield --version --help
- * Expected behavior: The `--help` option should take precedence over `--version`
+ * Expected behavior: The `--help` option shouldn't take precedence over `--version`
*/
- it('should take precedence over `--version`', async () => {
+ it("shouldn't take precedence over `--version`", async () => {
const { stdout, exitCode } = await executeCLIASync(TEMP_DIR, ['--version', '--help']);
expect(exitCode).toBe(0);
- expect(stdout).toContain(HELP_MESSAGE.GENERAL_HEADER);
+ expect(stdout).toContain(VERSION);
});
/**
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/index.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/index.test.ts
index 4c0d745ebec7c7..b5706929bd4f6e 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/index.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/index.test.ts
@@ -1,5 +1,5 @@
-import { ERROR, HELP_MESSAGE } from '../../utils/output';
-import { executeCLIASync } from '../../utils/process';
+import { ERROR, HELP_MESSAGE, VERSION } from '../../utils/output';
+import { CLI_PATH, executeCLIASync, executeCommandAsync } from '../../utils/process';
import { createTempProject, cleanUpProject } from '../../utils/project';
const TASKS_ANDROID_ERROR = `Error: Value of Android library name: ENOENT: no such file or directory`;
@@ -24,12 +24,15 @@ describe('basic cli tests', () => {
* Expected behavior: The CLI should display an error message
*/
it('should correctly parse passed commands', async () => {
- const { stderr, exitCode } = await executeCLIASync(TEMP_DIR, ['tasks:android'], {
- ignoreErrors: true,
- });
+ const { exitCode, stderr } = await executeCommandAsync(
+ TEMP_DIR,
+ 'bash',
+ ['-c', `yes no | node ${CLI_PATH} build:android --repo MavenLocal`],
+ { ignoreErrors: true }
+ );
// Expect error because we haven't run prebuild
expect(exitCode).not.toBe(0);
- expect(stderr).toContain(TASKS_ANDROID_ERROR);
+ expect(stderr).toContain(ERROR.MISSING_PREBUILD());
});
/**
@@ -39,7 +42,7 @@ describe('basic cli tests', () => {
it('should correctly parse passed flags', async () => {
const { stdout, exitCode } = await executeCLIASync(TEMP_DIR, ['--version', '--help']);
expect(exitCode).toBe(0);
- expect(stdout).toContain(HELP_MESSAGE.GENERAL_HEADER);
+ expect(stdout).toContain(VERSION);
});
/**
@@ -47,9 +50,9 @@ describe('basic cli tests', () => {
* Expected behavior: The CLI should display general help message
*/
it('should display help message if no arguments are provided', async () => {
- const { stdout, exitCode } = await executeCLIASync(TEMP_DIR, [], { ignoreErrors: true });
- expect(exitCode).toBe(0);
- expect(stdout).toContain(HELP_MESSAGE.GENERAL_HEADER);
+ const { stderr, exitCode } = await executeCLIASync(TEMP_DIR, [], { ignoreErrors: true });
+ expect(exitCode).not.toBe(0);
+ expect(stderr).toContain(HELP_MESSAGE.GENERAL_HEADER);
});
/**
@@ -61,7 +64,7 @@ describe('basic cli tests', () => {
ignoreErrors: true,
});
expect(exitCode).not.toBe(0);
- expect(stderr).toContain(ERROR.UNKNOWN_COMMAND());
+ expect(stderr).toContain(ERROR.UNKNOWN_COMMAND('unknown:command'));
});
/**
@@ -76,5 +79,15 @@ describe('basic cli tests', () => {
expect(stderr).toContain(ERROR.UNKNOWN_OPTION('--unknown-flag'));
});
- // TODO(pmleczek): Test for passing more than one command
+ /**
+ * Command: npx expo-brownfield build:android build:ios
+ * Expected behavior: The CLI should display the unkown arg error message for the first command
+ */
+ it('should allow passing only one command', async () => {
+ const { stderr, exitCode } = await executeCLIASync(TEMP_DIR, ['build:android', 'build:ios'], {
+ ignoreErrors: true,
+ });
+ expect(exitCode).not.toBe(0);
+ expect(stderr).toContain(ERROR.ADDITIONAL_COMMAND('build:android'));
+ });
});
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/tasks-android.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/tasks-android.test.ts
index 32c179ffaa611b..04b2e2fe111fe8 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/tasks-android.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/tasks-android.test.ts
@@ -1,6 +1,7 @@
-import { ERROR, TASKS_ANDROID } from '../../utils/output';
+import { BUILD, ERROR, TASKS_ANDROID } from '../../utils/output';
+import { CLI_PATH, executeCommandAsync } from '../../utils/process';
import { createTempProject, cleanUpProject } from '../../utils/project';
-import { tasksAndroidTest } from '../../utils/test';
+import { buildTestCommon, expectPrebuild, tasksAndroidTest } from '../../utils/test';
let TEMP_DIR: string;
let TEMP_DIR_PREBUILD: string;
@@ -26,7 +27,7 @@ describe('tasks:android command', () => {
* Command: npx expo-brownfield tasks:android --help/-h
* Expected behavior: The CLI should display the full help message
*/
- it('should display help message for --help/-h option', async () => {
+ it('should display help message for --help/-h/help option', async () => {
// Help message display shouldn't require prebuild
await tasksAndroidTest({
directory: TEMP_DIR,
@@ -38,6 +39,12 @@ describe('tasks:android command', () => {
args: ['-h'],
useSnapshot: true,
});
+ await buildTestCommon({
+ directory: TEMP_DIR,
+ command: 'help',
+ args: ['tasks:android'],
+ useSnapshot: true,
+ });
});
/**
@@ -65,6 +72,61 @@ describe('tasks:android command', () => {
stderr: [ERROR.ADDITIONAL_COMMAND('tasks:android')],
});
});
+
+ /**
+ * Command: npx expo-brownfield tasks:android --library
+ * Expected behavior: The CLI should display the error message about missing argument
+ * (no need to test for all arguments as it's handled by commander)
+ */
+ it('should fail if argument value is not passed', async () => {
+ await tasksAndroidTest({
+ directory: TEMP_DIR,
+ args: ['--library'],
+ successExit: false,
+ stderr: [ERROR.MISSING_ARGUMENT('l', 'library', 'library')],
+ });
+ });
+
+ /**
+ * Command: npx expo-brownfield build:ios
+ * Expected behavior: The CLI should fail if prebuild is cancelled
+ */
+ it('should fail if prebuild is cancelled', async () => {
+ // The command fails, because `expo-brownfield` is not added to app.json
+ // But the prebuild should succeed
+ const { exitCode, stdout, stderr } = await executeCommandAsync(
+ TEMP_DIR,
+ 'bash',
+ ['-c', `yes no | node ${CLI_PATH} tasks:android`],
+ { ignoreErrors: true }
+ );
+ expect(exitCode).not.toBe(0);
+ expect(stdout).toContain(BUILD.PREBUILD_WARNING('android'));
+ expect(stdout).toContain(BUILD.PREBUILD_PROMPT);
+ expect(stderr).toContain(ERROR.MISSING_PREBUILD());
+ });
+
+ /**
+ * Command: npx expo-brownfield tasks:android
+ * Expected behavior: The CLI should validate and ask for prebuild
+ */
+ it('should validate and ask for prebuild', async () => {
+ // The command fails, because `expo-brownfield` is not added to app.json
+ // But the prebuild should succeed
+ const { exitCode, stdout, stderr } = await executeCommandAsync(
+ TEMP_DIR,
+ 'bash',
+ ['-c', `yes | node ${CLI_PATH} tasks:android`],
+ { ignoreErrors: true }
+ );
+ expect(exitCode).not.toBe(0);
+ expect(stdout).toContain(BUILD.PREBUILD_WARNING('android'));
+ expect(stdout).toContain(BUILD.PREBUILD_PROMPT);
+ expect(stderr).toContain(`Could not find brownfield library in the project`);
+
+ // The android directory should be created and not empty
+ await expectPrebuild(TEMP_DIR, 'android');
+ });
});
/**
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/version.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/version.test.ts
index d63ade83eb7b69..22d5e92669e0cf 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/version.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/version.test.ts
@@ -37,18 +37,6 @@ describe('--version option', () => {
expect(stdout).toBe(VERSION);
});
- /**
- * Command: npx expo-brownfield tasks:android --version
- * Expected behavior: The CLI should not support the `--version` option after a command is used
- */
- it("shouldn't be supported after a command is used", async () => {
- const { stderr, exitCode } = await executeCLIASync(TEMP_DIR, ['tasks:android', '--version'], {
- ignoreErrors: true,
- });
- expect(exitCode).not.toBe(0);
- expect(stderr).toContain('Error: unknown or unexpected option: --version');
- });
-
/**
* Command: npx expo-brownfield --version tasks:android
* Expected behavior: The `--version` option should be executed instead of the command
diff --git a/packages/expo-brownfield/e2e/utils/output.ts b/packages/expo-brownfield/e2e/utils/output.ts
index 24c8697c70c671..70b7f17756cc08 100644
--- a/packages/expo-brownfield/e2e/utils/output.ts
+++ b/packages/expo-brownfield/e2e/utils/output.ts
@@ -4,36 +4,29 @@ import path from 'node:path';
* Help messages
*/
export const HELP_MESSAGE = {
- GENERAL_HEADER: `Usage: expo-brownfield []`,
+ GENERAL_HEADER: `Usage: expo-brownfield [options] [command]`,
};
/**
* Common build outputs
*/
export const BUILD = {
- BUILD_TYPE_ALL: `- Build type: All`,
- BUILD_TYPE_DEBUG: `- Build type: Debug`,
- BUILD_TYPE_RELEASE: `- Build type: Release`,
PREBUILD_PROMPT: `Do you want to run the prebuild now?`,
PREBUILD_WARNING: (platform: 'android' | 'ios') =>
`Prebuild for platform: ${platform} is missing`,
- VERBOSE: `- Verbose: true`,
+ VERBOSE: ` - Verbose: true`,
};
/**
* Android build outputs
*/
export const BUILD_ANDROID = {
- CONFIGURATION: `Build configuration:
-- Verbose: false
-- Build type: All
-- Brownfield library: brownfield
-- Repositories: []
-- Tasks: someGradleTask`,
- LIBRARY: `- Brownfield library: brownfieldlib`,
- REPOSTORIES: `- Repositories: MavenLocal, CustomLocal`,
- TASK: `- Tasks: task1`,
- TASKS: `- Tasks: task1, task2, task3`,
+ BUILD_VARIANT_ALL: `- Build variant: All`,
+ BUILD_VARIANT_DEBUG: `- Build variant: Debug`,
+ BUILD_VARIANT_RELEASE: `- Build variant: Release`,
+ LIBRARY: `- Library: brownfieldlib`,
+ TASK: [`- Tasks:`, `- task1`],
+ TASKS: [`- Tasks:`, `- task1`, `- task2`, `- task3`],
};
/**
@@ -50,12 +43,17 @@ export const BUILD_IOS = {
`-destination generic/platform=iphonesimulator`,
`-configuration ${configuration}`,
],
- CONFIGURATION: (projectRoot: string, workspace: string) => `Build configuration:
-- Verbose: false
-- Artifacts directory: ${projectRoot}/artifacts
-- Build type: Release
-- Xcode Scheme: testapp${workspace}brownfield
-- Xcode Workspace: ${projectRoot}/ios/testapp${workspace}.xcworkspace`,
+ BUILD_TYPE_DEBUG: `- Build configuration: Debug`,
+ BUILD_TYPE_RELEASE: `- Build configuration: Release`,
+ CONFIGURATION: (projectRoot: string, workspace: string) => [
+ `Resolved build configuration`,
+ `- Build configuration: Release`,
+ `- Scheme: testapp${workspace}brownfield`,
+ `- Workspace: ${projectRoot}/ios/testapp${workspace}.xcworkspace`,
+ `- Dry run: true`,
+ `- Verbose: false`,
+ `- Artifacts path: ${projectRoot}/artifacts`,
+ ],
HERMES_COPYING: `Copying hermes XCFramework from Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework to`,
PACKAGE_COMMAND: (projectRoot: string, workspace: string, configuration: 'Debug' | 'Release') => [
`xcodebuild`,
@@ -71,11 +69,11 @@ export const BUILD_IOS = {
*/
export const TASKS_ANDROID = {
RESULT: [
- `Publish tasks:`,
+ `Publishing tasks`,
'- publishBrownfieldAllPublicationToMavenLocal',
'- publishBrownfieldDebugPublicationToMavenLocal',
'- publishBrownfieldReleasePublicationToMavenLocal',
- 'Repositories:',
+ 'Repositories',
'- MavenLocal',
],
VERBOSE: [`> Configure project`, `Publishing tasks\n----------------`, `BUILD SUCCESSFUL in`],
@@ -85,15 +83,14 @@ export const TASKS_ANDROID = {
* Error outputs
*/
export const ERROR = {
- ADDITIONAL_COMMAND: (
- command: string
- ) => `Error: Command ${command} doesn't support additional commands
-For all available options please use the help command:
-npx expo-brownfield ${command} --help`,
+ ADDITIONAL_COMMAND: (command: string) =>
+ `error: too many arguments for '${command}'. Expected 0 arguments but got 1.`,
+ MISSING_ARGUMENT: (short: string, full: string, argumentName: string) =>
+ `error: option '-${short}, --${full} <${argumentName}>' argument missing`,
+ MISSING_PREBUILD: () => `Brownfield cannot be built without prebuilding the native project`,
MISSING_TASKS_OR_REPOSITORIES: () => `Error: At least one task or repository must be specified`,
- UNKNOWN_COMMAND: () => `Error: unknown command
-Supported commands: build:android, build:ios, tasks:android`,
- UNKNOWN_OPTION: (option: string) => `Error: unknown or unexpected option: ${option}`,
+ UNKNOWN_COMMAND: (command: string) => `error: unknown command '${command}'`,
+ UNKNOWN_OPTION: (option: string) => `error: unknown option '${option}'`,
};
/**
diff --git a/packages/expo-brownfield/e2e/utils/process.ts b/packages/expo-brownfield/e2e/utils/process.ts
index 39bb94e976f90e..46ab83cfa483b5 100644
--- a/packages/expo-brownfield/e2e/utils/process.ts
+++ b/packages/expo-brownfield/e2e/utils/process.ts
@@ -1,8 +1,8 @@
import spawnAsync from '@expo/spawn-async';
-const CLI_PATH = require.resolve('../../bin/cli.js');
-const CREATE_EXPO_BIN = require.resolve('../../../create-expo/build/index.js');
-const EXPO_CLI_BIN = require.resolve('../../../@expo/cli/build/bin/cli');
+export const CLI_PATH = require.resolve('../../bin/cli.js');
+export const CREATE_EXPO_BIN = require.resolve('../../../create-expo/build/index.js');
+export const EXPO_CLI_BIN = require.resolve('../../../@expo/cli/build/bin/cli');
export interface ExecuteCLIOptions {
ignoreErrors?: boolean;
diff --git a/packages/expo-brownfield/package.json b/packages/expo-brownfield/package.json
index 20c2151a823088..eb2dbfae890ce8 100644
--- a/packages/expo-brownfield/package.json
+++ b/packages/expo-brownfield/package.json
@@ -49,8 +49,8 @@
"preset": "expo-module-scripts"
},
"dependencies": {
- "arg": "^5.0.2",
"chalk": "^4.1.2",
+ "commander": "^14.0.3",
"diff": "^5.2.0",
"expo-build-properties": "~55.0.7",
"expo-manifests": "~55.0.6",
diff --git a/packages/expo-dev-menu/CHANGELOG.md b/packages/expo-dev-menu/CHANGELOG.md
index b6a9b2d0719af2..49a38886c3b652 100644
--- a/packages/expo-dev-menu/CHANGELOG.md
+++ b/packages/expo-dev-menu/CHANGELOG.md
@@ -10,6 +10,8 @@
### 💡 Others
+- [iOS] Ensures DevMenuWindow is created in the main thread ([#43078](https://github.com/expo/expo/pull/43078) by [@gabrieldonadel](https://github.com/gabrieldonadel))
+
## 55.0.6 — 2026-02-16
### 🐛 Bug fixes
diff --git a/packages/expo-dev-menu/ios/DevMenuManager.swift b/packages/expo-dev-menu/ios/DevMenuManager.swift
index c9021616817f16..57180b206dd48b 100644
--- a/packages/expo-dev-menu/ios/DevMenuManager.swift
+++ b/packages/expo-dev-menu/ios/DevMenuManager.swift
@@ -249,7 +249,7 @@ open class DevMenuManager: NSObject {
override init() {
super.init()
- self.window = DevMenuWindow(manager: self)
+ self.window = Dispatch.mainSync { DevMenuWindow(manager: self) }
self.packagerConnectionHandler = DevMenuPackagerConnectionHandler(manager: self)
self.packagerConnectionHandler?.setup()
DevMenuPreferences.setup()
diff --git a/packages/expo-dev-menu/ios/Interceptors/DevMenuKeyCommandsInterceptor.swift b/packages/expo-dev-menu/ios/Interceptors/DevMenuKeyCommandsInterceptor.swift
index 3ec5170db0fffe..4f21233e3ded87 100644
--- a/packages/expo-dev-menu/ios/Interceptors/DevMenuKeyCommandsInterceptor.swift
+++ b/packages/expo-dev-menu/ios/Interceptors/DevMenuKeyCommandsInterceptor.swift
@@ -10,10 +10,12 @@ class DevMenuKeyCommandsInterceptor {
static var isInstalled: Bool = false {
willSet {
if isInstalled != newValue {
- if newValue {
- registerKeyCommands()
- } else {
- unregisterKeyCommands()
+ DispatchQueue.main.async {
+ if newValue {
+ registerKeyCommands()
+ } else {
+ unregisterKeyCommands()
+ }
}
}
}
diff --git a/packages/expo-file-system/CHANGELOG.md b/packages/expo-file-system/CHANGELOG.md
index dbbbfc7fcfe0d0..1410626ef68b9a 100644
--- a/packages/expo-file-system/CHANGELOG.md
+++ b/packages/expo-file-system/CHANGELOG.md
@@ -8,6 +8,8 @@
### 🐛 Bug fixes
+- Fix Jest mock to support new `File`/`Directory`/`Paths` API. ([#43005](https://github.com/expo/expo/pull/43005) by [@aleqsio](https://github.com/aleqsio))
+
### 💡 Others
## 55.0.6 — 2026-02-16
diff --git a/packages/expo-file-system/mocks/FileSystem.ts b/packages/expo-file-system/mocks/FileSystem.ts
index 331faf558187e3..5a263722144c27 100644
--- a/packages/expo-file-system/mocks/FileSystem.ts
+++ b/packages/expo-file-system/mocks/FileSystem.ts
@@ -10,6 +10,12 @@ export type TypedArray = any;
export type CreateOptions = any;
+export const documentDirectory = 'file:///mock/document/';
+export const cacheDirectory = 'file:///mock/cache/';
+export const bundleDirectory = 'file:///mock/bundle/';
+export const totalDiskSpace = 1000000000;
+export const availableDiskSpace = 500000000;
+
export function info(url: URL): any {}
export async function downloadFileAsync(
@@ -18,7 +24,16 @@ export async function downloadFileAsync(
options: DownloadOptions | undefined
): Promise {}
+export async function pickDirectoryAsync(initialUri?: string): Promise {}
+
+export async function pickFileAsync(initialUri?: string, mimeType?: string): Promise {}
+
export class FileSystemFile {
+ uri: string;
+ exists: boolean = false;
+ constructor(uri: string) {
+ this.uri = uri;
+ }
validatePath(): any {}
textSync(): any {}
base64Sync(): any {}
@@ -30,6 +45,7 @@ export class FileSystemFile {
create(options: CreateOptions | undefined): any {}
copy(to: FileSystemPath): any {}
move(to: FileSystemPath): any {}
+ rename(newName: string): any {}
async text(): Promise {}
async base64(): Promise {}
async bytes(): Promise {}
@@ -42,11 +58,19 @@ export class FileSystemFileHandle {
}
export class FileSystemDirectory {
+ uri: string;
+ exists: boolean = false;
+ constructor(uri: string) {
+ this.uri = uri;
+ }
info(): any {}
validatePath(): any {}
delete(): any {}
create(options: CreateOptions | undefined): any {}
copy(to: FileSystemPath): any {}
move(to: FileSystemPath): any {}
+ rename(newName: string): any {}
listAsRecords(): any {}
+ createFile(name: string, mimeType: string | null): any {}
+ createDirectory(name: string): any {}
}
diff --git a/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts b/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts
new file mode 100644
index 00000000000000..9d53f406db1937
--- /dev/null
+++ b/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts
@@ -0,0 +1,94 @@
+import { File, Directory, Paths } from 'expo-file-system';
+
+describe('expo-file-system new API', () => {
+ it('exports File, Directory, and Paths classes', () => {
+ expect(File).toBeDefined();
+ expect(Directory).toBeDefined();
+ expect(Paths).toBeDefined();
+ });
+
+ it('Paths.document returns a Directory with a mock URI', () => {
+ const doc = Paths.document;
+ expect(doc).toBeDefined();
+ expect(doc.uri).toBe('file:///mock/document/');
+ });
+
+ it('Paths.cache returns a Directory with a mock URI', () => {
+ const cache = Paths.cache;
+ expect(cache).toBeDefined();
+ expect(cache.uri).toBe('file:///mock/cache/');
+ });
+
+ it('Paths.bundle returns a Directory with a mock URI', () => {
+ const bundle = Paths.bundle;
+ expect(bundle).toBeDefined();
+ expect(bundle.uri).toBe('file:///mock/bundle/');
+ });
+
+ it('Paths.totalDiskSpace and availableDiskSpace return numbers', () => {
+ expect(typeof Paths.totalDiskSpace).toBe('number');
+ expect(typeof Paths.availableDiskSpace).toBe('number');
+ });
+
+ it('can construct a File from Paths.cache', () => {
+ const file = new File(Paths.cache, 'test.txt');
+ expect(file).toBeDefined();
+ expect(file.uri).toContain('test.txt');
+ expect(file.uri).toContain('mock/cache');
+ });
+
+ it('can construct a Directory from Paths.document', () => {
+ const dir = new Directory(Paths.document, 'subdir');
+ expect(dir).toBeDefined();
+ expect(dir.uri).toContain('subdir');
+ expect(dir.uri).toContain('mock/document');
+ });
+
+ it('File has inherited methods from native mock', () => {
+ const file = new File(Paths.cache, 'test.txt');
+ expect(typeof file.delete).toBe('function');
+ expect(typeof file.create).toBe('function');
+ expect(typeof file.copy).toBe('function');
+ expect(typeof file.move).toBe('function');
+ expect(typeof file.text).toBe('function');
+ expect(typeof file.write).toBe('function');
+ });
+
+ it('Directory has inherited methods from native mock', () => {
+ const dir = new Directory(Paths.document, 'subdir');
+ expect(typeof dir.delete).toBe('function');
+ expect(typeof dir.create).toBe('function');
+ expect(typeof dir.copy).toBe('function');
+ expect(typeof dir.move).toBe('function');
+ });
+
+ it('File.parentDirectory returns a Directory', () => {
+ const file = new File(Paths.cache, 'subdir', 'test.txt');
+ const parent = file.parentDirectory;
+ expect(parent).toBeInstanceOf(Directory);
+ expect(parent.uri).toContain('subdir');
+ });
+
+ it('File.name and File.extension work', () => {
+ const file = new File(Paths.cache, 'test.txt');
+ expect(file.name).toBe('test.txt');
+ expect(file.extension).toBe('.txt');
+ });
+});
+
+describe('expo-file-system/legacy mock', () => {
+ it('legacy API functions are mocked', () => {
+ const legacy = require('expo-file-system/legacy');
+ expect(legacy.downloadAsync).toBeDefined();
+ expect(legacy.getInfoAsync).toBeDefined();
+ expect(legacy.readAsStringAsync).toBeDefined();
+ expect(legacy.deleteAsync).toBeDefined();
+ expect(typeof legacy.downloadAsync).toBe('function');
+ });
+
+ it('legacy mock functions return promises', async () => {
+ const legacy = require('expo-file-system/legacy');
+ const result = await legacy.downloadAsync();
+ expect(result).toEqual({ md5: 'md5', uri: 'uri' });
+ });
+});
diff --git a/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapBackingFieldAccessor.java b/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapBackingFieldAccessor.java
new file mode 100644
index 00000000000000..1d3e8d518a5489
--- /dev/null
+++ b/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapBackingFieldAccessor.java
@@ -0,0 +1,12 @@
+package com.facebook.react.uimanager;
+
+import com.facebook.react.bridge.ReadableMap;
+
+/**
+ * Access the package private property declared inside of [ReactStylesDiffMap]
+ */
+public class ReactStylesDiffMapBackingFieldAccessor {
+ static ReadableMap getBackingMap(ReactStylesDiffMap diffMap) {
+ return diffMap.internal_backingMap();
+ }
+}
diff --git a/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapHelper.kt b/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapHelper.kt
index 823ad6ea4b0ec6..1b72559a0aa1f1 100644
--- a/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapHelper.kt
+++ b/packages/expo-modules-core/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapHelper.kt
@@ -1,25 +1,7 @@
package com.facebook.react.uimanager
import com.facebook.react.bridge.ReadableMap
-import expo.modules.core.interfaces.DoNotStrip
-import java.lang.reflect.Field
-@get:DoNotStrip
-private val backingMapField: Field by lazy {
- ReactStylesDiffMap::class.java.getDeclaredField("backingMap").apply {
- isAccessible = true
- }
-}
-
-/**
- * Access the package private property declared inside of [ReactStylesDiffMap]
- * TODO: We should stop using this field and find a better way to access the backing map:
- * See: https://github.com/facebook/react-native/pull/51386
- */
fun ReactStylesDiffMap.getBackingMap(): ReadableMap {
- return try {
- backingMapField.get(this) as ReadableMap
- } catch (e: ReflectiveOperationException) {
- throw RuntimeException("Unable to access internal_backingMap via reflection", e)
- }
+ return ReactStylesDiffMapBackingFieldAccessor.getBackingMap(this)
}
diff --git a/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/ProjectConfiguration.kt b/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/ProjectConfiguration.kt
index 7c2206904731d5..782ec3932bb22a 100644
--- a/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/ProjectConfiguration.kt
+++ b/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/ProjectConfiguration.kt
@@ -82,7 +82,10 @@ internal fun Project.applyPublishing(expoModulesExtension: ExpoModuleExtension)
publishingExtension()
.publications
- .createReleasePublication(publicationInfo)
+ .createReleasePublication(
+ publicationInfo,
+ expoModulesExtension.pomConfigurator
+ )
createExpoPublishToMavenLocalTask(publicationInfo, expoModulesExtension)
diff --git a/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/android/MavenPublicationExtension.kt b/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/android/MavenPublicationExtension.kt
index 951fd97d580186..127e7d6cb421d3 100644
--- a/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/android/MavenPublicationExtension.kt
+++ b/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/android/MavenPublicationExtension.kt
@@ -4,6 +4,7 @@ package expo.modules.plugin.android
import expo.modules.plugin.androidLibraryExtension
import expo.modules.plugin.gradle.ExpoModuleExtension
+import expo.modules.plugin.gradle.POMConfigurator
import expo.modules.plugin.publishingExtension
import groovy.lang.Binding
import groovy.lang.GroovyShell
@@ -58,7 +59,10 @@ internal data class PublicationInfo(
}
}
-internal fun PublicationContainer.createReleasePublication(publicationInfo: PublicationInfo) {
+internal fun PublicationContainer.createReleasePublication(
+ publicationInfo: PublicationInfo,
+ pomConfigurator: POMConfigurator?
+) {
create("release", MavenPublication::class.java) { mavenPublication ->
with(mavenPublication) {
from(publicationInfo.components)
@@ -66,24 +70,46 @@ internal fun PublicationContainer.createReleasePublication(publicationInfo: Publ
artifactId = publicationInfo.artifactId
version = publicationInfo.version
- mavenPublication.pom { pom ->
- pom.name.set(publicationInfo.artifactId)
- pom.url.set("https://github.com/expo/expo")
+ if (pomConfigurator != null) {
+ pomConfigurator.execute(mavenPublication.pom)
+ } else {
+ defaultPom(publicationInfo.artifactId)
+ }
+ }
+ }
+}
- pom.licenses { licenses ->
- licenses.license { license ->
- license.name.set("MIT License")
- license.url.set("https://github.com/expo/expo/blob/main/LICENSE")
- }
- }
+internal fun MavenPublication.defaultPom(artifactId: String) {
+ pom { pom ->
+ pom.name.set(artifactId)
+ pom.url.set("https://github.com/expo/expo")
- pom.scm { scm ->
- scm.connection.set("https://github.com/expo/expo.git")
- scm.developerConnection.set("https://github.com/expo/expo.git")
- scm.url.set("https://github.com/expo/expo")
- }
+ pom.licenses { licenses ->
+ licenses.license { license ->
+ license.name.set("MIT License")
+ license.url.set("https://github.com/expo/expo/blob/main/LICENSE")
+ license.distribution.set("https://github.com/expo/expo/blob/main/LICENSE")
+ }
+ }
+
+ pom.organization { organization ->
+ organization.name.set("650 Industries, Inc. (“Expo”)")
+ organization.url.set("https://expo.dev/home")
+ }
+
+ pom.developers { developerSpec ->
+ developerSpec.developer { developer ->
+ developer.name.set("Expo Maintainers")
+ developer.email.set("support@expo.dev")
+ developer.url.set("https://github.com/orgs/expo/people")
}
}
+
+ pom.scm { scm ->
+ scm.connection.set("https://github.com/expo/expo.git")
+ scm.developerConnection.set("https://github.com/expo/expo.git")
+ scm.url.set("https://github.com/expo/expo")
+ }
}
}
@@ -188,10 +214,7 @@ private fun Project.expoPublishBody(publicationInfo: PublicationInfo, expoModule
private fun Project.validateProjectConfiguration(expoModulesExtension: ExpoModuleExtension) {
val shouldUsePublicationScript = expoModulesExtension.autolinking.getShouldUsePublicationScriptPath(this)
- // If the path to the script is not defined, we assume that we can publish the module.
- if (shouldUsePublicationScript == null) {
- return
- }
+ ?: return // If the path to the script is not defined, we assume that we can publish the module.
val binding = Binding()
binding.setVariable("providers", project.providers)
@@ -205,12 +228,14 @@ private fun Project.validateProjectConfiguration(expoModulesExtension: ExpoModul
}
private fun modifyModuleConfig(projectName: String, currentConfig: JsonObject, publicationInfo: PublicationInfo, pathToRepository: String?): JsonObject {
- val publicationObject = JsonObject(mapOf(
- "groupId" to publicationInfo.groupId.toJsonElement(),
- "artifactId" to publicationInfo.artifactId.toJsonElement(),
- "version" to publicationInfo.version.toJsonElement(),
- "repository" to (pathToRepository ?: "mavenLocal").toJsonElement(),
- ))
+ val publicationObject = JsonObject(
+ mapOf(
+ "groupId" to publicationInfo.groupId.toJsonElement(),
+ "artifactId" to publicationInfo.artifactId.toJsonElement(),
+ "version" to publicationInfo.version.toJsonElement(),
+ "repository" to (pathToRepository ?: "mavenLocal").toJsonElement(),
+ )
+ )
val androidObject = currentConfig.getOrDefault("android", JsonObject(emptyMap())).jsonObject.mutate {
val subProject = get("projects")
diff --git a/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/gradle/ExpoModuleExtension.kt b/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/gradle/ExpoModuleExtension.kt
index 896cc50f6a0dcb..c6119df10280ac 100644
--- a/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/gradle/ExpoModuleExtension.kt
+++ b/packages/expo-modules-core/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/gradle/ExpoModuleExtension.kt
@@ -2,12 +2,16 @@ package expo.modules.plugin.gradle
import expo.modules.plugin.AutolinkingIntegration
import expo.modules.plugin.AutolinkingIntegrationImpl
-import org.gradle.api.Project
-import java.io.File
-import java.util.Properties
import expo.modules.plugin.Version
import expo.modules.plugin.safeGet
+import org.gradle.api.Action
+import org.gradle.api.Project
+import org.gradle.api.publish.maven.MavenPom
import org.gradle.internal.extensions.core.extra
+import java.io.File
+import java.util.Properties
+
+typealias POMConfigurator = Action
/**
* An user-facing interface to interact with the `ExpoGradleHelperExtension`.
@@ -39,4 +43,10 @@ open class ExpoModuleExtension(val project: Project) {
}
var canBePublished: Boolean = true
+
+ internal var pomConfigurator: POMConfigurator? = null
+
+ fun pom(configurator: POMConfigurator) {
+ pomConfigurator = configurator
+ }
}
diff --git a/packages/expo-notifications/CHANGELOG.md b/packages/expo-notifications/CHANGELOG.md
index 817d3c06f7b570..53ec1bd81053ec 100644
--- a/packages/expo-notifications/CHANGELOG.md
+++ b/packages/expo-notifications/CHANGELOG.md
@@ -10,6 +10,8 @@
### 🐛 Bug fixes
+- Fixed crash in `NotificationForwarderActivity` on Android 11/12 when Parcelable extras fail to deserialize by using byte array serialization as fallback. ([#43203](https://github.com/expo/expo/pull/43203) by [@vonovak](https://github.com/vonovak))
+
### 💡 Others
## 55.0.7 — 2026-02-16
diff --git a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationForwarderActivity.kt b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationForwarderActivity.kt
index cefcccfacc3dfb..6d8900b8e7c6f8 100644
--- a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationForwarderActivity.kt
+++ b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationForwarderActivity.kt
@@ -3,6 +3,7 @@ package expo.modules.notifications.service
import android.app.Activity
import android.content.Intent
import android.os.Bundle
+import android.util.Log
import expo.modules.notifications.BuildConfig
import expo.modules.notifications.service.delegates.ExpoHandlingDelegate
@@ -14,11 +15,19 @@ import expo.modules.notifications.service.delegates.ExpoHandlingDelegate
class NotificationForwarderActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val broadcastIntent =
- NotificationsService.createNotificationResponseBroadcastIntent(applicationContext, intent)
- val notificationResponse = NotificationsService.getNotificationResponseFromBroadcastIntent(intent)
- ExpoHandlingDelegate.openAppToForeground(this, notificationResponse)
- sendBroadcast(broadcastIntent)
+ try {
+ val broadcastIntent =
+ NotificationsService.createNotificationResponseBroadcastIntent(applicationContext, intent)
+ val notificationResponse = NotificationsService.getNotificationResponseFromBroadcastIntent(intent)
+ ExpoHandlingDelegate.openAppToForeground(this, notificationResponse)
+ sendBroadcast(broadcastIntent)
+ } catch (e: IllegalArgumentException) {
+ Log.e("expo-notifications", "Failed to handle notification response: could not recover notification data from intent extras. This may happen on some Android versions. Opening app to foreground.", e)
+ // Open the app anyway so the user isn't stuck
+ packageManager.getLaunchIntentForPackage(packageName)?.let {
+ startActivity(it)
+ }
+ }
finish()
}
diff --git a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt
index ed4c7b12c482d2..e9222a9f6c604b 100644
--- a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt
+++ b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt
@@ -79,6 +79,13 @@ open class NotificationsService : BroadcastReceiver() {
const val NOTIFICATION_REQUESTS_KEY = "notificationRequests"
const val NOTIFICATION_ACTION_KEY = "notificationAction"
+ // Byte array keys used as fallback when Parcelable extras fail to deserialize.
+ // On some Android versions (especially 11/12), custom Parcelable extras placed in a
+ // PendingIntent can come back as null when delivered through NotificationForwarderActivity.
+ // See https://github.com/expo/expo/issues/38908
+ internal const val NOTIFICATION_BYTES_KEY = "notificationBytes"
+ internal const val NOTIFICATION_ACTION_BYTES_KEY = "notificationActionBytes"
+
/**
* A helper function for dispatching a "fetch all displayed notifications" command to the service.
*
@@ -452,6 +459,12 @@ open class NotificationsService : BroadcastReceiver() {
intent.putExtra(EVENT_TYPE_KEY, RECEIVE_RESPONSE_TYPE)
intent.putExtra(NOTIFICATION_KEY, notification)
intent.putExtra(NOTIFICATION_ACTION_KEY, action as Parcelable)
+ // Also store as byte arrays for resilience against Parcelable deserialization failures.
+ // On some Android versions (especially 11/12), custom Parcelable extras in a PendingIntent
+ // come back as null when delivered through NotificationForwarderActivity.
+ // See https://github.com/expo/expo/issues/38908
+ marshalObject(notification)?.let { intent.putExtra(NOTIFICATION_BYTES_KEY, it) }
+ marshalObject(action)?.let { intent.putExtra(NOTIFICATION_ACTION_BYTES_KEY, it) }
}
// Starting from Android 12,
@@ -478,12 +491,13 @@ open class NotificationsService : BroadcastReceiver() {
*/
fun createNotificationResponseBroadcastIntent(context: Context, intent: Intent?): Intent {
val extras = intent?.extras
- val notification = extras?.getParcelable(NOTIFICATION_KEY)
- val action = extras?.getParcelable(NOTIFICATION_ACTION_KEY)
+ // Fallback to byte arrays when Parcelable extras are null (see https://github.com/expo/expo/issues/38908)
+ val notification = extras?.getParcelable(NOTIFICATION_KEY) ?: unmarshalObject(Notification.CREATOR, extras?.getByteArray(NOTIFICATION_BYTES_KEY))
+ val action = extras?.getParcelable(NOTIFICATION_ACTION_KEY) ?: unmarshalObject(NotificationAction.CREATOR, extras?.getByteArray(NOTIFICATION_ACTION_BYTES_KEY))
if (notification == null || action == null) {
throw IllegalArgumentException("notification ($notification) and action ($action) should not be null")
}
- val textResponse = RemoteInput.getResultsFromIntent(intent)?.getString(USER_TEXT_RESPONSE_KEY)
+ val textResponse = intent?.let { RemoteInput.getResultsFromIntent(it) }?.getString(USER_TEXT_RESPONSE_KEY)
val isTextInputResponse = textResponse != null && action is TextInputNotificationAction
val backgroundAction = if (isTextInputResponse) {
TextInputNotificationAction(action.identifier, action.title, false, action.placeholder)
@@ -518,8 +532,12 @@ open class NotificationsService : BroadcastReceiver() {
}
fun getNotificationResponseFromBroadcastIntent(intent: Intent): NotificationResponse {
- val notification = intent.getParcelableExtra(NOTIFICATION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_KEY not found in the intent extras.")
- val action = intent.getParcelableExtra(NOTIFICATION_ACTION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_ACTION_KEY not found in the intent extras.")
+ val notification = intent.getParcelableExtra(NOTIFICATION_KEY)
+ ?: unmarshalObject(Notification.CREATOR, intent.getByteArrayExtra(NOTIFICATION_BYTES_KEY))
+ ?: throw IllegalArgumentException("$NOTIFICATION_KEY not found in the intent extras.")
+ val action = intent.getParcelableExtra(NOTIFICATION_ACTION_KEY)
+ ?: unmarshalObject(NotificationAction.CREATOR, intent.getByteArrayExtra(NOTIFICATION_ACTION_BYTES_KEY))
+ ?: throw IllegalArgumentException("$NOTIFICATION_ACTION_KEY not found in the intent extras.")
val response = if (action is TextInputNotificationAction) {
val userText = RemoteInput.getResultsFromIntent(intent)?.getString(USER_TEXT_RESPONSE_KEY) ?: ""
TextInputNotificationResponse(action, notification, userText)
diff --git a/packages/expo-notifications/android/src/test/java/expo/modules/notifications/service/NotificationsServiceResponseIntentTest.kt b/packages/expo-notifications/android/src/test/java/expo/modules/notifications/service/NotificationsServiceResponseIntentTest.kt
new file mode 100644
index 00000000000000..3ac5ea823b3ab6
--- /dev/null
+++ b/packages/expo-notifications/android/src/test/java/expo/modules/notifications/service/NotificationsServiceResponseIntentTest.kt
@@ -0,0 +1,110 @@
+package expo.modules.notifications.service
+
+import android.content.Intent
+import android.os.Parcel
+import androidx.test.core.app.ApplicationProvider
+import expo.modules.notifications.notifications.model.Notification
+import expo.modules.notifications.notifications.model.NotificationAction
+import expo.modules.notifications.notifications.model.NotificationContent
+import expo.modules.notifications.notifications.model.NotificationRequest
+import org.junit.Assert.*
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class NotificationsServiceResponseIntentTest {
+
+ private val context get() = ApplicationProvider.getApplicationContext()
+
+ /**
+ * Builds a Notification that can survive Parcel marshal/unmarshal round-trips.
+ */
+ private fun buildNotification(identifier: String = "test-id"): Notification {
+ return Notification(
+ NotificationRequest(
+ identifier,
+ NotificationContent.Builder().setTitle("Title").setText("Body").build(),
+ null
+ )
+ )
+ }
+
+ private fun buildAction(identifier: String = "default"): NotificationAction {
+ return NotificationAction(identifier, "Open", true)
+ }
+
+ /**
+ * Simulates the Android 11/12 bug where custom Parcelable extras come back as null
+ * from a PendingIntent, but byte array extras survive. Verifies that
+ * createNotificationResponseBroadcastIntent falls back to byte arrays.
+ * See https://github.com/expo/expo/issues/38908
+ */
+ @Test
+ fun `createNotificationResponseBroadcastIntent falls back to byte arrays when Parcelable extras are null`() {
+ val notification = buildNotification(identifier = "byte-array-test")
+ val action = buildAction()
+
+ val intent = Intent().apply {
+ putExtra(NotificationsService.NOTIFICATION_BYTES_KEY, marshalParcelable(notification))
+ putExtra(NotificationsService.NOTIFICATION_ACTION_BYTES_KEY, marshalParcelable(action))
+ }
+
+ val broadcastIntent = NotificationsService.createNotificationResponseBroadcastIntent(context, intent)
+
+ val resultNotification = broadcastIntent.getParcelableExtra(NotificationsService.NOTIFICATION_KEY)
+ assertEquals("byte-array-test", resultNotification!!.notificationRequest.identifier)
+ }
+
+ /**
+ * Same as above but for getNotificationResponseFromBroadcastIntent.
+ */
+ @Test
+ fun `getNotificationResponseFromBroadcastIntent falls back to byte arrays when Parcelable extras are null`() {
+ val notification = buildNotification(identifier = "fallback-test")
+ val action = buildAction(identifier = "tap")
+
+ val intent = Intent().apply {
+ putExtra(NotificationsService.NOTIFICATION_BYTES_KEY, marshalParcelable(notification))
+ putExtra(NotificationsService.NOTIFICATION_ACTION_BYTES_KEY, marshalParcelable(action))
+ }
+
+ val response = NotificationsService.getNotificationResponseFromBroadcastIntent(intent)
+
+ assertEquals("fallback-test", response.notification.notificationRequest.identifier)
+ assertEquals("tap", response.actionIdentifier)
+ }
+
+ /**
+ * Verifies the full round-trip: intent with both Parcelable and byte array extras
+ * can be consumed by both createNotificationResponseBroadcastIntent and
+ * getNotificationResponseFromBroadcastIntent.
+ */
+ @Test
+ fun `full round-trip with both Parcelable and byte array extras`() {
+ val notification = buildNotification(identifier = "round-trip-test")
+ val action = buildAction(identifier = "open")
+
+ val intent = Intent().apply {
+ putExtra(NotificationsService.NOTIFICATION_KEY, notification)
+ putExtra(NotificationsService.NOTIFICATION_ACTION_KEY, action as android.os.Parcelable)
+ putExtra(NotificationsService.NOTIFICATION_BYTES_KEY, marshalParcelable(notification))
+ putExtra(NotificationsService.NOTIFICATION_ACTION_BYTES_KEY, marshalParcelable(action))
+ }
+
+ val broadcastIntent = NotificationsService.createNotificationResponseBroadcastIntent(context, intent)
+ val response = NotificationsService.getNotificationResponseFromBroadcastIntent(intent)
+
+ assertNotNull(broadcastIntent)
+ assertEquals("round-trip-test", response.notification.notificationRequest.identifier)
+ assertEquals("open", response.actionIdentifier)
+ }
+
+ private fun marshalParcelable(parcelable: android.os.Parcelable): ByteArray {
+ val parcel = Parcel.obtain()
+ parcelable.writeToParcel(parcel, 0)
+ val bytes = parcel.marshall()
+ parcel.recycle()
+ return bytes
+ }
+}
diff --git a/packages/expo-ui/CHANGELOG.md b/packages/expo-ui/CHANGELOG.md
index 024ba85ad34443..722025951c8262 100644
--- a/packages/expo-ui/CHANGELOG.md
+++ b/packages/expo-ui/CHANGELOG.md
@@ -18,6 +18,7 @@
- [iOS] - Support `Section` `footer` prop with `title` prop. ([#42966](https://github.com/expo/expo/pull/42966) by [@nishan](https://github.com/intergalacticspacehighway))
- [iOS] Added `contentTransition` modifier. ([#42980](https://github.com/expo/expo/pull/42980) by [@nishan](https://github.com/intergalacticspacehighway))
+- [iOS] Added `ScrollView` component. ([#43162](https://github.com/expo/expo/pull/43162) by [@nishan](https://github.com/intergalacticspacehighway))
- [iOS] Added `selection` and `onSelectionChange` to `presentationDetents` modifier for programmatic control of bottom sheet detents. ([#42910](https://github.com/expo/expo/pull/42910) by [@nishan](https://github.com/intergalacticspacehighway))
### 🐛 Bug fixes
diff --git a/packages/expo-ui/build/swift-ui/ScrollView/index.d.ts b/packages/expo-ui/build/swift-ui/ScrollView/index.d.ts
new file mode 100644
index 00000000000000..ea4bffe9ea832a
--- /dev/null
+++ b/packages/expo-ui/build/swift-ui/ScrollView/index.d.ts
@@ -0,0 +1,16 @@
+import { type CommonViewModifierProps } from '../types';
+export type ScrollViewProps = {
+ children: React.ReactNode;
+ /**
+ * The scrollable axes.
+ * @default 'vertical'
+ */
+ axes?: 'vertical' | 'horizontal' | 'both';
+ /**
+ * Whether to show scroll indicators.
+ * @default true
+ */
+ showsIndicators?: boolean;
+} & CommonViewModifierProps;
+export declare function ScrollView(props: ScrollViewProps): import("react").JSX.Element;
+//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/packages/expo-ui/build/swift-ui/ScrollView/index.d.ts.map b/packages/expo-ui/build/swift-ui/ScrollView/index.d.ts.map
new file mode 100644
index 00000000000000..c792136beeae88
--- /dev/null
+++ b/packages/expo-ui/build/swift-ui/ScrollView/index.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/swift-ui/ScrollView/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;OAGG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC;IAC1C;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,uBAAuB,CAAC;AAO5B,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,+BAShD"}
\ No newline at end of file
diff --git a/packages/expo-ui/build/swift-ui/index.d.ts b/packages/expo-ui/build/swift-ui/index.d.ts
index 4874e2b352281f..935a99db059cb3 100644
--- a/packages/expo-ui/build/swift-ui/index.d.ts
+++ b/packages/expo-ui/build/swift-ui/index.d.ts
@@ -32,6 +32,7 @@ export * from './TextField';
export * from './SecureField';
export * from './Namespace';
export * from './GlassEffectContainer';
+export * from './ScrollView';
export * from './Shapes';
export * from './Popover';
export * from './Grid';
diff --git a/packages/expo-ui/build/swift-ui/index.d.ts.map b/packages/expo-ui/build/swift-ui/index.d.ts.map
index c639b64be0fa3c..24dc8865abb646 100644
--- a/packages/expo-ui/build/swift-ui/index.d.ts.map
+++ b/packages/expo-ui/build/swift-ui/index.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/swift-ui/index.tsx"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,SAAS,CAAC"}
\ No newline at end of file
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/swift-ui/index.tsx"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC;AACzC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,SAAS,CAAC"}
\ No newline at end of file
diff --git a/packages/expo-ui/ios/ExpoUIModule.swift b/packages/expo-ui/ios/ExpoUIModule.swift
index 3dd6d851cd2dad..818791f1f55362 100644
--- a/packages/expo-ui/ios/ExpoUIModule.swift
+++ b/packages/expo-ui/ios/ExpoUIModule.swift
@@ -123,6 +123,7 @@ public final class ExpoUIModule: Module {
ExpoUIView(ZStackView.self)
ExpoUIView(GlassEffectContainerView.self)
ExpoUIView(LabeledContentView.self)
+ ExpoUIView(ScrollViewComponent.self)
ExpoUIView(RectangleView.self)
ExpoUIView(RoundedRectangleView.self)
ExpoUIView(EllipseView.self)
diff --git a/packages/expo-ui/ios/Modifiers/ViewModifierRegistry.swift b/packages/expo-ui/ios/Modifiers/ViewModifierRegistry.swift
index 90fe9aeedca39d..8136cac79904f7 100644
--- a/packages/expo-ui/ios/Modifiers/ViewModifierRegistry.swift
+++ b/packages/expo-ui/ios/Modifiers/ViewModifierRegistry.swift
@@ -1200,6 +1200,18 @@ internal struct ListSectionMargins: ViewModifier, Record {
internal enum AxisOptions: String, Enumerable {
case horizontal
case vertical
+ case both
+
+ func toAxis() -> Axis.Set {
+ switch self {
+ case .vertical:
+ return .vertical
+ case .horizontal:
+ return .horizontal
+ case .both:
+ return [.vertical, .horizontal]
+ }
+ }
}
internal struct GridCellUnsizedAxes: ViewModifier, Record {
@@ -1208,12 +1220,7 @@ internal struct GridCellUnsizedAxes: ViewModifier, Record {
func body(content: Content) -> some View {
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) {
if let axes {
- switch axes {
- case .horizontal:
- content.gridCellUnsizedAxes(.horizontal)
- case .vertical:
- content.gridCellUnsizedAxes(.vertical)
- }
+ content.gridCellUnsizedAxes(axes.toAxis())
} else {
content
}
diff --git a/packages/expo-ui/ios/ScrollViewComponent.swift b/packages/expo-ui/ios/ScrollViewComponent.swift
new file mode 100644
index 00000000000000..3d658f39a69e67
--- /dev/null
+++ b/packages/expo-ui/ios/ScrollViewComponent.swift
@@ -0,0 +1,23 @@
+// Copyright 2026-present 650 Industries. All rights reserved.
+
+import SwiftUI
+import ExpoModulesCore
+
+public final class ScrollViewComponentProps: UIBaseViewProps {
+ @Field var axes: AxisOptions = .vertical
+ @Field var showsIndicators: Bool = true
+}
+
+public struct ScrollViewComponent: ExpoSwiftUI.View {
+ @ObservedObject public var props: ScrollViewComponentProps
+
+ public init(props: ScrollViewComponentProps) {
+ self.props = props
+ }
+
+ public var body: some View {
+ ScrollView(props.axes.toAxis(), showsIndicators: props.showsIndicators) {
+ Children()
+ }
+ }
+}
diff --git a/packages/expo-ui/src/swift-ui/ScrollView/index.tsx b/packages/expo-ui/src/swift-ui/ScrollView/index.tsx
new file mode 100644
index 00000000000000..786409fa28223d
--- /dev/null
+++ b/packages/expo-ui/src/swift-ui/ScrollView/index.tsx
@@ -0,0 +1,34 @@
+import { requireNativeView } from 'expo';
+
+import { createViewModifierEventListener } from '../modifiers/utils';
+import { type CommonViewModifierProps } from '../types';
+
+export type ScrollViewProps = {
+ children: React.ReactNode;
+ /**
+ * The scrollable axes.
+ * @default 'vertical'
+ */
+ axes?: 'vertical' | 'horizontal' | 'both';
+ /**
+ * Whether to show scroll indicators.
+ * @default true
+ */
+ showsIndicators?: boolean;
+} & CommonViewModifierProps;
+
+const ScrollViewNativeView: React.ComponentType = requireNativeView(
+ 'ExpoUI',
+ 'ScrollViewComponent'
+);
+
+export function ScrollView(props: ScrollViewProps) {
+ const { modifiers, ...restProps } = props;
+ return (
+
+ );
+}
diff --git a/packages/expo-ui/src/swift-ui/index.tsx b/packages/expo-ui/src/swift-ui/index.tsx
index 1ac6ddb321a267..6116233615599b 100644
--- a/packages/expo-ui/src/swift-ui/index.tsx
+++ b/packages/expo-ui/src/swift-ui/index.tsx
@@ -32,6 +32,7 @@ export * from './TextField';
export * from './SecureField';
export * from './Namespace';
export * from './GlassEffectContainer';
+export * from './ScrollView';
export * from './Shapes';
export * from './Popover';
export * from './Grid';
diff --git a/packages/expo-video/CHANGELOG.md b/packages/expo-video/CHANGELOG.md
index f6335ee3a58ed5..760de9a677a2f6 100644
--- a/packages/expo-video/CHANGELOG.md
+++ b/packages/expo-video/CHANGELOG.md
@@ -6,10 +6,14 @@
### 🎉 New features
+- [Android][iOS] Add `url` field to HLS video tracks. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
+
### 🐛 Bug fixes
### 💡 Others
+- Update HLS video track fetching for iOS 26+. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
+
## 55.0.6 — 2026-02-16
### 🛠 Breaking changes
@@ -62,6 +66,7 @@ _This version does not introduce any user-facing changes._
- [Android][iOS] Add `seek tolerance` and `scrubbingModeOptions` properties to the player. ([#40203](https://github.com/expo/expo/pull/40203) by [@behenate](https://github.com/behenate))
- Allow assigning `null` value to the `player` prop of the `VideoView` ([#40860](https://github.com/expo/expo/pull/40860) by [@behenate](https://github.com/behenate))
- [Android][iOS] Add `averageBitrate` and `peakBitrate` for video tracks. ([#41532](https://github.com/expo/expo/pull/41532) by [@behenate](https://github.com/behenate))
+- [Android][iOS] Add `url` field to HLS video tracks. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
### 🐛 Bug fixes
@@ -81,6 +86,7 @@ _This version does not introduce any user-facing changes._
- Add extract the object `VideoSource` type into separate `VideoSourceObject` type. ([#41514](https://github.com/expo/expo/pull/41514) by [@behenate](https://github.com/behenate))
- [Android] Set property values on calling thread. ([#41533](https://github.com/expo/expo/pull/41533) by [@behenate](https://github.com/behenate))
- Mark the video track `bitrate` field as deprecated. ([#41532](https://github.com/expo/expo/pull/41532) by [@behenate](https://github.com/behenate))
+- Update HLS video track fetching for iOS 26+. ([#41681](https://github.com/expo/expo/pull/41681) by [@behenate](https://github.com/behenate))
## 3.0.15 - 2025-12-05
diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt b/packages/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt
index 1d313e71d927ad..ee4e976659525b 100644
--- a/packages/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt
+++ b/packages/expo-video/android/src/main/java/expo/modules/video/player/VideoPlayer.kt
@@ -20,6 +20,7 @@ import androidx.media3.exoplayer.DecoderReuseEvaluation
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
+import androidx.media3.exoplayer.hls.HlsManifest
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.session.MediaSession
import androidx.media3.ui.PlayerView
@@ -249,7 +250,8 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
val newAudioTracks = audioTracks.availableAudioTracks
val newCurrentSubtitleTrack = subtitles.currentSubtitleTrack
val newCurrentAudioTrack = audioTracks.currentAudioTrack
- availableVideoTracks = tracks.toVideoTracks()
+ val hlsManifest = player.currentManifest as? HlsManifest
+ availableVideoTracks = tracks.toVideoTracks(hlsManifest)
refreshPlaybackInfo()
if (isLoadingNewSource) {
@@ -570,17 +572,23 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
// Extension functions
@OptIn(UnstableApi::class)
-private fun Tracks.toVideoTracks(): List {
+private fun Tracks.toVideoTracks(sourceManifest: HlsManifest?): List {
val videoTracks = mutableListOf()
for (group in this.groups) {
for (i in 0 until group.length) {
val format = group.getTrackFormat(i)
val isSupported = group.isTrackSupported(i)
+ val hlsVariant = sourceManifest?.multivariantPlaylist?.variants?.firstOrNull {
+ it.format.id == format.id
+ }
+
+ // We provide the variant url only for HLS sources
+ val variantUrl = hlsVariant?.url
if (!MimeTypes.isVideo(format.sampleMimeType)) {
continue
}
- videoTracks.add(VideoTrack.fromFormat(format, isSupported))
+ videoTracks.add(VideoTrack.fromFormat(format, isSupported, variantUrl))
}
}
return videoTracks.filterNotNull()
diff --git a/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt b/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt
index fc91f0c3d3afe9..f39e7afc2fc022 100644
--- a/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt
+++ b/packages/expo-video/android/src/main/java/expo/modules/video/records/Tracks.kt
@@ -1,5 +1,6 @@
package expo.modules.video.records
+import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
@@ -53,6 +54,7 @@ class AudioTrack(
@OptIn(UnstableApi::class)
class VideoTrack(
@Field val id: String,
+ @Field val url: Uri?,
@Field val size: VideoSize,
@Field val mimeType: String?,
@Field val isSupported: Boolean = true,
@@ -63,7 +65,7 @@ class VideoTrack(
var format: Format? = null
) : Record, Serializable {
companion object {
- fun fromFormat(format: Format?, isSupported: Boolean): VideoTrack? {
+ fun fromFormat(format: Format?, isSupported: Boolean, variantUrl: Uri?): VideoTrack? {
val id = format?.id ?: return null
val size = VideoSize(format)
val mimeType = format.sampleMimeType
@@ -73,6 +75,7 @@ class VideoTrack(
return VideoTrack(
id = id,
+ url = variantUrl,
size = size,
mimeType = mimeType,
isSupported = isSupported,
diff --git a/packages/expo-video/build/VideoPlayer.types.d.ts b/packages/expo-video/build/VideoPlayer.types.d.ts
index aea2081424dc5f..48cfe68e3f71b8 100644
--- a/packages/expo-video/build/VideoPlayer.types.d.ts
+++ b/packages/expo-video/build/VideoPlayer.types.d.ts
@@ -519,6 +519,10 @@ export type VideoTrack = {
* > This field is platform-specific and may return different depending on the operating system.
*/
id: string;
+ /**
+ * The URL of the `VideoTrack` for HLS video sources. `null` for other source types.
+ */
+ url: string | null;
/**
* Size of the video track.
*/
diff --git a/packages/expo-video/build/VideoPlayer.types.d.ts.map b/packages/expo-video/build/VideoPlayer.types.d.ts.map
index b623493e4611e2..47357a586f1108 100644
--- a/packages/expo-video/build/VideoPlayer.types.d.ts.map
+++ b/packages/expo-video/build/VideoPlayer.types.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACtE;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;;;OAIG;IACH,sBAAsB,EAAE,OAAO,CAAC;IAEhC;;;;;;OAMG;IACH,eAAe,EAAE,eAAe,CAAC;IAEjC;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;IAEf;;;;;;;;OAQG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IAE7C;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9C;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;IAExB;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;;;;;OAQG;IACH,wBAAwB,EAAE,OAAO,CAAC;IAElC;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEzB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAEnC;;;;;;;;OAQG;IACH,0BAA0B,EAAE,OAAO,CAAC;IAEpC;;;;;;;;OAQG;IACH,uBAAuB,EAAE,OAAO,CAAC;IAEjC;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAElC;;;;;;OAMG;IACH,aAAa,EAAE,aAAa,CAAC;IAE7B;;;;;;;;OAQG;IACH,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IAEpC;;;;;;OAMG;IACH,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAE9B;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,EAAE,UAAU,EAAE,CAAC;IAE5C;;;;;OAKG;IACH,QAAQ,CAAC,uBAAuB,EAAE,aAAa,EAAE,CAAC;IAElD;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAEvC;;;;;;;OAOG;IACH,QAAQ,CAAC,oBAAoB,EAAE,UAAU,EAAE,CAAC;IAE5C;;;;OAIG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAE3C;;;;;;;;OAQG;IACH,aAAa,EAAE,aAAa,CAAC;IAE7B;;;;;OAKG;IACH,oBAAoB,EAAE,oBAAoB,CAAC;IAE3C;;;;;;;OAOG;gBAED,MAAM,EAAE,WAAW,EACnB,qBAAqB,CAAC,EAAE,OAAO,EAC/B,oBAAoB,CAAC,EAAE,oBAAoB;IAG7C;;OAEG;IACH,IAAI,IAAI,IAAI;IAEZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAEb;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI;IAE5D;;;;OAIG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAEhD;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE7B;;OAEG;IACH,MAAM,IAAI,IAAI;IAEd;;;;;OAKG;IACH,uBAAuB,CACrB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAE7E,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,iBAAiB,CAAC;AAErE,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;IAEjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IAEzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzE;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;IAEjD;;;;;;OAMG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAE3C;;;;;;OAMG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAEvC;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAExC;;;;;OAKG;IACH,QAAQ,CAAC,+BAA+B,CAAC,EAAE,OAAO,CAAC;CACpD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAEnF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,SAAS,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9B;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B;;OAEG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IAErC;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;;;OAMG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;CACxC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC"}
\ No newline at end of file
+{"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACtE;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;;;OAIG;IACH,sBAAsB,EAAE,OAAO,CAAC;IAEhC;;;;;;OAMG;IACH,eAAe,EAAE,eAAe,CAAC;IAEjC;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;IAEf;;;;;;;;OAQG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IAE7C;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9C;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;IAExB;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;;;;;OAQG;IACH,wBAAwB,EAAE,OAAO,CAAC;IAElC;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEzB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAEnC;;;;;;;;OAQG;IACH,0BAA0B,EAAE,OAAO,CAAC;IAEpC;;;;;;;;OAQG;IACH,uBAAuB,EAAE,OAAO,CAAC;IAEjC;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAElC;;;;;;OAMG;IACH,aAAa,EAAE,aAAa,CAAC;IAE7B;;;;;;;;OAQG;IACH,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IAEpC;;;;;;OAMG;IACH,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAE9B;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,EAAE,UAAU,EAAE,CAAC;IAE5C;;;;;OAKG;IACH,QAAQ,CAAC,uBAAuB,EAAE,aAAa,EAAE,CAAC;IAElD;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAEvC;;;;;;;OAOG;IACH,QAAQ,CAAC,oBAAoB,EAAE,UAAU,EAAE,CAAC;IAE5C;;;;OAIG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAE3C;;;;;;;;OAQG;IACH,aAAa,EAAE,aAAa,CAAC;IAE7B;;;;;OAKG;IACH,oBAAoB,EAAE,oBAAoB,CAAC;IAE3C;;;;;;;OAOG;gBAED,MAAM,EAAE,WAAW,EACnB,qBAAqB,CAAC,EAAE,OAAO,EAC/B,oBAAoB,CAAC,EAAE,oBAAoB;IAG7C;;OAEG;IACH,IAAI,IAAI,IAAI;IAEZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAEb;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI;IAE5D;;;;OAIG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAEhD;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE7B;;OAEG;IACH,MAAM,IAAI,IAAI;IAEd;;;;;OAKG;IACH,uBAAuB,CACrB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAE7E,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,iBAAiB,CAAC;AAErE,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;IAEjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IAEzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzE;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;IAEjD;;;;;;OAMG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAE3C;;;;;;OAMG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAEvC;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAExC;;;;;OAKG;IACH,QAAQ,CAAC,+BAA+B,CAAC,EAAE,OAAO,CAAC;CACpD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAEnF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAEnB;;OAEG;IACH,IAAI,EAAE,SAAS,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAEvB;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9B;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B;;OAEG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IAErC;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;;;OAMG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;CACxC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC"}
\ No newline at end of file
diff --git a/packages/expo-video/build/VideoPlayer.types.js.map b/packages/expo-video/build/VideoPlayer.types.js.map
index 8e8158386badd0..54a1c17d84ecef 100644
--- a/packages/expo-video/build/VideoPlayer.types.js.map
+++ b/packages/expo-video/build/VideoPlayer.types.js.map
@@ -1 +1 @@
-{"version":3,"file":"VideoPlayer.types.js","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"","sourcesContent":["import { SharedObject } from 'expo';\n\nimport { VideoPlayerEvents } from './VideoPlayerEvents.types';\nimport { VideoThumbnail } from './VideoThumbnail';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject {\n /**\n * Boolean value whether the player is currently playing.\n * > Use `play` and `pause` methods to control the playback.\n */\n readonly playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Determines whether the player should allow external playback.\n * @default true\n * @platform ios\n */\n allowsExternalPlayback: boolean;\n\n /**\n * Determines how the player will interact with other audio playing in the system.\n *\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n audioMixingMode: AudioMixingMode;\n\n /**\n * Boolean value whether the player is currently muted.\n * Setting this property to `true`/`false` will mute/unmute the player.\n * @default false\n */\n muted: boolean;\n\n /**\n * Float value indicating the current playback time in seconds.\n *\n * If the player is not yet playing, this value indicates the time position\n * at which playback will begin once the `play()` method is called.\n *\n * Setting `currentTime` to a new value seeks the player to the given time.\n * Check out the [`seekTolerance`](#seektolerance) property to configure the seeking precision.\n */\n currentTime: number;\n\n /**\n * The exact timestamp when the currently displayed video frame was sent from the server,\n * based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.\n * If this metadata is missing, this property will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentLiveTimestamp: number | null;\n\n /**\n * Float value indicating the latency of the live stream in seconds.\n * If a livestream doesn't have the required metadata, this will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentOffsetFromLive: number | null;\n\n /**\n * Float value indicating the time offset from the live in seconds.\n * @platform ios\n */\n targetOffsetFromLive: number;\n\n /**\n * Float value indicating the duration of the current video in seconds.\n */\n readonly duration: number;\n\n /**\n * Float value between `0` and `1.0` representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * @default true\n */\n preservesPitch: boolean;\n\n /**\n * Float value indicating the interval in seconds at which the player will emit the [`timeUpdate`](#videoplayerevents) event.\n * When the value is equal to `0`, the event will not be emitted.\n *\n * @default 0\n */\n timeUpdateEventInterval: number;\n\n /**\n * Float value between `0` and `16.0` indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Boolean indicating if the player should keep the screen on while playing.\n *\n * > On Android, this property has an effect only when a [`VideoView`](#videoview) is visible. If you want to keep the screen awake at all times use [`expo-keep-awake`](./keep-awake/).\n *\n * @default true\n * @platform android\n * @platform ios\n */\n keepScreenOnWhilePlaying: boolean;\n\n /**\n * Boolean value indicating whether the player is currently playing a live stream.\n */\n readonly isLive: boolean;\n\n /**\n * Indicates the current status of the player.\n */\n readonly status: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n *\n * > **Note**: On Android, `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the now playing notification to work.\n * @default false\n * @platform android\n * @platform ios\n */\n showNowPlayingNotification: boolean;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n *\n * > **Note**: The `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the background playback to work.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Float value indicating how far the player has buffered the video in seconds.\n *\n * This value is 0 when the player has not buffered up to the current playback time.\n * When it's impossible to determine the buffer state (for example, when the player isn't playing any media), this value is -1.\n */\n readonly bufferedPosition: number;\n\n /**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * > You should provide a `BufferOptions` object when setting this property. Setting individual buffer properties is not supported.\n * @platform android\n * @platform ios\n */\n bufferOptions: BufferOptions;\n\n /**\n * Specifies the subtitle track which is currently displayed by the player. `null` when no subtitles are displayed.\n *\n * > To ensure a valid subtitle track, always assign one of the subtitle tracks from the [`availableSubtitleTracks`](#availablesubtitletracks) array.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n subtitleTrack: SubtitleTrack | null;\n\n /**\n * Specifies the audio track currently played by the player. `null` when no audio is played.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n audioTrack: AudioTrack | null;\n\n /**\n * An array of audio tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableAudioTracks: AudioTrack[];\n\n /**\n * An array of subtitle tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableSubtitleTracks: SubtitleTrack[];\n\n /**\n * Specifies the video track currently played by the player. `null` when no video is displayed.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n readonly videoTrack: VideoTrack | null;\n\n /**\n * An array of video tracks available for the current video.\n *\n * > On iOS, when using a HLS source, make sure that the uri contains `.m3u8` extension or that the [`contentType`](#contenttype) property of the [`VideoSource`](#videosource) has been set to `'hls'`. Otherwise, the video tracks will not be available.\n *\n * @platform android\n * @platform ios\n */\n readonly availableVideoTracks: VideoTrack[];\n\n /**\n * Indicates whether the player is currently playing back the media to an external device via AirPlay.\n *\n * @platform ios\n */\n readonly isExternalPlaybackActive: boolean;\n\n /**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n *\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * By default, the player seeks to the exact requested time.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n */\n seekTolerance: SeekTolerance;\n\n /**\n * Determines whether the scrubbing mode is enabled and what scrubbing optimizations should be enabled.\n *\n * > See [`SeekTolerance`](#seektolerance) to set the seeking tolerance, which can also affect the scrubbing performance.\n *\n */\n scrubbingModeOptions: ScrubbingModeOptions;\n\n /**\n * Initializes a new video player instance with the given source.\n *\n * @param source The source of the video to be played.\n * @param useSynchronousReplace Optional parameter, when `true` `source` from the first parameter will be loaded on the main thread.\n * @param playerBuilderOptions Options to apply to the player builder before the native constructor is invoked.\n * @hidden\n */\n constructor(\n source: VideoSource,\n useSynchronousReplace?: boolean,\n playerBuilderOptions?: PlayerBuilderOptions\n );\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n *\n * > On iOS, this method loads the asset data synchronously on the UI thread and can block it for extended periods of time.\n * > Use `replaceAsync` to load the asset asynchronously and avoid UI lags.\n *\n * > This method will be deprecated in the future.\n */\n replace(source: VideoSource, disableWarning?: boolean): void;\n\n /**\n * Replaces the current source with a new one, while offloading loading of the asset to a different thread.\n *\n * > On Android and Web, this method is equivalent to `replace`.\n */\n replaceAsync(source: VideoSource): Promise;\n\n /**\n * Seeks the playback by the given number of seconds. The time to which the player seeks may differ from the specified requested time for efficiency,\n * depending on the encoding and what is currently buffered by the player. Use this function to implement playback controls that seek by specific amount of time,\n * in which case, the actual time usually does not have to be precise. For frame accurate seeking, use the [`currentTime`](#currenttime) property.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n\n /**\n * Generates thumbnails from the currently played asset. The thumbnails are references to native images,\n * thus they can be used as a source of the `Image` component from `expo-image`.\n * @platform android\n * @platform ios\n */\n generateThumbnailsAsync(\n times: number | number[],\n options?: VideoThumbnailOptions\n ): Promise;\n}\n\n/**\n * Additional options for video thumbnails generation.\n */\nexport type VideoThumbnailOptions = {\n /**\n * If provided, the generated thumbnail will not exceed this width in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxWidth?: number;\n\n /**\n * If provided, the generated thumbnail will not exceed this height in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxHeight?: number;\n};\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport type VideoSource = string | number | null | VideoSourceObject;\n\nexport type VideoSourceObject = {\n /**\n * The URI of the video.\n *\n * On iOS, `PHAsset` URIs are supported, but can only be loaded using the [`replaceAsync`](#replaceasyncsource) method or the default [`VideoPlayer`](#videoplayer) constructor.\n *\n * This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.\n */\n uri?: string;\n\n /**\n * The asset ID of a local video asset, acquired with the `require` function.\n * This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.\n */\n assetId?: number;\n\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\n\n /**\n * Specifies information which will be displayed in the now playing notification.\n * When undefined the player will display information contained in the video metadata.\n * @platform android\n * @platform ios\n */\n metadata?: VideoMetadata;\n\n /**\n * Specifies headers sent with the video request.\n * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).\n * @platform android\n * @platform ios\n */\n headers?: Record;\n\n /**\n * Specifies whether the player should use caching for the video.\n * > Due to platform limitations, the cache cannot be used with HLS video sources on iOS. Caching DRM-protected videos is not supported on Android and iOS.\n * @default false\n * @platform android\n * @platform ios\n */\n useCaching?: boolean;\n\n /**\n * Specifies the content type of the video source. When set to `'auto'`, the player will try to automatically determine the content type.\n *\n * You should use this property when playing HLS, SmoothStreaming or DASH videos from an uri, which does not contain a standardized extension for the corresponding media type.\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n contentType?: ContentType;\n};\n\n/**\n * Contains information about any errors that the player encountered during the playback\n */\nexport type PlayerError = {\n message: string;\n};\n\n/**\n * Contains information that will be displayed in the now playing notification when the video is playing.\n * @platform android\n * @platform ios\n */\nexport type VideoMetadata = {\n /**\n * The title of the video.\n * @platform android\n * @platform ios\n */\n title?: string;\n /**\n * Secondary text that will be displayed under the title.\n * @platform android\n * @platform ios\n */\n artist?: string;\n /**\n * The uri of the video artwork.\n * @platform android\n * @platform ios\n */\n artwork?: string;\n};\n\n/**\n * Specifies which type of DRM to use:\n * - Android supports ClearKey, PlayReady and Widevine.\n * - iOS supports FairPlay.\n */\nexport type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\nexport type DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: Record;\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n\n /**\n * Specifies the base64 encoded certificate data for the FairPlay DRM.\n * When this property is set, the `certificateUrl` property is ignored.\n * @platform ios\n */\n base64CertificateData?: string;\n};\n\n/**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * @platform android\n * @platform ios\n */\nexport type BufferOptions = {\n /**\n * The duration in seconds which determines how much media the player should buffer ahead of the current playback time.\n *\n * On iOS when set to `0` the player will automatically decide appropriate buffer duration.\n *\n * Equivalent to [`AVPlayerItem.preferredForwardBufferDuration`](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration).\n * @default Android: 20, iOS: 0\n * @platform android\n * @platform ios\n */\n readonly preferredForwardBufferDuration?: number;\n\n /**\n * A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.\n *\n * Equivalent to [`AVPlayer.automaticallyWaitsToMinimizeStalling`](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal).\n * @default true\n * @platform ios\n */\n readonly waitsToMinimizeStalling?: boolean;\n\n /**\n * Minimum duration of the buffer in seconds required to continue playing after the player has been paused or started buffering.\n *\n * > This property will be ignored if `preferredForwardBufferDuration` is lower.\n * @default 2\n * @platform android\n */\n readonly minBufferForPlayback?: number;\n\n /**\n * The maximum number of bytes that the player can buffer from the network.\n * When 0 the player will automatically decide appropriate buffer size.\n *\n * @default 0\n * @platform android\n */\n readonly maxBufferBytes?: number | null;\n\n /**\n * A Boolean value which determines whether the player should prioritize time over size when buffering media.\n *\n * @default false\n * @platform android\n */\n readonly prioritizeTimeOverSizeThreshold?: boolean;\n};\n\n/**\n * Specifies the content type of the source.\n *\n * - `auto`: The player will automatically determine the content type of the video.\n * - `progressive`: The player will use progressive download content type. This is the default `ContentType` when the uri does not contain an extension.\n * - `hls`: The player will use HLS content type.\n * - `dash`: The player will use DASH content type (Android-only).\n * - `smoothStreaming`: The player will use SmoothStreaming content type (Android-only).\n *\n * @default `auto`\n */\nexport type ContentType = 'auto' | 'progressive' | 'hls' | 'dash' | 'smoothStreaming';\n\n/**\n * Specifies the audio mode that the player should use. Audio mode is set on per-app basis, if there are multiple players playing and\n * have different a `AudioMode` specified, the highest priority mode will be used. Priority order: 'doNotMix' > 'auto' > 'duckOthers' > 'mixWithOthers'.\n *\n * - `mixWithOthers`: The player will mix its audio output with other apps.\n * - `duckOthers`: The player will lower the volume of other apps if any of the active players is outputting audio.\n * - `auto`: The player will allow other apps to keep playing audio only when it is muted. On iOS it will always interrupt other apps when `showNowPlayingNotification` is `true` due to system requirements.\n * - `doNotMix`: The player will pause playback in other apps, even when it's muted.\n *\n * > On iOS, the Now Playing notification is dependent on the audio mode. If the audio mode is different from `doNotMix` or `auto` this feature will not work.\n */\nexport type AudioMixingMode = 'mixWithOthers' | 'duckOthers' | 'auto' | 'doNotMix';\n\nexport type SubtitleTrack = {\n /**\n * A string used by `expo-video` to identify the subtitle track.\n *\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the subtitle track. For example, `en`, `pl`, `de`.\n */\n language: string;\n\n /**\n * Label of the subtitle track in the language of the device.\n */\n label: string;\n};\n\n/**\n * Specifies a VideoTrack loaded from a [`VideoSource`](#videosource).\n */\nexport type VideoTrack = {\n /**\n * The id of the video track.\n *\n * > This field is platform-specific and may return different depending on the operating system.\n */\n id: string;\n\n /**\n * Size of the video track.\n */\n size: VideoSize;\n\n /**\n * MimeType of the video track or null if unknown.\n */\n mimeType: string | null;\n\n /**\n * Indicates whether the video track format is supported by the device.\n *\n * @platform android\n */\n isSupported: boolean;\n\n /**\n * Specifies the bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate if known, or else null.\n *\n * @deprecated Use `peakBitrate` or `averageBitrate` instead.\n */\n bitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n *\n */\n averageBitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n */\n peakBitrate: number | null;\n\n /**\n * Specifies the frame rate of the video track in frames per second.\n */\n frameRate: number | null;\n};\n\n/**\n * Specifies the size of a video track.\n */\nexport type VideoSize = {\n /**\n * Width of the video track in pixels.\n */\n width: number;\n /**\n * Height of the video track in pixels.\n */\n height: number;\n};\n\nexport type AudioTrack = {\n /**\n * A string used by expo-video to identify the audio track.\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the audio track. For example, 'en', 'pl', 'de'.\n */\n language: string;\n\n /**\n * Label of the audio track in the language of the device.\n */\n label: string;\n};\n\n/**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n * Larger tolerance will usually result in faster seeking.\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n *\n * @platform android\n * @platform ios\n */\nexport type SeekTolerance = {\n /**\n * The maximum time that the actual position seeked to may precede the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceBefore?: number;\n\n /**\n * The maximum time that the actual position seeked to may exceed the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceAfter?: number;\n};\n\n/**\n * Defines scrubbing mode options used by a [`VideoPlayer`](#videoplayer).\n */\nexport type ScrubbingModeOptions = {\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * You should only enable this when the player is receiving a large number of seeks in a short period of time. For less frequent seeks, fine-tuning the [`SeekTolerance`](#seektolerance-1) may be sufficient.\n *\n * On Android, the player may consume more resources in this mode, so it should only be used for short periods of time in response to user interaction (for example, dragging on a progress bar UI element).\n *\n * On Android, when `scrubbingModeEnabled` is `true`, the playback is suppressed. You should set this property back to `false` when the user interaction ends to allow the playback to resume.\n * For best results, on iOS you should pause the playback when scrubbing.\n *\n * > For best scrubbing performance, consider also increasing the seeking tolerance using the [`SeekTolerance`](#seektolerance-1) property.\n *\n * > Other scrubbing mode options will have no effect when this is `false`.\n * @default false\n * @platform android\n * @platform ios\n */\n scrubbingModeEnabled?: boolean;\n\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * @platform android\n * @default true\n */\n increaseCodecOperatingRate?: boolean;\n\n /**\n * Sets whether ExoPlayer's dynamic scheduling should be enabled in scrubbing mode.\n * This can result in available output buffers being handled more quickly when seeking.\n *\n * @platform android\n * @default true\n */\n enableDynamicScheduling?: boolean;\n /**\n * Sets whether to use `MediaCodec.BUFFER_FLAG_DECODE_ONLY` in scrubbing mode.\n * When playback is using MediaCodec on API 34+, this flag can speed up seeking by signalling that the decoded output of buffers between the previous keyframe and the target frame is not needed by the player.\n *\n * @platform android\n * @default true\n */\n useDecodeOnlyFlag?: boolean;\n\n /**\n * Sets whether to avoid flushing the decoder (where possible) in scrubbing mode.\n * When `true`, avoids flushing the decoder when a new seek starts decoding from a key-frame in compatible content.\n *\n * @platform android\n * @default true\n */\n allowSkippingMediaCodecFlush?: boolean;\n};\n\n/**\n * Options to apply to the player builder before the native constructor is invoked\n * @platform android\n */\nexport type PlayerBuilderOptions = {\n /**\n * Seek backward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekBackwardIncrement?: number;\n\n /**\n * Seek forward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekForwardIncrement?: number;\n};\n"]}
\ No newline at end of file
+{"version":3,"file":"VideoPlayer.types.js","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"","sourcesContent":["import { SharedObject } from 'expo';\n\nimport { VideoPlayerEvents } from './VideoPlayerEvents.types';\nimport { VideoThumbnail } from './VideoThumbnail';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject {\n /**\n * Boolean value whether the player is currently playing.\n * > Use `play` and `pause` methods to control the playback.\n */\n readonly playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Determines whether the player should allow external playback.\n * @default true\n * @platform ios\n */\n allowsExternalPlayback: boolean;\n\n /**\n * Determines how the player will interact with other audio playing in the system.\n *\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n audioMixingMode: AudioMixingMode;\n\n /**\n * Boolean value whether the player is currently muted.\n * Setting this property to `true`/`false` will mute/unmute the player.\n * @default false\n */\n muted: boolean;\n\n /**\n * Float value indicating the current playback time in seconds.\n *\n * If the player is not yet playing, this value indicates the time position\n * at which playback will begin once the `play()` method is called.\n *\n * Setting `currentTime` to a new value seeks the player to the given time.\n * Check out the [`seekTolerance`](#seektolerance) property to configure the seeking precision.\n */\n currentTime: number;\n\n /**\n * The exact timestamp when the currently displayed video frame was sent from the server,\n * based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.\n * If this metadata is missing, this property will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentLiveTimestamp: number | null;\n\n /**\n * Float value indicating the latency of the live stream in seconds.\n * If a livestream doesn't have the required metadata, this will return `null`.\n * @platform android\n * @platform ios\n */\n readonly currentOffsetFromLive: number | null;\n\n /**\n * Float value indicating the time offset from the live in seconds.\n * @platform ios\n */\n targetOffsetFromLive: number;\n\n /**\n * Float value indicating the duration of the current video in seconds.\n */\n readonly duration: number;\n\n /**\n * Float value between `0` and `1.0` representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * @default true\n */\n preservesPitch: boolean;\n\n /**\n * Float value indicating the interval in seconds at which the player will emit the [`timeUpdate`](#videoplayerevents) event.\n * When the value is equal to `0`, the event will not be emitted.\n *\n * @default 0\n */\n timeUpdateEventInterval: number;\n\n /**\n * Float value between `0` and `16.0` indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Boolean indicating if the player should keep the screen on while playing.\n *\n * > On Android, this property has an effect only when a [`VideoView`](#videoview) is visible. If you want to keep the screen awake at all times use [`expo-keep-awake`](./keep-awake/).\n *\n * @default true\n * @platform android\n * @platform ios\n */\n keepScreenOnWhilePlaying: boolean;\n\n /**\n * Boolean value indicating whether the player is currently playing a live stream.\n */\n readonly isLive: boolean;\n\n /**\n * Indicates the current status of the player.\n */\n readonly status: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n *\n * > **Note**: On Android, `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the now playing notification to work.\n * @default false\n * @platform android\n * @platform ios\n */\n showNowPlayingNotification: boolean;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n *\n * > **Note**: The `supportsBackgroundPlayback` property of the [config plugin](#configuration-in-app-config)\n * > has to be `true` for the background playback to work.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Float value indicating how far the player has buffered the video in seconds.\n *\n * This value is 0 when the player has not buffered up to the current playback time.\n * When it's impossible to determine the buffer state (for example, when the player isn't playing any media), this value is -1.\n */\n readonly bufferedPosition: number;\n\n /**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * > You should provide a `BufferOptions` object when setting this property. Setting individual buffer properties is not supported.\n * @platform android\n * @platform ios\n */\n bufferOptions: BufferOptions;\n\n /**\n * Specifies the subtitle track which is currently displayed by the player. `null` when no subtitles are displayed.\n *\n * > To ensure a valid subtitle track, always assign one of the subtitle tracks from the [`availableSubtitleTracks`](#availablesubtitletracks) array.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n subtitleTrack: SubtitleTrack | null;\n\n /**\n * Specifies the audio track currently played by the player. `null` when no audio is played.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n audioTrack: AudioTrack | null;\n\n /**\n * An array of audio tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableAudioTracks: AudioTrack[];\n\n /**\n * An array of subtitle tracks available for the current video.\n *\n * @platform android\n * @platform ios\n */\n readonly availableSubtitleTracks: SubtitleTrack[];\n\n /**\n * Specifies the video track currently played by the player. `null` when no video is displayed.\n *\n * @default null\n * @platform android\n * @platform ios\n */\n readonly videoTrack: VideoTrack | null;\n\n /**\n * An array of video tracks available for the current video.\n *\n * > On iOS, when using a HLS source, make sure that the uri contains `.m3u8` extension or that the [`contentType`](#contenttype) property of the [`VideoSource`](#videosource) has been set to `'hls'`. Otherwise, the video tracks will not be available.\n *\n * @platform android\n * @platform ios\n */\n readonly availableVideoTracks: VideoTrack[];\n\n /**\n * Indicates whether the player is currently playing back the media to an external device via AirPlay.\n *\n * @platform ios\n */\n readonly isExternalPlaybackActive: boolean;\n\n /**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n *\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * By default, the player seeks to the exact requested time.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n */\n seekTolerance: SeekTolerance;\n\n /**\n * Determines whether the scrubbing mode is enabled and what scrubbing optimizations should be enabled.\n *\n * > See [`SeekTolerance`](#seektolerance) to set the seeking tolerance, which can also affect the scrubbing performance.\n *\n */\n scrubbingModeOptions: ScrubbingModeOptions;\n\n /**\n * Initializes a new video player instance with the given source.\n *\n * @param source The source of the video to be played.\n * @param useSynchronousReplace Optional parameter, when `true` `source` from the first parameter will be loaded on the main thread.\n * @param playerBuilderOptions Options to apply to the player builder before the native constructor is invoked.\n * @hidden\n */\n constructor(\n source: VideoSource,\n useSynchronousReplace?: boolean,\n playerBuilderOptions?: PlayerBuilderOptions\n );\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n *\n * > On iOS, this method loads the asset data synchronously on the UI thread and can block it for extended periods of time.\n * > Use `replaceAsync` to load the asset asynchronously and avoid UI lags.\n *\n * > This method will be deprecated in the future.\n */\n replace(source: VideoSource, disableWarning?: boolean): void;\n\n /**\n * Replaces the current source with a new one, while offloading loading of the asset to a different thread.\n *\n * > On Android and Web, this method is equivalent to `replace`.\n */\n replaceAsync(source: VideoSource): Promise;\n\n /**\n * Seeks the playback by the given number of seconds. The time to which the player seeks may differ from the specified requested time for efficiency,\n * depending on the encoding and what is currently buffered by the player. Use this function to implement playback controls that seek by specific amount of time,\n * in which case, the actual time usually does not have to be precise. For frame accurate seeking, use the [`currentTime`](#currenttime) property.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n\n /**\n * Generates thumbnails from the currently played asset. The thumbnails are references to native images,\n * thus they can be used as a source of the `Image` component from `expo-image`.\n * @platform android\n * @platform ios\n */\n generateThumbnailsAsync(\n times: number | number[],\n options?: VideoThumbnailOptions\n ): Promise;\n}\n\n/**\n * Additional options for video thumbnails generation.\n */\nexport type VideoThumbnailOptions = {\n /**\n * If provided, the generated thumbnail will not exceed this width in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxWidth?: number;\n\n /**\n * If provided, the generated thumbnail will not exceed this height in pixels, preserving its aspect ratio.\n * @platform android\n * @platform ios\n */\n maxHeight?: number;\n};\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport type VideoSource = string | number | null | VideoSourceObject;\n\nexport type VideoSourceObject = {\n /**\n * The URI of the video.\n *\n * On iOS, `PHAsset` URIs are supported, but can only be loaded using the [`replaceAsync`](#replaceasyncsource) method or the default [`VideoPlayer`](#videoplayer) constructor.\n *\n * This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.\n */\n uri?: string;\n\n /**\n * The asset ID of a local video asset, acquired with the `require` function.\n * This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.\n */\n assetId?: number;\n\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\n\n /**\n * Specifies information which will be displayed in the now playing notification.\n * When undefined the player will display information contained in the video metadata.\n * @platform android\n * @platform ios\n */\n metadata?: VideoMetadata;\n\n /**\n * Specifies headers sent with the video request.\n * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).\n * @platform android\n * @platform ios\n */\n headers?: Record;\n\n /**\n * Specifies whether the player should use caching for the video.\n * > Due to platform limitations, the cache cannot be used with HLS video sources on iOS. Caching DRM-protected videos is not supported on Android and iOS.\n * @default false\n * @platform android\n * @platform ios\n */\n useCaching?: boolean;\n\n /**\n * Specifies the content type of the video source. When set to `'auto'`, the player will try to automatically determine the content type.\n *\n * You should use this property when playing HLS, SmoothStreaming or DASH videos from an uri, which does not contain a standardized extension for the corresponding media type.\n * @default 'auto'\n * @platform android\n * @platform ios\n */\n contentType?: ContentType;\n};\n\n/**\n * Contains information about any errors that the player encountered during the playback\n */\nexport type PlayerError = {\n message: string;\n};\n\n/**\n * Contains information that will be displayed in the now playing notification when the video is playing.\n * @platform android\n * @platform ios\n */\nexport type VideoMetadata = {\n /**\n * The title of the video.\n * @platform android\n * @platform ios\n */\n title?: string;\n /**\n * Secondary text that will be displayed under the title.\n * @platform android\n * @platform ios\n */\n artist?: string;\n /**\n * The uri of the video artwork.\n * @platform android\n * @platform ios\n */\n artwork?: string;\n};\n\n/**\n * Specifies which type of DRM to use:\n * - Android supports ClearKey, PlayReady and Widevine.\n * - iOS supports FairPlay.\n */\nexport type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\nexport type DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: Record;\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n\n /**\n * Specifies the base64 encoded certificate data for the FairPlay DRM.\n * When this property is set, the `certificateUrl` property is ignored.\n * @platform ios\n */\n base64CertificateData?: string;\n};\n\n/**\n * Specifies buffer options which will be used by the player when buffering the video.\n *\n * @platform android\n * @platform ios\n */\nexport type BufferOptions = {\n /**\n * The duration in seconds which determines how much media the player should buffer ahead of the current playback time.\n *\n * On iOS when set to `0` the player will automatically decide appropriate buffer duration.\n *\n * Equivalent to [`AVPlayerItem.preferredForwardBufferDuration`](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration).\n * @default Android: 20, iOS: 0\n * @platform android\n * @platform ios\n */\n readonly preferredForwardBufferDuration?: number;\n\n /**\n * A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.\n *\n * Equivalent to [`AVPlayer.automaticallyWaitsToMinimizeStalling`](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal).\n * @default true\n * @platform ios\n */\n readonly waitsToMinimizeStalling?: boolean;\n\n /**\n * Minimum duration of the buffer in seconds required to continue playing after the player has been paused or started buffering.\n *\n * > This property will be ignored if `preferredForwardBufferDuration` is lower.\n * @default 2\n * @platform android\n */\n readonly minBufferForPlayback?: number;\n\n /**\n * The maximum number of bytes that the player can buffer from the network.\n * When 0 the player will automatically decide appropriate buffer size.\n *\n * @default 0\n * @platform android\n */\n readonly maxBufferBytes?: number | null;\n\n /**\n * A Boolean value which determines whether the player should prioritize time over size when buffering media.\n *\n * @default false\n * @platform android\n */\n readonly prioritizeTimeOverSizeThreshold?: boolean;\n};\n\n/**\n * Specifies the content type of the source.\n *\n * - `auto`: The player will automatically determine the content type of the video.\n * - `progressive`: The player will use progressive download content type. This is the default `ContentType` when the uri does not contain an extension.\n * - `hls`: The player will use HLS content type.\n * - `dash`: The player will use DASH content type (Android-only).\n * - `smoothStreaming`: The player will use SmoothStreaming content type (Android-only).\n *\n * @default `auto`\n */\nexport type ContentType = 'auto' | 'progressive' | 'hls' | 'dash' | 'smoothStreaming';\n\n/**\n * Specifies the audio mode that the player should use. Audio mode is set on per-app basis, if there are multiple players playing and\n * have different a `AudioMode` specified, the highest priority mode will be used. Priority order: 'doNotMix' > 'auto' > 'duckOthers' > 'mixWithOthers'.\n *\n * - `mixWithOthers`: The player will mix its audio output with other apps.\n * - `duckOthers`: The player will lower the volume of other apps if any of the active players is outputting audio.\n * - `auto`: The player will allow other apps to keep playing audio only when it is muted. On iOS it will always interrupt other apps when `showNowPlayingNotification` is `true` due to system requirements.\n * - `doNotMix`: The player will pause playback in other apps, even when it's muted.\n *\n * > On iOS, the Now Playing notification is dependent on the audio mode. If the audio mode is different from `doNotMix` or `auto` this feature will not work.\n */\nexport type AudioMixingMode = 'mixWithOthers' | 'duckOthers' | 'auto' | 'doNotMix';\n\nexport type SubtitleTrack = {\n /**\n * A string used by `expo-video` to identify the subtitle track.\n *\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the subtitle track. For example, `en`, `pl`, `de`.\n */\n language: string;\n\n /**\n * Label of the subtitle track in the language of the device.\n */\n label: string;\n};\n\n/**\n * Specifies a VideoTrack loaded from a [`VideoSource`](#videosource).\n */\nexport type VideoTrack = {\n /**\n * The id of the video track.\n *\n * > This field is platform-specific and may return different depending on the operating system.\n */\n id: string;\n\n /**\n * The URL of the `VideoTrack` for HLS video sources. `null` for other source types.\n */\n url: string | null;\n\n /**\n * Size of the video track.\n */\n size: VideoSize;\n\n /**\n * MimeType of the video track or null if unknown.\n */\n mimeType: string | null;\n\n /**\n * Indicates whether the video track format is supported by the device.\n *\n * @platform android\n */\n isSupported: boolean;\n\n /**\n * Specifies the bitrate in bits per second. This is the peak bitrate if known, or else the average bitrate if known, or else null.\n *\n * @deprecated Use `peakBitrate` or `averageBitrate` instead.\n */\n bitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n *\n */\n averageBitrate: number | null;\n\n /**\n * Specifies the average bitrate in bits per second or null if the value is unknown.\n */\n peakBitrate: number | null;\n\n /**\n * Specifies the frame rate of the video track in frames per second.\n */\n frameRate: number | null;\n};\n\n/**\n * Specifies the size of a video track.\n */\nexport type VideoSize = {\n /**\n * Width of the video track in pixels.\n */\n width: number;\n /**\n * Height of the video track in pixels.\n */\n height: number;\n};\n\nexport type AudioTrack = {\n /**\n * A string used by expo-video to identify the audio track.\n * @platform android\n */\n id?: string;\n\n /**\n * Language of the audio track. For example, 'en', 'pl', 'de'.\n */\n language: string;\n\n /**\n * Label of the audio track in the language of the device.\n */\n label: string;\n};\n\n/**\n * Determines the time that the actual position seeked to may precede or exceed the requested seek position.\n * Larger tolerance will usually result in faster seeking.\n * This property affects the precision of setting the [`currentTime`](#currenttime) property and the [`seekBy`](#seekbyseconds) method, and on Android, it also affects the accuracy of the scrubber from the default native controls.\n *\n * > If you are trying to optimize for scrubbing (many frequent seeks), also see [`ScrubbingModeOptions`](#scrubbingmodeoptions-1).\n *\n * @platform android\n * @platform ios\n */\nexport type SeekTolerance = {\n /**\n * The maximum time that the actual position seeked to may precede the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceBefore?: number;\n\n /**\n * The maximum time that the actual position seeked to may exceed the requested seek position, in seconds. Must be non-negative.\n * @default 0\n */\n toleranceAfter?: number;\n};\n\n/**\n * Defines scrubbing mode options used by a [`VideoPlayer`](#videoplayer).\n */\nexport type ScrubbingModeOptions = {\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * You should only enable this when the player is receiving a large number of seeks in a short period of time. For less frequent seeks, fine-tuning the [`SeekTolerance`](#seektolerance-1) may be sufficient.\n *\n * On Android, the player may consume more resources in this mode, so it should only be used for short periods of time in response to user interaction (for example, dragging on a progress bar UI element).\n *\n * On Android, when `scrubbingModeEnabled` is `true`, the playback is suppressed. You should set this property back to `false` when the user interaction ends to allow the playback to resume.\n * For best results, on iOS you should pause the playback when scrubbing.\n *\n * > For best scrubbing performance, consider also increasing the seeking tolerance using the [`SeekTolerance`](#seektolerance-1) property.\n *\n * > Other scrubbing mode options will have no effect when this is `false`.\n * @default false\n * @platform android\n * @platform ios\n */\n scrubbingModeEnabled?: boolean;\n\n /**\n * Whether the codec operating rate should be increased in scrubbing mode.\n *\n * @platform android\n * @default true\n */\n increaseCodecOperatingRate?: boolean;\n\n /**\n * Sets whether ExoPlayer's dynamic scheduling should be enabled in scrubbing mode.\n * This can result in available output buffers being handled more quickly when seeking.\n *\n * @platform android\n * @default true\n */\n enableDynamicScheduling?: boolean;\n /**\n * Sets whether to use `MediaCodec.BUFFER_FLAG_DECODE_ONLY` in scrubbing mode.\n * When playback is using MediaCodec on API 34+, this flag can speed up seeking by signalling that the decoded output of buffers between the previous keyframe and the target frame is not needed by the player.\n *\n * @platform android\n * @default true\n */\n useDecodeOnlyFlag?: boolean;\n\n /**\n * Sets whether to avoid flushing the decoder (where possible) in scrubbing mode.\n * When `true`, avoids flushing the decoder when a new seek starts decoding from a key-frame in compatible content.\n *\n * @platform android\n * @default true\n */\n allowSkippingMediaCodecFlush?: boolean;\n};\n\n/**\n * Options to apply to the player builder before the native constructor is invoked\n * @platform android\n */\nexport type PlayerBuilderOptions = {\n /**\n * Seek backward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekBackwardIncrement?: number;\n\n /**\n * Seek forward increment in seconds.\n * Values will be clamped between 0.001 and 999 seconds.\n * @platform android\n */\n seekForwardIncrement?: number;\n};\n"]}
\ No newline at end of file
diff --git a/packages/expo-video/ios/Records/Tracks.swift b/packages/expo-video/ios/Records/Tracks.swift
index 18055a186685c1..13c067f390a407 100644
--- a/packages/expo-video/ios/Records/Tracks.swift
+++ b/packages/expo-video/ios/Records/Tracks.swift
@@ -35,6 +35,7 @@ internal struct AudioTrack: Record {
internal struct VideoTrack: Record, Equatable {
@Field var id: String? = nil
+ @Field var url: URL? = nil
@Field var size: VideoSize? = nil
@Field var mimeType: String? = nil
@Field var bitrate: Int? = nil // deprecated as of SDK 55
@@ -79,7 +80,34 @@ internal struct VideoTrack: Record, Equatable {
)
}
- static func from(hlsHeaderLine: String, idLine: String) -> VideoTrack? {
+ @available(iOS 26, tvOS 26, *)
+ static func from(assetVariant: AVAssetVariant, isPlayable: Bool, mainUrl: URL) -> VideoTrack? {
+ guard let videoAttributes = assetVariant.videoAttributes else {
+ return nil
+ }
+
+ let trackUrl = assetVariant.url
+ let id = extractHlsTrackId(trackUrl: trackUrl, mainUrl: mainUrl)
+ let videoSize = videoAttributes.videoSize
+ let mimeType = videoAttributes.getFormattedCodecString()
+ let frameRate = videoAttributes.nominalFrameRate.map(Float.init)
+ let peakBitrate = assetVariant.peakBitRate.map(Int.init)
+ let averageBitrate = assetVariant.averageBitRate.map(Int.init)
+
+ return VideoTrack(
+ id: id,
+ url: trackUrl,
+ size: videoSize,
+ mimeType: mimeType,
+ bitrate: peakBitrate ?? averageBitrate,
+ peakBitrate: peakBitrate,
+ averageBitrate: averageBitrate,
+ isSupported: isPlayable,
+ frameRate: frameRate
+ )
+ }
+
+ static func from(hlsHeaderLine: String, idLine: String, mainUrl: URL) -> VideoTrack? {
// The minimum information we require from a video track is it's resolution
guard hlsHeaderLine.starts(with: "#EXT-X-STREAM-INF"), hlsHeaderLine.contains("RESOLUTION") else {
return nil
@@ -105,6 +133,7 @@ internal struct VideoTrack: Record, Equatable {
return nil
}
+ let id = idLine.trimmingCharacters(in: .whitespacesAndNewlines)
let size = VideoSize(width: width, height: height)
let mimeType = codecsToMimeType(codecs: details["CODECS"])
var peakBitrage: Int? = nil
@@ -125,7 +154,8 @@ internal struct VideoTrack: Record, Equatable {
let bitrate = peakBitrage ?? averageBitrate
return VideoTrack(
- id: idLine,
+ id: id,
+ url: resolveMediaUrl(pathLine: idLine, mainUrl: mainUrl),
size: size,
mimeType: mimeType,
bitrate: bitrate,
@@ -157,82 +187,3 @@ internal struct VideoTrack: Record, Equatable {
}
}
// swiftlint:enable redundant_optional_initialization
-
-// https://developer.apple.com/documentation/avfoundation/avpartialasyncproperty/formatdescriptions
-private extension AVAssetTrack {
- var mediaFormat: String {
- get async throws {
- var format = ""
- let descriptions = try await load(.formatDescriptions)
- for (index, formatDesc) in descriptions.enumerated() {
- let subType = CMFormatDescriptionGetMediaSubType(formatDesc).toString()
-
- // The reported subType is different for iOS and Android, ideally they should be the same
- let correctedSubType: String
- switch subType {
- case "avc1": // H264 videos
- correctedSubType = "avc"
- case "hev1": // H265 videos
- correctedSubType = "hevc"
- default:
- correctedSubType = subType
- }
- format += "video/\(correctedSubType)"
- if index < descriptions.count - 1 {
- format += ","
- }
- }
- return format
- }
- }
-
- // Decently reliable way to extract peak bitrate from MP4 containers
- // Unlike for average bitrate, we can't get this information from AVKit API
- func getPeakBitrate() async -> Int? {
- guard let videoDescriptions = try? await self.load(.formatDescriptions),
- let videoDescription = (videoDescriptions.first { $0.mediaType == .video }),
- let extensions = videoDescription.getBitrateParentExtension(),
- // If the container publishes the peak bitrate at all, it should be declared in this box
- let btrtData = extensions["btrt"] as? Data, btrtData.count >= 12
- else {
- return nil
- }
-
- // https://mpeggroup.github.io/FileFormatConformance/?query=%3D%22btrt%22 (see under `Syntax`)
- // Byte 0-3 (00060dd5): Buffer Size
- // Byte 4-7 (0051fb78): Max Bitrate (Peak)
- // Byte 8-11 (001e6270): Average Bitrate
-
- // Extract bytes 4-7 (The MaxBitrate field)
- let maxBitrateData = btrtData.subdata(in: 4..<8)
-
- let maxBitrate = maxBitrateData.reduce(0) { result, byte in
- return (result << 8) | UInt32(byte)
- }
-
- return Int(maxBitrate)
- }
-}
-
-private extension CMFormatDescription {
- func getBitrateParentExtension() -> [String: Any]? {
- let extensionKey = kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms
- return CMFormatDescriptionGetExtension(self, extensionKey: extensionKey) as? [String: Any]
- }
-}
-
-private extension FourCharCode {
- // Create a string representation of a FourCC.
- func toString() -> String {
- let bytes: [CChar] = [
- CChar((self >> 24) & 0xff),
- CChar((self >> 16) & 0xff),
- CChar((self >> 8) & 0xff),
- CChar(self & 0xff),
- 0
- ]
- let result = String(cString: bytes)
- let characterSet = CharacterSet.whitespaces
- return result.trimmingCharacters(in: characterSet)
- }
-}
diff --git a/packages/expo-video/ios/Utils/AVAssetVariant+VideoTracks.swift b/packages/expo-video/ios/Utils/AVAssetVariant+VideoTracks.swift
new file mode 100644
index 00000000000000..bad941f867c9bc
--- /dev/null
+++ b/packages/expo-video/ios/Utils/AVAssetVariant+VideoTracks.swift
@@ -0,0 +1,17 @@
+import AVKit
+
+extension AVAssetVariant.VideoAttributes {
+ var videoSize: VideoSize {
+ return VideoSize(width: Int(presentationSize.width), height: Int(presentationSize.height))
+ }
+
+ func getFormattedCodecString() -> String {
+ guard let codecType = self.codecTypes.first else {
+ return "video/unknown"
+ }
+
+ let fourCC = CMVideoCodecType(codecType).toCorrectedString()
+
+ return "video/\(fourCC)"
+ }
+}
diff --git a/packages/expo-video/ios/Utils/AvAssetTrack+VideoTracks.swift b/packages/expo-video/ios/Utils/AvAssetTrack+VideoTracks.swift
new file mode 100644
index 00000000000000..1ed65ad41c32cc
--- /dev/null
+++ b/packages/expo-video/ios/Utils/AvAssetTrack+VideoTracks.swift
@@ -0,0 +1,52 @@
+import AVKit
+
+extension AVAssetTrack {
+ var mediaFormat: String {
+ get async throws {
+ var format = ""
+ let descriptions = try await load(.formatDescriptions)
+ for (index, formatDesc) in descriptions.enumerated() {
+ let subType = CMFormatDescriptionGetMediaSubType(formatDesc).toCorrectedString()
+
+ // The reported subType is different for iOS and Android, ideally they should be the same
+ format += "video/\(subType)"
+ if index < descriptions.count - 1 {
+ format += ","
+ }
+ }
+ return format
+ }
+ }
+
+ // Decently reliable way to extract peak bitrate from MP4 containers
+ // Unlike for average bitrate, we can't get this information from AVKit API
+ func getPeakBitrate() async -> Int? {
+ guard let videoDescriptions = try? await self.load(.formatDescriptions),
+ let videoDescription = (videoDescriptions.first { $0.mediaType == .video }),
+ let extensions = getBitrateParentExtension(from: videoDescription),
+ // If the container publishes the peak bitrate at all, it should be declared in this box
+ let btrtData = extensions["btrt"] as? Data, btrtData.count >= 12
+ else {
+ return nil
+ }
+
+ // https://mpeggroup.github.io/FileFormatConformance/?query=%3D%22btrt%22 (see under `Syntax`)
+ // Byte 0-3 (00060dd5): Buffer Size
+ // Byte 4-7 (0051fb78): Max Bitrate (Peak)
+ // Byte 8-11 (001e6270): Average Bitrate
+
+ // Extract bytes 4-7 (The MaxBitrate field)
+ let maxBitrateData = btrtData.subdata(in: 4..<8)
+
+ let maxBitrate = maxBitrateData.reduce(0) { result, byte in
+ return (result << 8) | UInt32(byte)
+ }
+
+ return Int(maxBitrate)
+ }
+
+ private func getBitrateParentExtension(from videoDescription: CMFormatDescription) -> [String: Any]? {
+ let extensionKey = kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms
+ return CMFormatDescriptionGetExtension(videoDescription, extensionKey: extensionKey) as? [String: Any]
+ }
+}
diff --git a/packages/expo-video/ios/Utils/FourCharCode+toString.swift b/packages/expo-video/ios/Utils/FourCharCode+toString.swift
new file mode 100644
index 00000000000000..9c1fab689bd99a
--- /dev/null
+++ b/packages/expo-video/ios/Utils/FourCharCode+toString.swift
@@ -0,0 +1,25 @@
+extension FourCharCode {
+ func toCorrectedString() -> String {
+ switch toString() {
+ case "avc1": // H264 videos
+ return "avc"
+ case "hev1": // H265 videos
+ return "hevc"
+ default:
+ return self.toString()
+ }
+ }
+
+ func toString() -> String {
+ let bytes: [CChar] = [
+ CChar((self >> 24) & 0xff),
+ CChar((self >> 16) & 0xff),
+ CChar((self >> 8) & 0xff),
+ CChar(self & 0xff),
+ 0
+ ]
+ let result = String(cString: bytes)
+ let characterSet = CharacterSet.whitespaces
+ return result.trimmingCharacters(in: characterSet)
+ }
+}
diff --git a/packages/expo-video/ios/Utils/HlsUriUtils.swift b/packages/expo-video/ios/Utils/HlsUriUtils.swift
new file mode 100644
index 00000000000000..7171ae14e49401
--- /dev/null
+++ b/packages/expo-video/ios/Utils/HlsUriUtils.swift
@@ -0,0 +1,27 @@
+/// Resolves a variant stream's URL relative to the main Playlist URL.
+func resolveMediaUrl(pathLine: String, mainUrl: URL) -> URL? {
+ let cleanedPath = pathLine.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ if let absoluteURL = URL(string: cleanedPath), absoluteURL.scheme != nil {
+ return absoluteURL
+ }
+
+ return URL(string: cleanedPath, relativeTo: mainUrl)
+}
+
+// Based on the track URL and the main playlist URL generates a
+// RFC 8216 - compliant https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.4.2
+// Which we use as the track id
+func extractHlsTrackId(trackUrl: URL, mainUrl: URL) -> String {
+ let mainBaseString = mainUrl.deletingLastPathComponent().absoluteString
+ let trackString = trackUrl.absoluteString
+
+ if trackString.hasPrefix(mainBaseString) {
+ // mainUrl: "https://test.com/video/masterManifest.m3u8"
+ // Track: "https://test.com/video/playlist/manifest1.m3u8"
+ // Result: "playlist/manifest1.m3u8"
+ return String(trackString.dropFirst(mainBaseString.count))
+ }
+
+ return trackString.trimmingCharacters(in: .whitespacesAndNewlines)
+}
diff --git a/packages/expo-video/ios/VideoPlayerItem.swift b/packages/expo-video/ios/VideoPlayerItem.swift
index 7a6d2a97d7e9e9..c3d3394391ac94 100644
--- a/packages/expo-video/ios/VideoPlayerItem.swift
+++ b/packages/expo-video/ios/VideoPlayerItem.swift
@@ -20,7 +20,7 @@ class VideoPlayerItem: AVPlayerItem {
return nil
}
self.videoSource = videoSource
- self.isHls = videoSource.uri?.pathExtension == "m3u8" || videoSource.contentType == .hls
+ self.isHls = videoSource.uri?.isHLS == true || videoSource.contentType == .hls
let asset = VideoAsset(url: url, videoSource: videoSource)
self.urlAsset = asset
@@ -33,7 +33,7 @@ class VideoPlayerItem: AVPlayerItem {
return nil
}
self.videoSource = videoSource
- self.isHls = videoSource.uri?.pathExtension == "m3u8" || videoSource.contentType == .hls
+ self.isHls = videoSource.uri?.isHLS == true || videoSource.contentType == .hls
let asset = VideoAsset(url: url, videoSource: videoSource)
self.urlAsset = asset
@@ -55,25 +55,54 @@ class VideoPlayerItem: AVPlayerItem {
func createTracksLoadingTask() {
tracksLoadingTask = Task { [weak self] in
- var tracks: [VideoTrack] = []
- guard let self else {
+ guard let self, let mainUrl = videoSource.uri else {
return []
}
- if isHls {
- do {
- tracks = try await self.fetchHlsVideoTracks()
- } catch {
- tracks = []
- log.warn("[expo-video] Failed to fetch HLS video tracks, this is not required for playback, but `expo-video` will have no knowledge of the available tracks: \(error.localizedDescription)")
- }
- } else {
- let avAssetTracks = (try? await asset.loadTracks(withMediaType: .video)) ?? []
- for avAssetTrack in avAssetTracks {
+ var tracks: [VideoTrack] = []
+ if let assetTracks = try? await urlAsset.loadTracks(withMediaType: .video) {
+ for avAssetTrack in assetTracks {
tracks.append(await VideoTrack.from(assetTrack: avAssetTrack))
}
}
- return tracks
+
+ guard isHls else {
+ return tracks
+ }
+
+ let hlsTracks = await loadHlsTracks(mainUrl: mainUrl)
+ return tracks + hlsTracks
+ }
+ }
+
+ // MARK: - HLS Helpers
+
+ private func loadHlsTracks(mainUrl: URL) async -> [VideoTrack] {
+ if #available(iOS 26.0, tvOS 26, *) {
+ return await loadModernHlsTracks(mainUrl: mainUrl)
+ }
+
+ return await loadLegacyHlsTracks()
+ }
+
+ @available(iOS 26.0, tvOS 26, *)
+ private func loadModernHlsTracks(mainUrl: URL) async -> [VideoTrack] {
+ guard let variants = try? await urlAsset.load(.variants) else {
+ return []
+ }
+ let isPlayable = (try? await urlAsset.load(.isPlayable)) ?? false
+
+ return variants.compactMap { variant in
+ VideoTrack.from(assetVariant: variant, isPlayable: isPlayable, mainUrl: mainUrl)
+ }
+ }
+
+ private func loadLegacyHlsTracks() async -> [VideoTrack] {
+ do {
+ return try await self.fetchHlsVideoTracks()
+ } catch {
+ log.warn("[expo-video] Failed to fetch HLS video tracks: \(error.localizedDescription)")
+ return []
}
}
@@ -93,13 +122,21 @@ class VideoPlayerItem: AVPlayerItem {
let (data, _) = try await URLSession.shared.data(for: request)
let content = String(data: data, encoding: .utf8) ?? ""
- return parseM3U8(content)
+ return parseM3U8(content, mainUrl: uri)
}
- private func parseM3U8(_ content: String) -> [VideoTrack] {
+ private func parseM3U8(_ content: String, mainUrl: URL) -> [VideoTrack] {
let lines = content.components(separatedBy: "\n")
return zip(lines, lines.dropFirst()).compactMap { line, nextLine in
- VideoTrack.from(hlsHeaderLine: line, idLine: nextLine)
+ VideoTrack.from(hlsHeaderLine: line, idLine: nextLine, mainUrl: mainUrl)
}
}
}
+
+private extension URL {
+ var isHLS: Bool {
+ // https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8
+ // Above is a valid link, even though the path extension is an empty string, we can use a more primitive suffix method as a fallback
+ return self.pathExtension.lowercased() == "m3u8" || self.absoluteString.lowercased().hasSuffix("m3u8")
+ }
+}
diff --git a/packages/expo-video/ios/VideoPlayerObserver.swift b/packages/expo-video/ios/VideoPlayerObserver.swift
index 5c0a2f9dbc3bdc..56daccd4ac933d 100644
--- a/packages/expo-video/ios/VideoPlayerObserver.swift
+++ b/packages/expo-video/ios/VideoPlayerObserver.swift
@@ -588,18 +588,7 @@ private extension AVPlayerItemAccessLogEvent {
return nil
}
- // We need to find a base uri to which the track id is added for streaming a specific source
- var components = URLComponents(url: itemUrl, resolvingAgainstBaseURL: false)
- components?.query = nil
-
- guard let baseUriString = components?.url?.deletingLastPathComponent().absoluteString else {
- return nil
- }
-
- // Removing the base uri from the log uri allows us to get the id, which can be matched to an existing VideoTrack
- let id = logUri.replacingOccurrences(of: baseUriString, with: "")
-
- return videoTracks.first { $0.id == id }
+ return videoTracks.first { $0.url?.absoluteString == logUri }
}
}
diff --git a/packages/expo-video/src/VideoPlayer.types.ts b/packages/expo-video/src/VideoPlayer.types.ts
index 3275f60a141031..d0240354f3be5f 100644
--- a/packages/expo-video/src/VideoPlayer.types.ts
+++ b/packages/expo-video/src/VideoPlayer.types.ts
@@ -598,6 +598,11 @@ export type VideoTrack = {
*/
id: string;
+ /**
+ * The URL of the `VideoTrack` for HLS video sources. `null` for other source types.
+ */
+ url: string | null;
+
/**
* Size of the video track.
*/
diff --git a/packages/jest-expo/CHANGELOG.md b/packages/jest-expo/CHANGELOG.md
index a117005de50faa..0f866922208473 100644
--- a/packages/jest-expo/CHANGELOG.md
+++ b/packages/jest-expo/CHANGELOG.md
@@ -8,6 +8,8 @@
### 🐛 Bug fixes
+- Fix `expo-file-system` mock to target `expo-file-system/legacy` instead of replacing the entire module, and preserve prototype chains for native module class mocks. ([#43005](https://github.com/expo/expo/pull/43005) by [@aleqsio](https://github.com/aleqsio))
+
### 💡 Others
## 55.0.6 — 2026-02-03
diff --git a/packages/jest-expo/src/preset/setup.js b/packages/jest-expo/src/preset/setup.js
index 1331d0a6d53c78..601b5470040279 100644
--- a/packages/jest-expo/src/preset/setup.js
+++ b/packages/jest-expo/src/preset/setup.js
@@ -127,7 +127,7 @@ Object.keys(mockNativeModules.NativeUnimoduleProxy.viewManagersMetadata).forEach
jest.mock('expo/src/async-require/messageSocket', () => undefined);
try {
- jest.mock('expo-file-system', () => ({
+ jest.mock('expo-file-system/legacy', () => ({
downloadAsync: jest.fn(() => Promise.resolve({ md5: 'md5', uri: 'uri' })),
getInfoAsync: jest.fn(() => Promise.resolve({ exists: true, md5: 'md5', uri: 'uri' })),
readAsStringAsync: jest.fn(() => Promise.resolve()),
@@ -266,7 +266,14 @@ jest.doMock('expo-modules-core', () => {
const nativeModule = new NativeModule();
for (const [key, value] of Object.entries(nativeModuleMock)) {
- nativeModule[key] = typeof value === 'function' ? jest.fn(value) : value;
+ if (typeof value === 'function') {
+ // Don't wrap classes with jest.fn() as it destroys the prototype chain
+ // needed for `extends` (e.g. File extends ExpoFileSystem.FileSystemFile).
+ const isClass = Object.getOwnPropertyNames(value.prototype ?? {}).length > 1;
+ nativeModule[key] = isClass ? value : jest.fn(value);
+ } else {
+ nativeModule[key] = value;
+ }
}
return nativeModule;
}
diff --git a/tools/src/commands/GenerateDocsAPIData.ts b/tools/src/commands/GenerateDocsAPIData.ts
index 63b4b14cd48a2f..9fe551afca227a 100644
--- a/tools/src/commands/GenerateDocsAPIData.ts
+++ b/tools/src/commands/GenerateDocsAPIData.ts
@@ -64,6 +64,7 @@ const uiPackagesMapping: Record = {
'expo-ui/swift-ui/zstack': ['swift-ui/ZStack/index.tsx', 'expo-ui'],
'expo-ui/swift-ui/spacer': ['swift-ui/Spacer/index.tsx', 'expo-ui'],
'expo-ui/swift-ui/image': ['swift-ui/Image/index.tsx', 'expo-ui'],
+ 'expo-ui/swift-ui/scrollview': ['swift-ui/ScrollView/index.tsx', 'expo-ui'],
'expo-ui/swift-ui/foreach': ['swift-ui/ForEach/index.tsx', 'expo-ui'],
'expo-ui/jetpack-compose/textinput': ['jetpack-compose/TextInput/index.tsx', 'expo-ui'],
'expo-ui/jetpack-compose/chip': ['jetpack-compose/Chip/index.tsx', 'expo-ui'],
diff --git a/yarn.lock b/yarn.lock
index 0fb912bdadaff7..c8e1ccb8f8096c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6549,6 +6549,11 @@ commander@^12.0.0, commander@^12.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+commander@^14.0.3:
+ version "14.0.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.3.tgz#425d79b48f9af82fcd9e4fc1ea8af6c5ec07bbc2"
+ integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==
+
commander@^2.20.0, commander@^2.8.1:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"