Mobile: Fixes #14804: Migrate expo-av to expo-audio (#14847)

This commit is contained in:
Victor Gherardi
2026-04-08 15:21:47 +02:00
committed by GitHub
parent 04d0626ede
commit e794176171
6 changed files with 116 additions and 88 deletions
@@ -2,7 +2,7 @@ import * as React from 'react';
import { PrimaryButton, SecondaryButton } from '../buttons'; import { PrimaryButton, SecondaryButton } from '../buttons';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Audio, InterruptionModeIOS } from 'expo-av'; import { AudioQuality, getRecordingPermissionsAsync, IOSOutputFormat, requestRecordingPermissionsAsync, setAudioModeAsync, type RecordingOptions, useAudioRecorder as useExpoAudioRecorder, useAudioRecorderState } from 'expo-audio';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import { OnFileSavedCallback, RecorderState } from './types'; import { OnFileSavedCallback, RecorderState } from './types';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
@@ -11,7 +11,6 @@ import FsDriverWeb from '../../utils/fs-driver/fs-driver-rn.web';
import uuid from '@joplin/lib/uuid'; import uuid from '@joplin/lib/uuid';
import RecordingControls from './RecordingControls'; import RecordingControls from './RecordingControls';
import { Text } from 'react-native-paper'; import { Text } from 'react-native-paper';
import { AndroidAudioEncoder, AndroidOutputFormat, IOSAudioQuality, IOSOutputFormat, RecordingOptions } from 'expo-av/build/Audio';
import time from '@joplin/lib/time'; import time from '@joplin/lib/time';
import { toFileExtension } from '@joplin/lib/mime-utils'; import { toFileExtension } from '@joplin/lib/mime-utils';
import { formatMsToDurationCompat, msleep } from '@joplin/utils/time'; import { formatMsToDurationCompat, msleep } from '@joplin/utils/time';
@@ -25,23 +24,21 @@ interface Props {
// Modified from the Expo default recording options to create // Modified from the Expo default recording options to create
// .m4a recordings on both Android and iOS (rather than .3gp on Android). // .m4a recordings on both Android and iOS (rather than .3gp on Android).
const recordingOptions = (): RecordingOptions => ({ const recordingOptions: RecordingOptions = {
extension: '.m4a',
isMeteringEnabled: true, isMeteringEnabled: true,
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 64000,
android: { android: {
extension: '.m4a', extension: '.m4a',
outputFormat: AndroidOutputFormat.MPEG_4, outputFormat: 'mpeg4',
audioEncoder: AndroidAudioEncoder.AAC, audioEncoder: 'aac',
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 64000,
}, },
ios: { ios: {
extension: '.m4a', extension: '.m4a',
audioQuality: IOSAudioQuality.MIN, audioQuality: AudioQuality.MIN,
outputFormat: IOSOutputFormat.MPEG4AAC, outputFormat: IOSOutputFormat.MPEG4AAC,
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 64000,
linearPCMBitDepth: 16, linearPCMBitDepth: 16,
linearPCMIsBigEndian: false, linearPCMIsBigEndian: false,
linearPCMIsFloat: false, linearPCMIsFloat: false,
@@ -56,14 +53,16 @@ const recordingOptions = (): RecordingOptions => ({
].find(type => MediaRecorder.isTypeSupported(type)) ?? 'audio/webm', ].find(type => MediaRecorder.isTypeSupported(type)) ?? 'audio/webm',
bitsPerSecond: 128000, bitsPerSecond: 128000,
} : {}, } : {},
}); };
const getRecordingFileName = (extension: string) => { const getRecordingFileName = (extension: string) => {
return `recording-${time.formatDateToLocal(new Date())}${extension}`; return `recording-${time.formatDateToLocal(new Date())}${extension}`;
}; };
const recordingToSaveData = async (recording: Audio.Recording) => { const recordingToSaveData = async (recordingUri: string|null) => {
let uri = recording.getURI(); if (!recordingUri) throw new Error(_('Unable to access the recording file.'));
let uri = recordingUri;
let type: string|undefined; let type: string|undefined;
let fileName; let fileName;
@@ -73,7 +72,7 @@ const recordingToSaveData = async (recording: Audio.Recording) => {
const fetchResult = await fetch(uri); const fetchResult = await fetch(uri);
const blob = await fetchResult.blob(); const blob = await fetchResult.blob();
type = recordingOptions().web.mimeType; type = recordingOptions.web.mimeType;
const extension = `.${toFileExtension(type)}`; const extension = `.${toFileExtension(type)}`;
fileName = getRecordingFileName(extension); fileName = getRecordingFileName(extension);
const file = new File([blob], fileName); const file = new File([blob], fileName);
@@ -82,10 +81,9 @@ const recordingToSaveData = async (recording: Audio.Recording) => {
await (shim.fsDriver() as FsDriverWeb).createReadOnlyVirtualFile(path, file); await (shim.fsDriver() as FsDriverWeb).createReadOnlyVirtualFile(path, file);
uri = path; uri = path;
} else { } else {
const options = recordingOptions();
const extension = Platform.select({ const extension = Platform.select({
android: options.android.extension, android: recordingOptions.android.extension,
ios: options.ios.extension, ios: recordingOptions.ios.extension,
default: '', default: '',
}); });
fileName = getRecordingFileName(extension); fileName = getRecordingFileName(extension);
@@ -95,75 +93,72 @@ const recordingToSaveData = async (recording: Audio.Recording) => {
}; };
const resetAudioMode = async () => { const resetAudioMode = async () => {
await Audio.setAudioModeAsync({ await setAudioModeAsync({
// When enabled, iOS may use the small (phone call) speaker allowsRecording: false,
// instead of the default one, so it's disabled when not recording: allowsBackgroundRecording: false,
allowsRecordingIOS: false, playsInSilentMode: false,
playsInSilentModeIOS: false, shouldPlayInBackground: false,
staysActiveInBackground: false,
}); });
}; };
const useAudioRecorder = (onFileSaved: OnFileSavedCallback, onDismiss: ()=> void) => { const useAudioRecorder = (onFileSaved: OnFileSavedCallback, onDismiss: ()=> void) => {
const [permissionResponse, requestPermissions] = Audio.usePermissions();
const [recordingState, setRecordingState] = useState<RecorderState>(RecorderState.Idle); const [recordingState, setRecordingState] = useState<RecorderState>(RecorderState.Idle);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [duration, setDuration] = useState(0); const recorder = useExpoAudioRecorder(recordingOptions);
const recorderStatus = useAudioRecorderState(recorder, 100);
const isRecordingRef = useRef(false);
const recordingRef = useRef<Audio.Recording|null>(null);
const onStartRecording = useCallback(async () => { const onStartRecording = useCallback(async () => {
try { try {
setRecordingState(RecorderState.Loading); setRecordingState(RecorderState.Loading);
setError('');
if (permissionResponse?.status !== 'granted') { const permissionResponse = await getRecordingPermissionsAsync();
const response = await requestPermissions(); if (permissionResponse.status !== 'granted') {
const response = await requestRecordingPermissionsAsync();
if (!response.granted) { if (!response.granted) {
throw new Error(_('Missing permission to record audio.')); throw new Error(_('Missing permission to record audio.'));
} }
// Work around "This experience is currently in the background, so the audio session could not be activated" // Work around "This experience is currently in the background, so the audio session could not be activated"
// See https://github.com/expo/expo/issues/21782 // See https://github.com/expo/expo/issues/21782
// May be resolved by migrating to expo-audio.
await msleep(500); await msleep(500);
} }
await Audio.setAudioModeAsync({ await setAudioModeAsync({
allowsRecordingIOS: true, allowsRecording: true,
playsInSilentModeIOS: true, allowsBackgroundRecording: true,
staysActiveInBackground: true, playsInSilentMode: true,
shouldPlayInBackground: true,
// Fixes an issue where opening a recording in the iOS audio player // Fixes an issue where opening a recording in the iOS audio player
// breaks creating new recordings. // breaks creating new recordings.
// See https://github.com/expo/expo/issues/31152#issuecomment-2341811087 // See https://github.com/expo/expo/issues/31152#issuecomment-2341811087
interruptionModeIOS: InterruptionModeIOS.DoNotMix, interruptionMode: 'doNotMix',
}); });
await recorder.prepareToRecordAsync();
isRecordingRef.current = true;
recorder.record();
setRecordingState(RecorderState.Recording); setRecordingState(RecorderState.Recording);
const recording = new Audio.Recording();
await recording.prepareToRecordAsync(recordingOptions());
recording.setOnRecordingStatusUpdate(status => {
setDuration(status.durationMillis);
});
recordingRef.current = recording;
await recording.startAsync();
} catch (error) { } catch (error) {
logger.error('Error starting recording:', error); logger.error('Error starting recording:', error);
setError(`Recording error: ${error}`); setError(`Recording error: ${error}`);
setRecordingState(RecorderState.Error); setRecordingState(RecorderState.Error);
void recordingRef.current?.stopAndUnloadAsync(); if (isRecordingRef.current) {
recordingRef.current = null; isRecordingRef.current = false;
void recorder.stop();
}
} }
}, [permissionResponse, requestPermissions]); }, [recorder]);
const onStopRecording = useCallback(async () => { const onStopRecording = useCallback(async () => {
const recording = recordingRef.current;
recordingRef.current = null;
try { try {
setRecordingState(RecorderState.Processing); setRecordingState(RecorderState.Processing);
await recording.stopAndUnloadAsync(); await recorder.stop();
isRecordingRef.current = false;
await resetAudioMode(); await resetAudioMode();
const saveEvent = await recordingToSaveData(recording); const saveEvent = await recordingToSaveData(recorder.uri);
onFileSaved(saveEvent); onFileSaved(saveEvent);
onDismiss(); onDismiss();
} catch (error) { } catch (error) {
@@ -171,25 +166,35 @@ const useAudioRecorder = (onFileSaved: OnFileSavedCallback, onDismiss: ()=> void
setError(`Save error: ${error}`); setError(`Save error: ${error}`);
setRecordingState(RecorderState.Error); setRecordingState(RecorderState.Error);
} }
}, [onFileSaved, onDismiss]); }, [onFileSaved, onDismiss, recorder]);
const onStartStopRecording = useCallback(async () => { const onStartStopRecording = useCallback(async () => {
if (recordingState === RecorderState.Idle) { if (recordingState === RecorderState.Idle) {
await onStartRecording(); await onStartRecording();
} else if (recordingState === RecorderState.Recording && recordingRef.current) { } else if (recordingState === RecorderState.Recording) {
await onStopRecording(); await onStopRecording();
} }
}, [recordingState, onStartRecording, onStopRecording]); }, [recordingState, onStartRecording, onStopRecording]);
useEffect(() => () => { useEffect(() => () => {
if (recordingRef.current) { if (isRecordingRef.current) {
void recordingRef.current?.stopAndUnloadAsync(); isRecordingRef.current = false;
recordingRef.current = null;
void resetAudioMode();
}
}, []);
return { onStartStopRecording, error, duration, recordingState }; const stopRecorderOnCleanup = async () => {
try {
await recorder.stop();
} catch (error) {
logger.warn('Error stopping recorder during cleanup:', error);
}
await resetAudioMode();
};
void stopRecorderOnCleanup();
}
}, [recorder]);
return { onStartStopRecording, error, duration: recorderStatus.durationMillis, recordingState };
}; };
const AudioRecordingBanner: React.FC<Props> = props => { const AudioRecordingBanner: React.FC<Props> = props => {
+14 -15
View File
@@ -1,7 +1,4 @@
PODS: PODS:
- EXAV (16.0.8):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (18.0.13): - EXConstants (18.0.13):
- ExpoModulesCore - ExpoModulesCore
- EXImageLoader (6.0.0): - EXImageLoader (6.0.0):
@@ -34,6 +31,8 @@ PODS:
- Yoga - Yoga
- ExpoAsset (12.0.12): - ExpoAsset (12.0.12):
- ExpoModulesCore - ExpoModulesCore
- ExpoAudio (1.1.1):
- ExpoModulesCore
- ExpoCamera (17.0.10): - ExpoCamera (17.0.10):
- ExpoModulesCore - ExpoModulesCore
- ZXingObjC/OneD - ZXingObjC/OneD
@@ -72,9 +71,9 @@ PODS:
- ReactNativeDependencies - ReactNativeDependencies
- Yoga - Yoga
- FBLazyVector (0.81.6) - FBLazyVector (0.81.6)
- hermes-engine (0.81.5): - hermes-engine (0.81.6):
- hermes-engine/Pre-built (= 0.81.5) - hermes-engine/Pre-built (= 0.81.6)
- hermes-engine/Pre-built (0.81.5) - hermes-engine/Pre-built (0.81.6)
- JoplinCommonShareExtension (1.0.0) - JoplinCommonShareExtension (1.0.0)
- JoplinRNShareExtension (1.0.0): - JoplinRNShareExtension (1.0.0):
- JoplinCommonShareExtension - JoplinCommonShareExtension
@@ -154,7 +153,7 @@ PODS:
- React-utils - React-utils
- ReactNativeDependencies - ReactNativeDependencies
- Yoga - Yoga
- React-Core-prebuilt (0.81.5): - React-Core-prebuilt (0.81.6):
- ReactNativeDependencies - ReactNativeDependencies
- React-Core/CoreModulesHeaders (0.81.6): - React-Core/CoreModulesHeaders (0.81.6):
- hermes-engine - hermes-engine
@@ -2134,7 +2133,7 @@ PODS:
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- ReactNativeDependencies - ReactNativeDependencies
- Yoga - Yoga
- SDWebImage/Core (5.21.5) - SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0): - SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
@@ -2169,11 +2168,11 @@ PODS:
- ZXingObjC/Core - ZXingObjC/Core
DEPENDENCIES: DEPENDENCIES:
- EXAV (from `../node_modules/expo-av/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`) - EXConstants (from `../node_modules/expo-constants/ios`)
- EXImageLoader (from `../node_modules/expo-image-loader/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`)
- Expo (from `../node_modules/expo`) - Expo (from `../node_modules/expo`)
- ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoAudio (from `../node_modules/expo-audio/ios`)
- ExpoCamera (from `../node_modules/expo-camera/ios`) - ExpoCamera (from `../node_modules/expo-camera/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`) - ExpoFont (from `../node_modules/expo-font/ios`)
@@ -2293,8 +2292,6 @@ SPEC REPOS:
- ZXingObjC - ZXingObjC
EXTERNAL SOURCES: EXTERNAL SOURCES:
EXAV:
:path: "../node_modules/expo-av/ios"
EXConstants: EXConstants:
:path: "../node_modules/expo-constants/ios" :path: "../node_modules/expo-constants/ios"
EXImageLoader: EXImageLoader:
@@ -2303,6 +2300,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo" :path: "../node_modules/expo"
ExpoAsset: ExpoAsset:
:path: "../node_modules/expo-asset/ios" :path: "../node_modules/expo-asset/ios"
ExpoAudio:
:path: "../node_modules/expo-audio/ios"
ExpoCamera: ExpoCamera:
:path: "../node_modules/expo-camera/ios" :path: "../node_modules/expo-camera/ios"
ExpoFileSystem: ExpoFileSystem:
@@ -2522,11 +2521,11 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga" :path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS: SPEC CHECKSUMS:
EXAV: b60fcf142fae6684d295bc28cd7cfcb3335570ea
EXConstants: fce59a631a06c4151602843667f7cfe35f81e271 EXConstants: fce59a631a06c4151602843667f7cfe35f81e271
EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05 EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05
Expo: 04993fbd7b06dc98ffac58da8847298470dc3db1 Expo: 04993fbd7b06dc98ffac58da8847298470dc3db1
ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18 ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18
ExpoAudio: e4cfe3a2f3317b8487460685385a9867a07fb4fb
ExpoCamera: 6a326deb45ba840749652e4c15198317aa78497e ExpoCamera: 6a326deb45ba840749652e4c15198317aa78497e
ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063 ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063
ExpoFont: 35ac6191ed86bbf56b3ebd2d9154eda9fad5b509 ExpoFont: 35ac6191ed86bbf56b3ebd2d9154eda9fad5b509
@@ -2534,7 +2533,7 @@ SPEC CHECKSUMS:
ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6 ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6
ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583 ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583
FBLazyVector: 14ce6e3675cacb2683ad30272f04274a4ee5b67d FBLazyVector: 14ce6e3675cacb2683ad30272f04274a4ee5b67d
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 hermes-engine: 7219f6e751ad6ec7f3d7ec121830ee34dae40749
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7 JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860 JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
@@ -2546,7 +2545,7 @@ SPEC CHECKSUMS:
React: 348d1689d8686d034c5b7667dc45de86c6319dd1 React: 348d1689d8686d034c5b7667dc45de86c6319dd1
React-callinvoker: 2c3b664f3482f5bc5560ea1edcbbe69748752f08 React-callinvoker: 2c3b664f3482f5bc5560ea1edcbbe69748752f08
React-Core: 346787852200a732b187805344b8a350d464e004 React-Core: 346787852200a732b187805344b8a350d464e004
React-Core-prebuilt: 02f0ad625ddd47463c009c2d0c5dd35c0d982599 React-Core-prebuilt: 721ab014acfaff1e4b8fc0d2f7d6f41ea9a706ed
React-CoreModules: 7e07391a1082d02c37f846a362f7574ab035933c React-CoreModules: 7e07391a1082d02c37f846a362f7574ab035933c
React-cxxreact: c50d278c785792a077a6b357aaabd9e5d09e9c6f React-cxxreact: c50d278c785792a077a6b357aaabd9e5d09e9c6f
React-debug: 1b91785fec02ea76c793ead23bed1528d96b4262 React-debug: 1b91785fec02ea76c793ead23bed1528d96b4262
@@ -2635,7 +2634,7 @@ SPEC CHECKSUMS:
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 0e600372fb35783fe30d413efd28d11de2bf6cf0 RNShare: 0e600372fb35783fe30d413efd28d11de2bf6cf0
RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522 RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
WhisperVoiceTyping: 343ea840cbde2a5f3508f8b016ebcf1c089179ea WhisperVoiceTyping: 343ea840cbde2a5f3508f8b016ebcf1c089179ea
Yoga: 786fa7d9d2ff6060b4e688062243fa69c323d140 Yoga: 786fa7d9d2ff6060b4e688062243fa69c323d140
+29 -2
View File
@@ -76,14 +76,41 @@ jest.mock('@react-native-clipboard/clipboard', () => {
return { default: { getString: jest.fn(), setString: jest.fn() } }; return { default: { getString: jest.fn(), setString: jest.fn() } };
}); });
jest.doMock('expo-audio', () => {
return {
AudioQuality: {
MIN: 'min',
},
IOSOutputFormat: {
MPEG4AAC: 'mpeg4aac',
},
getRecordingPermissionsAsync: jest.fn(async () => ({
status: 'granted',
granted: true,
})),
requestRecordingPermissionsAsync: jest.fn(async () => ({
status: 'granted',
granted: true,
})),
setAudioModeAsync: jest.fn(async () => null),
useAudioRecorder: jest.fn(() => ({
prepareToRecordAsync: jest.fn(async () => null),
record: jest.fn(),
stop: jest.fn(async () => null),
uri: null,
})),
useAudioRecorderState: jest.fn(() => ({
durationMillis: 0,
})),
};
});
const emptyMockPackages = [ const emptyMockPackages = [
'react-native-share', 'react-native-share',
'react-native-file-viewer', 'react-native-file-viewer',
'react-native-image-picker', 'react-native-image-picker',
'@react-native-documents/picker', '@react-native-documents/picker',
'@joplin/react-native-saf-x', '@joplin/react-native-saf-x',
'expo-av',
'expo-av/build/Audio',
'expo-image-manipulator', 'expo-image-manipulator',
]; ];
for (const packageName of emptyMockPackages) { for (const packageName of emptyMockPackages) {
+1 -1
View File
@@ -49,7 +49,7 @@
"deprecated-react-native-prop-types": "5.0.0", "deprecated-react-native-prop-types": "5.0.0",
"events": "3.3.0", "events": "3.3.0",
"expo": "54.0.31", "expo": "54.0.31",
"expo-av": "16.0.8", "expo-audio": "1.1.1",
"expo-camera": "17.0.10", "expo-camera": "17.0.10",
"expo-image-manipulator": "14.0.8", "expo-image-manipulator": "14.0.8",
"expo-local-authentication": "17.0.8", "expo-local-authentication": "17.0.8",
+1 -1
View File
@@ -88,7 +88,7 @@
"browserify", "browserify",
"codemirror", "codemirror",
"cspell", "cspell",
"expo-av", // Must be updated with expo "expo-audio", // Must be updated with expo
"file-loader", "file-loader",
"gradle", "gradle",
"html-webpack-plugin", "html-webpack-plugin",
+6 -9
View File
@@ -10827,7 +10827,7 @@ __metadata:
esbuild: "npm:0.27.2" esbuild: "npm:0.27.2"
events: "npm:3.3.0" events: "npm:3.3.0"
expo: "npm:54.0.31" expo: "npm:54.0.31"
expo-av: "npm:16.0.8" expo-audio: "npm:1.1.1"
expo-camera: "npm:17.0.10" expo-camera: "npm:17.0.10"
expo-image-manipulator: "npm:14.0.8" expo-image-manipulator: "npm:14.0.8"
expo-local-authentication: "npm:17.0.8" expo-local-authentication: "npm:17.0.8"
@@ -28797,18 +28797,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"expo-av@npm:16.0.8": "expo-audio@npm:1.1.1":
version: 16.0.8 version: 1.1.1
resolution: "expo-av@npm:16.0.8" resolution: "expo-audio@npm:1.1.1"
peerDependencies: peerDependencies:
expo: "*" expo: "*"
expo-asset: "*"
react: "*" react: "*"
react-native: "*" react-native: "*"
react-native-web: "*" checksum: 10/b2013e196eb56d6b31f62d82e3918d81626b8d48bd3b9c3b50d8d97b8329de8ec1cb700cb61cc03b637371213aa2de78fec10208e742e8e2a2019dbf25777814
peerDependenciesMeta:
react-native-web:
optional: true
checksum: 10/c274ae6b98e30b673d2dd03119da703a379c01367466022fa1d2dc1b17ce9475711488132d2c7271a88ecda3800c5d03697371761fa8330e40ca0c02f9b951fa
languageName: node languageName: node
linkType: hard linkType: hard