Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
141 changes: 89 additions & 52 deletions package/expo-package/src/handlers/Audio.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AudioComponent } from '../optionalDependencies/Video';
import { AudioComponent, RecordingObject } from '../optionalDependencies/Video';

export enum AndroidOutputFormat {
DEFAULT = 0,
Expand Down Expand Up @@ -212,56 +212,93 @@ export type RecordingOptions = {
keepAudioActiveHint?: boolean;
};

export const Audio = AudioComponent
? {
startRecording: async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => {
try {
const permissionsGranted = await AudioComponent.getPermissionsAsync().granted;
if (!permissionsGranted) {
await AudioComponent.requestPermissionsAsync();
}
await AudioComponent.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
});
const androidOptions = {
audioEncoder: AndroidAudioEncoder.AAC,
extension: '.aac',
outputFormat: AndroidOutputFormat.AAC_ADTS,
};
const iosOptions = {
audioQuality: IOSAudioQuality.HIGH,
bitRate: 128000,
extension: '.aac',
numberOfChannels: 2,
outputFormat: IOSOutputFormat.MPEG4AAC,
sampleRate: 44100,
};
const options = {
...recordingOptions,
android: androidOptions,
ios: iosOptions,
web: {},
};
const sleep = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});

const { recording } = await AudioComponent.Recording.createAsync(
options,
onRecordingStatusUpdate,
);
return { accessGranted: true, recording };
} catch (error) {
console.error('Failed to start recording', error);
return { accessGranted: false, recording: null };
}
},
stopRecording: async () => {
try {
await AudioComponent.setAudioModeAsync({
allowsRecordingIOS: false,
});
} catch (error) {
console.log('Error stopping recoding', error);
}
},
class _Audio {
recording: typeof RecordingObject | null = null;

startRecording = async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => {
try {
const permissions = await AudioComponent.getPermissionsAsync();
const permissionsStatus = permissions.status;
let permissionsGranted = permissions.granted;

// If permissions have not been determined yet, ask the user for permissions.
if (permissionsStatus === 'undetermined') {
const newPermissions = await AudioComponent.requestPermissionsAsync();
permissionsGranted = newPermissions.granted;
}

// If they are explicitly denied after this, exit early by throwing an error
// that will be caught in the catch block below (as a single source of not
// starting the player). The player would error itself anyway if we did not do
// this, but there's no reason to run the asynchronous calls when we know
// immediately that the player will not be run.
if (!permissionsGranted) {
throw new Error('Missing audio recording permission.');
}
await AudioComponent.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
});
const androidOptions = {
audioEncoder: AndroidAudioEncoder.AAC,
extension: '.aac',
outputFormat: AndroidOutputFormat.AAC_ADTS,
};
const iosOptions = {
audioQuality: IOSAudioQuality.HIGH,
bitRate: 128000,
extension: '.aac',
numberOfChannels: 2,
outputFormat: IOSOutputFormat.MPEG4AAC,
sampleRate: 44100,
};
const options = {
...recordingOptions,
android: androidOptions,
ios: iosOptions,
web: {},
};

// This is a band-aid fix for this (still unresolved) issue on Expo's side:
// https://github.com/expo/expo/issues/21782. It only occurs whenever we get
// the permissions dialog and actually select "Allow", causing the player to
// throw an error and send the wrong data downstream. So, if the original
// permissions.status is 'undetermined', meaning we got to here by allowing
// permissions - we sleep for 500ms before proceeding. Any subsequent calls
// to startRecording() will not invoke the sleep.
if (permissionsStatus === 'undetermined') {
await sleep(500);
}

const { recording } = await AudioComponent.Recording.createAsync(
options,
onRecordingStatusUpdate,
);
this.recording = recording;
return { accessGranted: true, recording };
} catch (error) {
console.error('Failed to start recording', error);
return { accessGranted: false, recording: null };
}
};
stopRecording = async () => {
try {
await this.recording.stopAndUnloadAsync();
await AudioComponent.setAudioModeAsync({
allowsRecordingIOS: false,
});
this.recording = null;
} catch (error) {
console.log('Error stopping recoding', error);
}
: null;
};
}

export const Audio = AudioComponent ? new _Audio() : null;
4 changes: 3 additions & 1 deletion package/expo-package/src/optionalDependencies/Video.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
let VideoComponent;
let AudioComponent;
let RecordingObject;
try {
const audioVideoPackage = require('expo-av');
VideoComponent = audioVideoPackage.Video;
AudioComponent = audioVideoPackage.Audio;
RecordingObject = audioVideoPackage.RecordingObject;
} catch (e) {
// do nothing
}
Expand All @@ -14,4 +16,4 @@ if (!VideoComponent || !AudioComponent) {
);
}

export { AudioComponent, VideoComponent };
export { AudioComponent, VideoComponent, RecordingObject };
2 changes: 2 additions & 0 deletions package/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export const setNetInfoFetchMock = (fn) => {
registerNativeHandlers({
Audio: {
startPlayer: jest.fn(),
startRecording: jest.fn(() => ({ accessGranted: true, recording: 'some-recording-path' })),
stopPlayer: jest.fn(),
stopRecording: jest.fn(),
},
compressImage: () => null,
deleteFile: () => null,
Expand Down
149 changes: 80 additions & 69 deletions package/native-package/src/optionalDependencies/Audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,74 +168,85 @@ const verifyAndroidPermissions = async () => {
return true;
};

export const Audio = AudioRecorderPackage
? {
pausePlayer: async () => {
await audioRecorderPlayer.pausePlayer();
},
resumePlayer: async () => {
await audioRecorderPlayer.resumePlayer();
},
startPlayer: async (uri, _, onPlaybackStatusUpdate) => {
try {
const playback = await audioRecorderPlayer.startPlayer(uri);
console.log({ playback });
audioRecorderPlayer.addPlayBackListener((status) => {
onPlaybackStatusUpdate(status);
});
} catch (error) {
console.log('Error starting player', error);
}
},
startRecording: async (options: RecordingOptions, onRecordingStatusUpdate) => {
if (Platform.OS === 'android') {
try {
await verifyAndroidPermissions();
} catch (err) {
console.warn('Audio Recording Permissions error', err);
return;
}
}
try {
const path = Platform.select({
android: `${RNFS.CachesDirectoryPath}/sound.aac`,
ios: 'sound.aac',
});
const audioSet = {
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AudioSourceAndroid: AudioSourceAndroidType.MIC,
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
AVFormatIDKeyIOS: AVEncodingOption.aac,
AVModeIOS: AVModeIOSOption.measurement,
AVNumberOfChannelsKeyIOS: 2,
OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS,
};
const recording = await audioRecorderPlayer.startRecorder(
path,
audioSet,
options?.isMeteringEnabled,
);
class _Audio {
pausePlayer = async () => {
await audioRecorderPlayer.pausePlayer();
};
resumePlayer = async () => {
await audioRecorderPlayer.resumePlayer();
};
startPlayer = async (uri, _, onPlaybackStatusUpdate) => {
try {
const playback = await audioRecorderPlayer.startPlayer(uri);
console.log({ playback });
audioRecorderPlayer.addPlayBackListener((status) => {
onPlaybackStatusUpdate(status);
});
} catch (error) {
console.log('Error starting player', error);
}
};
startRecording = async (options: RecordingOptions, onRecordingStatusUpdate) => {
if (Platform.OS === 'android') {
try {
await verifyAndroidPermissions();
} catch (err) {
console.warn('Audio Recording Permissions error', err);
return;
}
}
try {
const path = Platform.select({
android: `${RNFS.CachesDirectoryPath}/sound.aac`,
ios: 'sound.aac',
});
const audioSet = {
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AudioSourceAndroid: AudioSourceAndroidType.MIC,
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
AVFormatIDKeyIOS: AVEncodingOption.aac,
AVModeIOS: AVModeIOSOption.measurement,
AVNumberOfChannelsKeyIOS: 2,
OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS,
};
const recording = await audioRecorderPlayer.startRecorder(
path,
audioSet,
options?.isMeteringEnabled,
);

audioRecorderPlayer.addRecordBackListener((status) => {
onRecordingStatusUpdate(status);
});
return { accessGranted: true, recording };
} catch (error) {
console.error('Failed to start recording', error);
return { accessGranted: false, recording: null };
}
},
stopPlayer: async () => {
try {
await audioRecorderPlayer.stopPlayer();
audioRecorderPlayer.removePlayBackListener();
} catch (error) {
console.log(error);
}
},
stopRecording: async () => {
await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
},
audioRecorderPlayer.addRecordBackListener((status) => {
onRecordingStatusUpdate(status);
});
return { accessGranted: true, recording };
} catch (error) {
console.error('Failed to start recording', error);
// There is currently a bug in react-native-audio-recorder-player and we
// need to do this until it gets fixed. More information can be found here:
// https://github.com/hyochan/react-native-audio-recorder-player/pull/625
// eslint-disable-next-line no-underscore-dangle
audioRecorderPlayer._isRecording = false;
// eslint-disable-next-line no-underscore-dangle
audioRecorderPlayer._hasPausedRecord = false;
return { accessGranted: false, recording: null };
}
: null;
};
stopPlayer = async () => {
try {
await audioRecorderPlayer.stopPlayer();
audioRecorderPlayer.removePlayBackListener();
} catch (error) {
console.log(error);
}
};
stopRecording = async () => {
try {
await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
} catch (error) {
console.log(error);
}
};
}

export const Audio = AudioRecorderPackage ? new _Audio() : null;
Loading