Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: audio recording variety bugs #2648

Merged
merged 13 commits into from
Sep 3, 2024
Merged
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
Loading