diff --git a/apps/common-app/src/demos/Record/Record.tsx b/apps/common-app/src/demos/Record/Record.tsx index 5c93ff2d1..11b45ed94 100644 --- a/apps/common-app/src/demos/Record/Record.tsx +++ b/apps/common-app/src/demos/Record/Record.tsx @@ -47,7 +47,7 @@ const Record: FC = () => { resumeIconResourceName: 'resume', color: 0xff6200, }); - } + }; const onStartRecording = useCallback(async () => { if (state !== RecordingState.Idle) { @@ -67,10 +67,20 @@ const Record: FC = () => { setHasPermissions(true); } - const success = await AudioManager.setAudioSessionActivity(true); + let success = false; + + try { + success = await AudioManager.setAudioSessionActivity(true); + } catch (error) { + console.error(error); + Alert.alert('Error', 'Failed to activate audio session for recording.'); + setState(RecordingState.Idle); + return; + } if (!success) { Alert.alert('Error', 'Failed to activate audio session for recording.'); + setState(RecordingState.Idle); return; } diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index 60358f5d4..a7124f45e 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -123,7 +123,28 @@ - (dispatch_queue_t)methodQueue resolve reject : (RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - auto success = [self.audioSessionManager setActive:enabled]; + NSError *error = nil; + + auto success = [self.audioSessionManager setActive:enabled error:&error]; + + if (!success) { + NSDictionary *meta = @{ + @"nativeCode" : @(error.code), + @"nativeDomain" : error.domain ?: @"", + @"nativeDesc" : error.description ?: @"", + }; + + NSError *jsError = + [NSError errorWithDomain:@"AudioAPIModule" + code:error.code + userInfo:@{ + NSLocalizedDescriptionKey : @"Failed to set audio session active state", + @"meta" : meta, + }]; + + reject(@"E_AUDIO_SESSION", @"Failed to set audio session active state", jsError); + return; + } resolve(@(success)); }); diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm index ebd537204..e411933db 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioEngine.mm @@ -38,7 +38,7 @@ - (void)cleanup self.sourceFormats = nil; self.inputNode = nil; - [self.sessionManager setActive:false]; + [self.sessionManager setActive:false error:nil]; self.sessionManager = nil; } @@ -182,7 +182,9 @@ - (bool)startEngine return true; } - if (![self.sessionManager setActive:true]) { + if (![self.sessionManager setActive:true error:&error]) { + // TODO: return user facing error + NSLog(@"Error while activating audio session: %@", [error debugDescription]); return false; } diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h index dda6484c7..36488f633 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.h @@ -28,7 +28,7 @@ options:(NSArray *)options allowHaptics:(BOOL)allowHaptics; -- (bool)setActive:(bool)active; +- (bool)setActive:(bool)active error:(NSError **)error; - (void)markInactive; - (void)disableSessionManagement; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm index ca8502a53..87ddb6189 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/system/AudioSessionManager.mm @@ -109,15 +109,14 @@ - (void)setAudioSessionOptions:(NSString *)categoryStr } } -- (bool)setActive:(bool)active +- (bool)setActive:(bool)active error:(NSError **)error { + bool success = false; + if (!self.shouldManageSession) { return true; } - NSError *error = nil; - bool success = false; - if (self.isActive == active) { return true; } @@ -130,18 +129,12 @@ - (bool)setActive:(bool)active } } - success = [self.audioSession setActive:active error:&error]; + success = [self.audioSession setActive:active error:error]; if (success) { self.isActive = active; } - if (error != nil) { - NSLog(@"[AudioSessionManager] setting session as %@ failed", active ? @"ACTIVE" : @"INACTIVE"); - } else { - NSLog(@"[AudioSessionManager] session is %@", active ? @"ACTIVE" : @"INACTIVE"); - } - return success; } diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index af153ee41..b7f928404 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -1,12 +1,13 @@ +import { AudioEventEmitter, AudioEventSubscription } from '../events'; +import { SystemEventCallback, SystemEventName } from '../events/types'; +import { NativeAudioAPIModule } from '../specs'; +import { parseNativeError } from './errors'; import { - SessionOptions, - PermissionStatus, AudioDevicesInfo, IAudioManager, + PermissionStatus, + SessionOptions, } from './types'; -import { SystemEventName, SystemEventCallback } from '../events/types'; -import { AudioEventEmitter, AudioEventSubscription } from '../events'; -import { NativeAudioAPIModule } from '../specs'; class AudioManager implements IAudioManager { private readonly audioEventEmitter: AudioEventEmitter; @@ -18,8 +19,15 @@ class AudioManager implements IAudioManager { return NativeAudioAPIModule.getDevicePreferredSampleRate(); } - setAudioSessionActivity(enabled: boolean): Promise { - return NativeAudioAPIModule.setAudioSessionActivity(enabled); + async setAudioSessionActivity(enabled: boolean): Promise { + try { + const success = + await NativeAudioAPIModule.setAudioSessionActivity(enabled); + + return success; + } catch (error) { + throw parseNativeError(error); + } } setAudioSessionOptions(options: SessionOptions) { diff --git a/packages/react-native-audio-api/src/system/errors.ts b/packages/react-native-audio-api/src/system/errors.ts new file mode 100644 index 000000000..b9cbd35e1 --- /dev/null +++ b/packages/react-native-audio-api/src/system/errors.ts @@ -0,0 +1,111 @@ +import { AudioApiError } from '../errors'; + +export interface NativeActivationErrorMetadata { + nativeDesc: string; + nativeCode: number; + nativeDomain: string; +} + +interface ErrorWithUserInfo { + userInfo?: { + meta?: Record; + }; +} + +interface ErrorWithNativeError { + nativeError?: { + userInfo: { + meta?: Record; + }; + }; +} + +interface ErrorWithDetails { + details?: { + meta?: Record; + }; +} + +function parseNativeCode(code: number): string { + switch (code) { + case 0: + return 'NoError'; + case -50: + return 'BadParam'; + case 1836282486: + return 'MediaServicesFailed'; + case 560030580: + return 'IsBusy'; + case 560161140: + return 'IncompatibleCategory'; + case 560557684: + return 'CannotInterruptOthers'; + case 1701737535: + return 'MissingEntitlement'; + case 1936290409: + return 'SiriIsRecording'; + case 561015905: + return 'CannotStartPlaying'; + case 561145187: + return 'CannotStartRecording'; + case 561017449: + return 'InsufficientPriority'; + case 561145203: + return 'ResourceNotAvailable'; + case 2003329396: + return 'Unspecified'; + case 561210739: + return 'ExpiredSession'; + case 1768841571: + return 'SessionNotActive'; + default: + return 'NoError'; + } +} + +export class SessionActivationError extends AudioApiError { + nativeErrorInfo?: NativeActivationErrorMetadata; + + constructor(nativeErrorInfo?: NativeActivationErrorMetadata) { + if (!nativeErrorInfo) { + super('Failed to activate audio session with unknown error'); + this.name = 'SessionActivationError'; + return; + } + + const codeName = parseNativeCode(nativeErrorInfo.nativeCode); + + super( + `[${codeName}] Failed to activate audio session, code: ${nativeErrorInfo.nativeCode}` + ); + + this.name = 'SessionActivationError'; + this.nativeErrorInfo = nativeErrorInfo; + } +} + +export function parseNativeError(error: unknown): SessionActivationError { + const errorMeta = + (error as ErrorWithUserInfo)?.userInfo?.meta ?? + (error as ErrorWithNativeError)?.nativeError?.userInfo?.meta ?? + (error as ErrorWithDetails)?.details?.meta; + + console.log('Parsed error meta:', errorMeta); + + if (!errorMeta || typeof errorMeta !== 'object') { + return new SessionActivationError(); + } + + const { nativeCode, nativeDesc, nativeDomain } = + errorMeta as unknown as NativeActivationErrorMetadata; + + if (isNaN(nativeCode) || !nativeDesc || !nativeDomain) { + return new SessionActivationError(); + } + + return new SessionActivationError({ + nativeCode, + nativeDesc, + nativeDomain, + }); +} diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index 013a8ae43..dbac67762 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -1,5 +1,5 @@ -import type { SystemEventName, SystemEventCallback } from '../events/types'; import type { AudioEventSubscription } from '../events'; +import type { SystemEventCallback, SystemEventName } from '../events/types'; export type IOSCategory = | 'record'