Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6bae48f
[ios][dev-launcher] rework local permissions UI (#43252)
alanjhughes Feb 19, 2026
a88fab5
[video] Add `name`, `isDefault` and `autoSelect` fields to audio and …
behenate Feb 19, 2026
c558433
[android][image] Add `CookieJar` to image request client (#43257)
alanjhughes Feb 19, 2026
4f6b841
fix(fingerprint): Add independent dependency for `@expo/env` (#42764)
kitten Feb 19, 2026
ca793d7
[ios][audio] Support native preloading (#43061)
alanjhughes Feb 19, 2026
fbd9d3a
[android][audio] Support native preloading (#43062)
alanjhughes Feb 19, 2026
a95d752
[web][audio] Support preloading (#43063)
alanjhughes Feb 19, 2026
3269b7b
[core][location][Android] Remove `MapHelper` (#43240)
lukmccall Feb 19, 2026
1370354
[core][Android] Remove `ModuleNotFoundException` (#43261)
lukmccall Feb 19, 2026
66a3275
[core][task-manager][Android] Remove `AppConfigurationError` & `AppLo…
lukmccall Feb 19, 2026
2b06710
[core][web-browser][Android] Remove `CurrentActivityNotFoundException…
lukmccall Feb 19, 2026
fbe8e90
[web][audio] Fix nan duration (#43268)
alanjhughes Feb 19, 2026
a894fa3
[maps][Android] Run `spotless` (#43269)
lukmccall Feb 19, 2026
f96d4e3
[android][video] Avoid crash when FullscreenPlayerActivity init fails…
amyu Feb 19, 2026
d92c956
fix(cli,metro-config): Force enable Metro's internal `forceNodeFilesy…
kitten Feb 19, 2026
55a0d53
[dev-launcher] rephrase LocalNetworkPermissionView message (#43278)
vonovak Feb 19, 2026
6cbc04d
[create-expo-module] Fix private registry access (#43275)
alanjhughes Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions apps/native-component-list/src/screens/Audio/AudioPreloadScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import {
preload,
getPreloadedSources,
clearAllPreloadedSources,
useAudioPlayer,
useAudioPlayerStatus,
AudioPlayer,
AudioSource,
AudioStatus,
} from 'expo-audio';
import React from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';

import HeadingText from '../../components/HeadingText';
import ListButton from '../../components/ListButton';
import Colors from '../../constants/Colors';

export const sfx1: AudioSource = {
uri: 'https://cdn.freesound.org/previews/370/370182_6430986-hq.mp3',
};

export const sfx2: AudioSource = {
uri: 'https://cdn.freesound.org/previews/401/401542_2331641-hq.mp3',
};

export default function AudioPreloadScreen(props: any) {
React.useLayoutEffect(() => {
props.navigation.setOptions({
title: 'Audio Preloading',
});
});

return (
<ScrollView contentContainerStyle={styles.contentContainer}>
<HeadingText>Sound Effects</HeadingText>
<Text style={styles.hint}>
Both sounds were preloaded in module scope. Tap the buttons to play — they should start
near-instantly.
</Text>
<SoundEffectButtons />

<HeadingText>Preload Cache</HeadingText>
<PreloadCacheInfo />

<HeadingText>Preloaded Player</HeadingText>
<Text style={styles.hint}>
This player uses a preloaded source. Try replacing to swap between the two preloaded tracks.
</Text>
<PreloadedPlayer />
</ScrollView>
);
}

function SoundEffectButtons() {
const player1 = useAudioPlayer(sfx1, { keepAudioSessionActive: true });
const player2 = useAudioPlayer(sfx2, { keepAudioSessionActive: true });
const status1 = useAudioPlayerStatus(player1);
const status2 = useAudioPlayerStatus(player2);

const playSfx = (player: AudioPlayer, status: AudioStatus) => {
if (status.playing) {
player.seekTo(0);
} else {
if (status.didJustFinish || status.currentTime > 0) {
player.seekTo(0);
}
player.play();
}
};

return (
<View style={styles.sfxRow}>
<SfxButton
label="Sound 1"
isLoaded={status1.isLoaded}
onPress={() => playSfx(player1, status1)}
/>
<SfxButton
label="Sound 2"
isLoaded={status2.isLoaded}
onPress={() => playSfx(player2, status2)}
/>
</View>
);
}

function SfxButton({
label,
isLoaded,
onPress,
}: {
label: string;
isLoaded: boolean;
onPress: () => void;
}) {
return (
<Pressable
style={({ pressed }) => [styles.sfxButton, pressed && styles.sfxButtonPressed]}
onPress={onPress}>
<Text style={styles.sfxButtonText}>{label}</Text>
<Text style={styles.sfxButtonStatus}>{isLoaded ? 'Ready' : 'Loading...'}</Text>
</Pressable>
);
}

function PreloadCacheInfo() {
const [sources, setSources] = React.useState<string[]>([]);

const refresh = async () => setSources(await getPreloadedSources());

return (
<View>
<Text style={styles.hint}>
Preloaded sources are consumed when a player is created with a matching URL. Use the buttons
below to preload, inspect, and clear the cache.
</Text>
<ListButton
title="Preload Both"
onPress={async () => {
await preload(sfx1);
await preload(sfx2);
refresh();
}}
/>
<ListButton title="Query Cache" onPress={refresh} />
<Text style={styles.hint}>
{sources.length === 0 ? 'Cache is empty.' : `${sources.length} source(s) in cache:`}
</Text>
{sources.map((uri) => (
<Text key={uri} style={styles.cacheUri} numberOfLines={1}>
{uri}
</Text>
))}
<ListButton
title="Clear Cache"
onPress={async () => {
await clearAllPreloadedSources();
refresh();
}}
/>
</View>
);
}

function PreloadedPlayer() {
const player = useAudioPlayer(sfx1);
const status = useAudioPlayerStatus(player);
const [currentSource, setCurrentSource] = React.useState<1 | 2>(1);

const handleReplace = () => {
if (currentSource === 1) {
player.replace(sfx2);
setCurrentSource(2);
} else {
player.replace(sfx1);
setCurrentSource(1);
}
};

return (
<View>
<Text style={styles.hint}>Current: Sound {currentSource}</Text>
<Text style={styles.hint}>
{status.isLoaded ? `Loaded — ${Math.round(status.duration)}s` : 'Loading...'}
{status.playing ? ' — Playing' : ''}
</Text>
<View style={styles.buttonRow}>
<ListButton
title={status.playing ? 'Pause' : 'Play'}
onPress={() => {
if (status.playing) {
player.pause();
} else {
if (status.didJustFinish) player.seekTo(0);
player.play();
}
}}
/>
<ListButton
title={`Switch to Sound ${currentSource === 1 ? 2 : 1}`}
onPress={handleReplace}
/>
</View>
</View>
);
}

const styles = StyleSheet.create({
contentContainer: {
padding: 10,
},
hint: {
marginVertical: 4,
fontSize: 13,
color: '#666',
},
sfxRow: {
flexDirection: 'row',
gap: 12,
marginVertical: 12,
},
sfxButton: {
flex: 1,
backgroundColor: Colors.tintColor,
borderRadius: 12,
paddingVertical: 20,
alignItems: 'center',
justifyContent: 'center',
},
sfxButtonPressed: {
opacity: 0.7,
},
sfxButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
sfxButtonStatus: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
marginTop: 4,
},
cacheUri: {
fontSize: 11,
color: '#999',
fontFamily: 'monospace',
marginLeft: 8,
marginVertical: 2,
},
buttonRow: {
flexDirection: 'row',
gap: 8,
},
});
15 changes: 14 additions & 1 deletion apps/native-component-list/src/screens/Audio/AudioScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { preload } from 'expo-audio';

import { sfx1, sfx2 } from './AudioPreloadScreen';
import { optionalRequire } from '../../navigation/routeBuilder';
import ComponentListScreen, { apiScreensToListElements } from '../ComponentListScreen';

preload(sfx1);
preload(sfx2);

export const AudioScreens = [
{
name: 'Expo Audio Player',
Expand Down Expand Up @@ -34,7 +40,6 @@ export const AudioScreens = [
return optionalRequire(() => require('./CreateAudioPlayerScreen'));
},
},

{
name: 'Expo Audio Lock Screen Controls',
route: 'audio/expo-audio-controls',
Expand All @@ -51,6 +56,14 @@ export const AudioScreens = [
return optionalRequire(() => require('./AudioEventsScreen'));
},
},
{
name: 'Expo Audio Preloading',
route: 'audio/expo-audio-preload',
options: {},
getComponent() {
return optionalRequire(() => require('./AudioPreloadScreen'));
},
},
];

export default function AudioScreen() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@ export default function VideoAudioTracksScreen() {
handleAudioTrackChange(value);
}}>
{availableAudioTracks &&
availableAudioTracks.map((source, index) => (
<Picker.Item
key={index}
label={availableAudioTracks[index]?.label ?? 'Off'}
value={index}
/>
))}
availableAudioTracks.map((source, index) => {
let label = availableAudioTracks[index]?.label ?? 'Off';
const name = availableAudioTracks[index]?.name;
// Apple uses a weird algorithm to determine whether to add the name tag to the track label
// This way we will get the same results on Android and iOS in the picker
if (name && !label.includes(name)) {
label = `${name} - ${label}`;
}
return <Picker.Item key={index} label={label} value={index} />;
})}
</Picker>
<Text>Current audio track: {audioTrack?.label ?? availableAudioTracks[0]?.label}</Text>
</View>
Expand Down
56 changes: 0 additions & 56 deletions expo-audio/android/src/main/java/expo/modules/audio/Playable.kt

This file was deleted.

2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 💡 Others

- Force `forceNodeFilesystemAPI` when watchman is enabled (the default) but not present ([#43251](https://github.com/expo/expo/pull/43251) by [@kitten](https://github.com/kitten))

## 55.0.9 — 2026-02-16

_This version does not introduce any user-facing changes._
Expand Down
11 changes: 10 additions & 1 deletion packages/@expo/cli/src/start/server/metro/instantiateMetro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import MetroHmrServer, { Client as MetroHmrClient } from '@expo/metro/metro/HmrS
import RevisionNotFoundError from '@expo/metro/metro/IncrementalBundler/RevisionNotFoundError';
import type MetroServer from '@expo/metro/metro/Server';
import formatBundlingError from '@expo/metro/metro/lib/formatBundlingError';
import { mergeConfig, resolveConfig, type ConfigT } from '@expo/metro/metro-config';
import { InputConfigT, mergeConfig, resolveConfig, type ConfigT } from '@expo/metro/metro-config';
import { Terminal } from '@expo/metro/metro-core';
import { createStableModuleIdFactory, getDefaultConfig } from '@expo/metro-config';
import chalk from 'chalk';
Expand Down Expand Up @@ -160,6 +160,15 @@ export async function loadMetroConfigAsync(
},
};

// NOTE(@kitten): `useWatchman` is currently enabled by default, but it also disables `forceNodeFilesystemAPI`.
// If we instead set it to the special value `null`, it gets enables but also bypasses the "native find" codepath,
// which is slower than just using the Node filesystem API
// See: https://github.com/facebook/metro/blob/b9c243f/packages/metro-file-map/src/index.js#L326
// See: https://github.com/facebook/metro/blob/b9c243f/packages/metro/src/node-haste/DependencyGraph/createFileMap.js#L109
if (config.resolver.useWatchman === true) {
asWritable(config.resolver).useWatchman = null as any;
}

globalThis.__requireCycleIgnorePatterns = config.resolver?.requireCycleIgnorePatterns;

if (isExporting) {
Expand Down
2 changes: 2 additions & 0 deletions packages/@expo/fingerprint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### 🐛 Bug fixes

- Fix resolution to `expo -> @expo/cli -> @expo/env` being unstable ([#42764](https://github.com/expo/expo/pull/42764) by [@kitten](https://github.com/kitten))

### 💡 Others

## 0.16.3 — 2026-02-03
Expand Down
Loading
Loading