1// Copyright 2017-present 650 Industries. All rights reserved. 2 3#import <AVFoundation/AVFoundation.h> 4 5#import <ExpoModulesCore/EXUIManager.h> 6#import <ExpoModulesCore/EXEventEmitterService.h> 7#import <ExpoModulesCore/EXAppLifecycleService.h> 8#import <ExpoModulesCore/EXFileSystemInterface.h> 9#import <ExpoModulesCore/EXPermissionsInterface.h> 10#import <ExpoModulesCore/EXPermissionsMethodsDelegate.h> 11#import <ExpoModulesCore/EXJavaScriptContextProvider.h> 12 13#import <EXAV/EXAV.h> 14#import <EXAV/EXAVPlayerData.h> 15#import <EXAV/EXVideoView.h> 16#import <EXAV/EXAudioRecordingPermissionRequester.h> 17#import <EXAV/EXAV+AudioSampleCallback.h> 18 19NSString *const EXAudioRecordingOptionsIsMeteringEnabledKey = @"isMeteringEnabled"; 20NSString *const EXAudioRecordingOptionsKeepAudioActiveHintKey = @"keepAudioActiveHint"; 21NSString *const EXAudioRecordingOptionsKey = @"ios"; 22NSString *const EXAudioRecordingOptionExtensionKey = @"extension"; 23NSString *const EXAudioRecordingOptionOutputFormatKey = @"outputFormat"; 24NSString *const EXAudioRecordingOptionAudioQualityKey = @"audioQuality"; 25NSString *const EXAudioRecordingOptionSampleRateKey = @"sampleRate"; 26NSString *const EXAudioRecordingOptionNumberOfChannelsKey = @"numberOfChannels"; 27NSString *const EXAudioRecordingOptionBitRateKey = @"bitRate"; 28NSString *const EXAudioRecordingOptionBitRateStrategyKey = @"bitRateStrategy"; 29NSString *const EXAudioRecordingOptionBitDepthHintKey = @"bitDepthHint"; 30NSString *const EXAudioRecordingOptionLinearPCMBitDepthKey = @"linearPCMBitDepth"; 31NSString *const EXAudioRecordingOptionLinearPCMIsBigEndianKey = @"linearPCMIsBigEndian"; 32NSString *const EXAudioRecordingOptionLinearPCMIsFloatKey = @"linearPCMIsFloat"; 33 34NSString *const EXDidUpdatePlaybackStatusEventName = @"didUpdatePlaybackStatus"; 35 36NSString *const EXDidUpdateMetadataEventName = @"didUpdateMetadata"; 37 38@interface EXAV () 39 40@property (nonatomic, weak) RCTBridge *bridge; 41 42@property (nonatomic, weak) id kernelAudioSessionManagerDelegate; 43@property (nonatomic, weak) id kernelPermissionsServiceDelegate; 44 45@property (nonatomic, assign) BOOL audioIsEnabled; 46@property (nonatomic, assign) EXAVAudioSessionMode currentAudioSessionMode; 47@property (nonatomic, assign) BOOL isBackgrounded; 48 49@property (nonatomic, assign) EXAudioInterruptionMode audioInterruptionMode; 50@property (nonatomic, assign) BOOL playsInSilentMode; 51@property (nonatomic, assign) BOOL allowsAudioRecording; 52@property (nonatomic, assign) BOOL staysActiveInBackground; 53 54@property (nonatomic, assign) int soundDictionaryKeyCount; 55@property (nonatomic, strong) NSMutableDictionary <NSNumber *, EXAVPlayerData *> *soundDictionary; 56@property (nonatomic, assign) BOOL isBeingObserved; 57@property (nonatomic, strong) NSHashTable <NSObject<EXAVObject> *> *videoSet; 58 59@property (nonatomic, strong) NSString *audioRecorderFilename; 60@property (nonatomic, strong) NSDictionary *audioRecorderSettings; 61@property (nonatomic, strong) AVAudioRecorder *audioRecorder; 62@property (nonatomic, assign) BOOL audioRecorderIsPreparing; 63@property (nonatomic, assign) BOOL audioRecorderShouldBeginRecording; 64 65// Media services may reset if the active recording input is no longer available 66// during a recording session (i.e. airpods run out of batteries). We expose this property 67// to allow the client decide what to do in this case—to prompt the user to select another input 68// or tear down the recording session. 69@property (nonatomic, assign) BOOL mediaServicesDidReset; 70 71@property (nonatomic, assign) int audioRecorderDurationMillis; 72@property (nonatomic, assign) int prevAudioRecorderDurationMillis; 73@property (nonatomic, assign) int audioRecorderStartTimestamp; 74 75@property (nonatomic, weak) EXModuleRegistry *expoModuleRegistry; 76@property (nonatomic, weak) id<EXPermissionsInterface> permissionsManager; 77 78@end 79 80@implementation EXAV 81 82EX_EXPORT_MODULE(ExponentAV); 83 84- (instancetype)init 85{ 86 if (self = [super init]) { 87 _audioIsEnabled = YES; 88 _currentAudioSessionMode = EXAVAudioSessionModeInactive; 89 _isBackgrounded = NO; 90 91 _audioInterruptionMode = EXAudioInterruptionModeMixWithOthers; 92 _playsInSilentMode = false; 93 _allowsAudioRecording = false; 94 _staysActiveInBackground = false; 95 96 _soundDictionaryKeyCount = 0; 97 _soundDictionary = [NSMutableDictionary new]; 98 _isBeingObserved = NO; 99 _videoSet = [NSHashTable weakObjectsHashTable]; 100 101 _audioRecorderFilename = nil; 102 _audioRecorderSettings = nil; 103 _audioRecorder = nil; 104 _audioRecorderIsPreparing = false; 105 _audioRecorderShouldBeginRecording = false; 106 _audioRecorderDurationMillis = 0; 107 _prevAudioRecorderDurationMillis = 0; 108 _audioRecorderStartTimestamp = 0; 109 _mediaServicesDidReset = false; 110 } 111 return self; 112} 113 114+ (const NSArray<Protocol *> *)exportedInterfaces 115{ 116 return @[@protocol(EXAVInterface)]; 117} 118 119- (void)installJsiBindings 120{ 121 id<EXJavaScriptContextProvider> jsContextProvider = [_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXJavaScriptContextProvider)]; 122 void *jsRuntimePtr = [jsContextProvider javaScriptRuntimePointer]; 123 if (jsRuntimePtr) { 124 [self installJSIBindingsForRuntime:jsRuntimePtr withSoundDictionary:_soundDictionary]; 125 } 126} 127 128- (NSDictionary *)constantsToExport 129{ 130 // install JSI bindings here because `constantsToExport` is called when the JS runtime has been created 131 [self installJsiBindings]; 132 133 return @{ 134 @"Qualities": @{ 135 @"Low": AVAudioTimePitchAlgorithmLowQualityZeroLatency, 136 @"Medium": AVAudioTimePitchAlgorithmTimeDomain, 137 @"High": AVAudioTimePitchAlgorithmSpectral 138 } 139 }; 140} 141 142#pragma mark - Expo experience lifecycle 143 144- (void)setModuleRegistry:(EXModuleRegistry *)expoModuleRegistry 145{ 146 [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] unregisterAppLifecycleListener:self]; 147 _expoModuleRegistry = expoModuleRegistry; 148 _kernelAudioSessionManagerDelegate = [_expoModuleRegistry getSingletonModuleForName:@"AudioSessionManager"]; 149 if (!_isBackgrounded) { 150 [_kernelAudioSessionManagerDelegate moduleDidForeground:self]; 151 } 152 [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] registerAppLifecycleListener:self]; 153 _permissionsManager = [_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)]; 154 [EXPermissionsMethodsDelegate registerRequesters:@[[EXAudioRecordingPermissionRequester new]] withPermissionsManager:_permissionsManager]; 155} 156 157- (void)onAppForegrounded 158{ 159 [_kernelAudioSessionManagerDelegate moduleDidForeground:self]; 160 _isBackgrounded = NO; 161 162 [self _runBlockForAllAVObjects:^(NSObject<EXAVObject> *exAVObject) { 163 [exAVObject appDidForeground]; 164 }]; 165} 166 167- (void)onAppBackgrounded 168{ 169 _isBackgrounded = YES; 170 if (!_staysActiveInBackground) { 171 [self _deactivateAudioSession]; // This will pause all players and stop all recordings 172 173 [self _runBlockForAllAVObjects:^(NSObject<EXAVObject> *exAVObject) { 174 [exAVObject appDidBackgroundStayActive:NO]; 175 }]; 176 [_kernelAudioSessionManagerDelegate moduleDidBackground:self]; 177 } else { 178 [self _runBlockForAllAVObjects:^(NSObject<EXAVObject> *exAVObject) { 179 [exAVObject appDidBackgroundStayActive:YES]; 180 }]; 181 } 182} 183 184- (void)onAppContentWillReload 185{ 186 // We need to clear audio tap before sound gets destroyed to avoid 187 // using pointer to deallocated EXAVPlayerData in MTAudioTap process callback 188 for (NSNumber *key in [_soundDictionary allKeys]) { 189 [self _removeAudioCallbackForKey:key]; 190 } 191} 192 193#pragma mark - RCTBridgeModule 194 195- (void)setBridge:(RCTBridge *)bridge 196{ 197 _bridge = bridge; 198} 199 200// Required in Expo Go only - EXAV conforms to RCTBridgeModule protocol 201// and in Expo Go, kernel calls [EXReactAppManager rebuildBridge] 202// which requires this to be implemented. Normal "bare" RN modules 203// use RCT_EXPORT_MODULE macro which implement this automatically. 204+(NSString *)moduleName 205{ 206 return @"ExponentAV"; 207} 208 209// Both RCTBridgeModule and EXExportedModule define `constantsToExport`. We implement 210// that method for the latter, but React Bridge displays a yellow LogBox warning: 211// "Module EXAV requires main queue setup since it overrides `constantsToExport` but doesn't implement `requiresMainQueueSetup`." 212// Since we don't care about that (RCTBridgeModule is used here for another reason), 213// we just need this to dismiss that warning. 214+ (BOOL)requiresMainQueueSetup 215{ 216 // We are now using main thread to avoid thread safety issues with `EXAVPlayerData` and `EXVideoView` 217 // return `YES` to avoid deadlock warnings. 218 return YES; 219} 220 221#pragma mark - RCTEventEmitter 222 223- (void)startObserving 224{ 225 _isBeingObserved = YES; 226} 227 228- (void)stopObserving 229{ 230 _isBeingObserved = NO; 231} 232 233#pragma mark - Global audio state control API 234 235- (void)registerVideoForAudioLifecycle:(NSObject<EXAVObject> *)video 236{ 237 [_videoSet addObject:video]; 238} 239 240- (void)unregisterVideoForAudioLifecycle:(NSObject<EXAVObject> *)video 241{ 242 [_videoSet removeObject:video]; 243} 244 245- (void)_runBlockForAllAVObjects:(void (^)(NSObject<EXAVObject> *exAVObject))block 246{ 247 for (EXAVPlayerData *data in [_soundDictionary allValues]) { 248 block(data); 249 } 250 for (NSObject<EXAVObject> *video in [_videoSet allObjects]) { 251 block(video); 252 } 253} 254 255// This method is placed here so that it is easily referrable from _setAudioSessionCategoryForAudioMode. 256- (NSError *)_setAudioMode:(NSDictionary *)mode 257{ 258 BOOL playsInSilentMode = ((NSNumber *)mode[@"playsInSilentModeIOS"]).boolValue; 259 EXAudioInterruptionMode interruptionMode = ((NSNumber *)mode[@"interruptionModeIOS"]).intValue; 260 BOOL allowsRecording = ((NSNumber *)mode[@"allowsRecordingIOS"]).boolValue; 261 BOOL shouldPlayInBackground = ((NSNumber *)mode[@"staysActiveInBackground"]).boolValue; 262 263 if (!playsInSilentMode && interruptionMode == EXAudioInterruptionModeDuckOthers) { 264 return EXErrorWithMessage(@"Impossible audio mode: playsInSilentMode == false and duckOthers == true cannot be set on iOS."); 265 } else if (!playsInSilentMode && allowsRecording) { 266 return EXErrorWithMessage(@"Impossible audio mode: playsInSilentMode == false and allowsRecording == true cannot be set on iOS."); 267 } else if (!playsInSilentMode && shouldPlayInBackground) { 268 return EXErrorWithMessage(@"Impossible audio mode: playsInSilentMode == false and staysActiveInBackground == true cannot be set on iOS."); 269 } else { 270 if (!allowsRecording) { 271 if (_audioRecorder && [_audioRecorder isRecording]) { 272 [_audioRecorder pause]; 273 } 274 } 275 276 _playsInSilentMode = playsInSilentMode; 277 _audioInterruptionMode = interruptionMode; 278 _allowsAudioRecording = allowsRecording; 279 _staysActiveInBackground = shouldPlayInBackground; 280 281 if (_currentAudioSessionMode != EXAVAudioSessionModeInactive) { 282 return [self _updateAudioSessionCategoryForAudioSessionMode:[self _getAudioSessionModeRequired]]; 283 } 284 return nil; 285 } 286} 287 288- (NSError *)_updateAudioSessionCategoryForAudioSessionMode:(EXAVAudioSessionMode)audioSessionMode 289{ 290 AVAudioSessionCategory requiredAudioCategory; 291 AVAudioSessionCategoryOptions requiredAudioCategoryOptions = 0; 292 293 if (!_playsInSilentMode) { 294 // _allowsRecording is guaranteed to be false, and _interruptionMode is guaranteed to not be EXAudioInterruptionModeDuckOthers (see above) 295 if (_audioInterruptionMode == EXAudioInterruptionModeDoNotMix) { 296 requiredAudioCategory = AVAudioSessionCategorySoloAmbient; 297 } else { 298 requiredAudioCategory = AVAudioSessionCategoryAmbient; 299 } 300 } else { 301 EXAudioInterruptionMode activeInterruptionMode = audioSessionMode == EXAVAudioSessionModeActiveMuted ? EXAudioInterruptionModeMixWithOthers : _audioInterruptionMode; 302 NSString *category = _allowsAudioRecording ? AVAudioSessionCategoryPlayAndRecord : AVAudioSessionCategoryPlayback; 303 requiredAudioCategory = category; 304 switch (activeInterruptionMode) { 305 case EXAudioInterruptionModeDoNotMix: 306 break; 307 case EXAudioInterruptionModeDuckOthers: 308 requiredAudioCategoryOptions = AVAudioSessionCategoryOptionDuckOthers; 309 break; 310 case EXAudioInterruptionModeMixWithOthers: 311 default: 312 requiredAudioCategoryOptions = AVAudioSessionCategoryOptionMixWithOthers; 313 break; 314 } 315 } 316 317 if ([[_kernelAudioSessionManagerDelegate activeCategory] isEqual:requiredAudioCategory] && [_kernelAudioSessionManagerDelegate activeCategoryOptions] == requiredAudioCategoryOptions) { 318 return nil; 319 } 320 321 if (_allowsAudioRecording) { 322 // Bluetooth input is only available when recording is allowed 323 requiredAudioCategoryOptions = requiredAudioCategoryOptions | AVAudioSessionCategoryOptionAllowBluetooth; 324 } 325 326 return [_kernelAudioSessionManagerDelegate setCategory:requiredAudioCategory withOptions:requiredAudioCategoryOptions forModule:self]; 327} 328 329- (EXAVAudioSessionMode)_getAudioSessionModeRequired 330{ 331 __block EXAVAudioSessionMode audioSessionModeRequired = EXAVAudioSessionModeInactive; 332 333 [self _runBlockForAllAVObjects:^(NSObject<EXAVObject> *exAVObject) { 334 EXAVAudioSessionMode audioSessionModeRequiredByThisObject = [exAVObject getAudioSessionModeRequired]; 335 if (audioSessionModeRequiredByThisObject > audioSessionModeRequired) { 336 audioSessionModeRequired = audioSessionModeRequiredByThisObject; 337 } 338 }]; 339 340 if (_audioRecorder) { 341 if (_audioRecorderShouldBeginRecording || [_audioRecorder isRecording]) { 342 audioSessionModeRequired = EXAVAudioSessionModeActive; 343 } else if (_audioRecorderIsPreparing && audioSessionModeRequired == EXAVAudioSessionModeInactive) { 344 audioSessionModeRequired = EXAVAudioSessionModeActiveMuted; 345 } 346 } 347 348 return audioSessionModeRequired; 349} 350 351- (NSError *)promoteAudioSessionIfNecessary 352{ 353 if (!_audioIsEnabled) { 354 return EXErrorWithMessage(@"Expo Audio is disabled, so the audio session could not be activated."); 355 } 356 if (_isBackgrounded && !_staysActiveInBackground && ![_kernelAudioSessionManagerDelegate isActiveForModule:self]) { 357 return EXErrorWithMessage(@"This experience is currently in the background, so the audio session could not be activated."); 358 } 359 360 EXAVAudioSessionMode audioSessionModeRequired = [self _getAudioSessionModeRequired]; 361 362 if (audioSessionModeRequired == EXAVAudioSessionModeInactive) { 363 return nil; 364 } 365 366 NSError *error; 367 368 error = [self _updateAudioSessionCategoryForAudioSessionMode:audioSessionModeRequired]; 369 if (error) { 370 return error; 371 } 372 373 error = [_kernelAudioSessionManagerDelegate setActive:YES forModule:self]; 374 if (error) { 375 return error; 376 } 377 378 _currentAudioSessionMode = audioSessionModeRequired; 379 return nil; 380} 381 382- (NSError *)_deactivateAudioSession 383{ 384 if (_currentAudioSessionMode == EXAVAudioSessionModeInactive) { 385 return nil; 386 } 387 388 // We must have all players, recorders, and videos paused in order to effectively deactivate the session. 389 [self _runBlockForAllAVObjects:^(NSObject<EXAVObject> *exAVObject) { 390 [exAVObject pauseImmediately]; 391 }]; 392 if (_audioRecorder && [_audioRecorder isRecording]) { 393 [_audioRecorder pause]; 394 } 395 396 NSError *error = [_kernelAudioSessionManagerDelegate setActive:NO forModule:self]; 397 398 if (!error) { 399 _currentAudioSessionMode = EXAVAudioSessionModeInactive; 400 } 401 return error; 402} 403 404- (NSError *)demoteAudioSessionIfPossible 405{ 406 EXAVAudioSessionMode audioSessionModeRequired = [self _getAudioSessionModeRequired]; 407 408 // Current audio session mode is lower than the required one 409 // (we should rather promote the session than demote it). 410 if (_currentAudioSessionMode <= audioSessionModeRequired) { 411 return nil; 412 } 413 414 // We require the session to be muted and it is active. 415 // Let's only update the category. 416 if (audioSessionModeRequired == EXAVAudioSessionModeActiveMuted) { 417 NSError *error = [self _updateAudioSessionCategoryForAudioSessionMode:audioSessionModeRequired]; 418 if (!error) { 419 _currentAudioSessionMode = EXAVAudioSessionModeActiveMuted; 420 } 421 return error; 422 } 423 424 // We require the session to be inactive and it is active, let's deactivate it! 425 return [self _deactivateAudioSession]; 426} 427 428- (void)handleAudioSessionInterruption:(NSNotification *)notification 429{ 430 NSNumber *interruptionType = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey]; 431 if (interruptionType.unsignedIntegerValue == AVAudioSessionInterruptionTypeBegan) { 432 _currentAudioSessionMode = EXAVAudioSessionModeInactive; 433 } 434 435 [self _runBlockForAllAVObjects:^(NSObject<EXAVObject> *exAVObject) { 436 [exAVObject handleAudioSessionInterruption:notification]; 437 }]; 438} 439 440- (void)handleMediaServicesReset:(NSNotification *)notification 441{ 442 // See here: https://developer.apple.com/library/content/qa/qa1749/_index.html 443 // (this is an unlikely notification to receive, but best practices suggests that we catch it just in case) 444 445 // This is called whenever AirPods disconnect while a recording is in progress. 446 // The "best practice" is to tear down and recreate the audio session, but we're choosing to no-op 447 // in order to be able to resume recording with the phone mic. 448 449 _mediaServicesDidReset = true; 450} 451 452#pragma mark - Internal sound playback helper methods 453 454- (void)_runBlock:(void (^)(EXAVPlayerData *data))block 455 withSoundForKey:(nonnull NSNumber *)key 456 withRejecter:(EXPromiseRejectBlock)reject 457{ 458 EXAVPlayerData *data = _soundDictionary[key]; 459 if (data) { 460 block(data); 461 } else { 462 reject(@"E_AUDIO_NOPLAYER", @"Sound object not loaded. Did you unload it using Audio.unloadAsync?", nil); 463 } 464} 465 466- (void)_removeSoundForKey:(NSNumber *)key 467{ 468 EXAVPlayerData *data = _soundDictionary[key]; 469 if (data) { 470 [data pauseImmediately]; 471 _soundDictionary[key] = nil; 472 [self demoteAudioSessionIfPossible]; 473 } 474} 475 476- (void)_removeAudioCallbackForKey:(NSNumber *)key 477{ 478 EXAVPlayerData *data = _soundDictionary[key]; 479 if (data) { 480 [data setSampleBufferCallback:nil]; 481 } 482} 483 484#pragma mark - Internal video playback helper method 485 486- (void)_runBlock:(void (^)(EXVideoView *view))block 487withEXVideoViewForTag:(nonnull NSNumber *)reactTag 488 withRejecter:(EXPromiseRejectBlock)reject 489{ 490 // TODO check that the bridge is still valid after the dispatch 491 // TODO check if the queues are ok 492 [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXUIManager)] executeUIBlock:^(id view) { 493 if ([view isKindOfClass:[EXVideoView class]]) { 494 block(view); 495 } else { 496 reject(@"E_VIDEO_TAGINCORRECT", [NSString stringWithFormat:@"Invalid view returned from registry, expecting EXVideo, got: %@", view], nil); 497 } 498 } forView:reactTag ofClass:[EXVideoView class]]; 499} 500 501#pragma mark - Internal audio recording helper methods 502 503- (NSString *)_getBitRateStrategyFromEnum:(NSNumber *)bitRateEnumSelected 504{ 505 if (bitRateEnumSelected) { 506 switch ([bitRateEnumSelected integerValue]) { 507 case EXAudioRecordingOptionBitRateStrategyConstant: 508 return AVAudioBitRateStrategy_Constant; 509 case EXAudioRecordingOptionBitRateStrategyLongTermAverage: 510 return AVAudioBitRateStrategy_LongTermAverage; 511 case EXAudioRecordingOptionBitRateStrategyVariableConstrained: 512 return AVAudioBitRateStrategy_VariableConstrained; 513 break; 514 case EXAudioRecordingOptionBitRateStrategyVariable: 515 return AVAudioBitRateStrategy_Variable; 516 default: 517 return nil; 518 } 519 } 520 return nil; 521} 522 523- (NSDictionary<NSString *, NSString *> *)_getAVKeysForRecordingOptionsKeys:(NSString *)bitRateStrategy 524{ 525 return @{EXAudioRecordingOptionOutputFormatKey: AVFormatIDKey, 526 EXAudioRecordingOptionAudioQualityKey: 527 bitRateStrategy == AVAudioBitRateStrategy_Variable 528 ? AVEncoderAudioQualityForVBRKey : AVEncoderAudioQualityKey, 529 EXAudioRecordingOptionSampleRateKey: AVSampleRateKey, 530 EXAudioRecordingOptionNumberOfChannelsKey: AVNumberOfChannelsKey, 531 EXAudioRecordingOptionBitRateKey: AVEncoderBitRateKey, 532 EXAudioRecordingOptionBitDepthHintKey: AVEncoderBitDepthHintKey, 533 EXAudioRecordingOptionLinearPCMBitDepthKey: AVLinearPCMBitDepthKey, 534 EXAudioRecordingOptionLinearPCMIsBigEndianKey: AVLinearPCMIsBigEndianKey, 535 EXAudioRecordingOptionLinearPCMIsFloatKey: AVLinearPCMIsFloatKey}; 536} 537 538- (UInt32)_getFormatIDFromString:(NSString *)typeString 539{ 540 const char *s = typeString.UTF8String; 541 UInt32 typeCode = s[3] | (s[2] << 8) | (s[1] << 16) | (s[0] << 24); 542 return typeCode; 543} 544 545- (void)_setNewAudioRecorderFilenameAndSettings:(NSDictionary *)optionsFromJS 546{ 547 NSDictionary *iosOptionsFromJS = optionsFromJS[EXAudioRecordingOptionsKey]; 548 549 NSString *extension = iosOptionsFromJS[EXAudioRecordingOptionExtensionKey]; 550 _audioRecorderFilename = [NSString stringWithFormat:@"recording-%@%@", [[NSUUID UUID] UUIDString], extension]; 551 552 NSString *bitRateStrategy = [self _getBitRateStrategyFromEnum:iosOptionsFromJS[EXAudioRecordingOptionBitRateStrategyKey]]; 553 NSDictionary<NSString *, NSString *> *avKeysForRecordingOptionsKeys = [self _getAVKeysForRecordingOptionsKeys:bitRateStrategy]; 554 555 NSMutableDictionary *recorderSettings = [NSMutableDictionary new]; 556 for (NSString *recordingOptionsKey in avKeysForRecordingOptionsKeys) { 557 if (iosOptionsFromJS[recordingOptionsKey]) { 558 recorderSettings[avKeysForRecordingOptionsKeys[recordingOptionsKey]] = iosOptionsFromJS[recordingOptionsKey]; 559 } 560 } 561 recorderSettings[AVEncoderBitRateStrategyKey] = bitRateStrategy; 562 563 if ( 564 iosOptionsFromJS[EXAudioRecordingOptionOutputFormatKey] && 565 [iosOptionsFromJS[EXAudioRecordingOptionOutputFormatKey] isKindOfClass:[NSString class]] 566 ) { 567 recorderSettings[AVFormatIDKey] = 568 @([self _getFormatIDFromString:iosOptionsFromJS[EXAudioRecordingOptionOutputFormatKey]]); 569 } 570 571 _audioRecorderSettings = recorderSettings; 572} 573 574- (NSError *)_createNewAudioRecorder 575{ 576 if (_audioRecorder) { 577 return EXErrorWithMessage(@"Recorder is already prepared."); 578 } 579 580 id<EXFileSystemInterface> fileSystem = [_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXFileSystemInterface)]; 581 582 if (!fileSystem) { 583 return EXErrorWithMessage(@"No FileSystem module."); 584 } 585 586 NSString *directory = [fileSystem.cachesDirectory stringByAppendingPathComponent:@"AV"]; 587 [fileSystem ensureDirExistsWithPath:directory]; 588 NSString *soundFilePath = [directory stringByAppendingPathComponent:_audioRecorderFilename]; 589 NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath]; 590 591 NSError *error; 592 AVAudioRecorder *recorder = [[AVAudioRecorder alloc] initWithURL:soundFileURL 593 settings:_audioRecorderSettings 594 error:&error]; 595 if (error == nil) { 596 _audioRecorder = recorder; 597 } 598 return error; 599} 600 601- (int)_getDurationMillisOfRecordingAudioRecorder 602{ 603 return _audioRecorder ? (int) (_audioRecorder.currentTime * 1000) : 0; 604} 605 606- (NSDictionary *)_getAudioRecorderStatus 607{ 608 if (_audioRecorder) { 609 // [_audioRecorder currentTime] returns bad values after changing to bluetooth input 610 // so we track durationMillis independently of the audio recorder. 611 // see: https://stackoverflow.com/questions/43351904/avaudiorecorder-currenttime-giving-bad-values 612 int curTimestamp = (int) (_audioRecorder.deviceCurrentTime * 1000); 613 int curDuration = [_audioRecorder isRecording] ? (curTimestamp - _audioRecorderStartTimestamp) : 0; 614 int durationMillis = _prevAudioRecorderDurationMillis + curDuration; 615 616 NSMutableDictionary *result = [@{ 617 @"canRecord": @(YES), 618 @"isRecording": @([_audioRecorder isRecording]), 619 @"durationMillis": @(durationMillis), 620 @"mediaServicesDidReset": @(_mediaServicesDidReset), 621 } mutableCopy]; 622 623 if (_audioRecorder.meteringEnabled) { 624 [_audioRecorder updateMeters]; 625 float currentLevel = [_audioRecorder averagePowerForChannel: 0]; 626 result[@"metering"] = @(currentLevel); 627 } 628 629 return result; 630 } else { 631 return nil; 632 } 633} 634 635- (BOOL)_checkAudioRecorderExistsOrReject:(EXPromiseRejectBlock)reject 636{ 637 if (_audioRecorder == nil) { 638 reject(@"E_AUDIO_NORECORDER", @"Recorder does not exist. Prepare it first using Audio.prepareToRecordAsync.", nil); 639 } 640 return _audioRecorder != nil; 641} 642 643- (void)_removeAudioRecorder:(BOOL)removeFilenameAndSettings 644{ 645 if (_audioRecorder) { 646 [_audioRecorder stop]; 647 [self demoteAudioSessionIfPossible]; 648 _audioRecorder = nil; 649 } 650 if (removeFilenameAndSettings) { 651 _audioRecorderFilename = nil; 652 _audioRecorderSettings = nil; 653 _audioRecorderDurationMillis = 0; 654 } 655} 656 657- (NSArray<NSString *> *)supportedEvents 658{ 659 return @[EXDidUpdatePlaybackStatusEventName, EXDidUpdateMetadataEventName, @"ExponentAV.onError"]; 660} 661 662#pragma mark - Audio API: Global settings 663 664EX_EXPORT_METHOD_AS(setAudioIsEnabled, 665 setAudioIsEnabled:(BOOL)value 666 resolver:(EXPromiseResolveBlock)resolve 667 rejecter:(EXPromiseRejectBlock)reject) 668{ 669 _audioIsEnabled = value; 670 671 if (!value) { 672 [self _deactivateAudioSession]; 673 } 674 resolve(nil); 675} 676 677EX_EXPORT_METHOD_AS(setAudioMode, 678 setAudioMode:(NSDictionary *)mode 679 resolver:(EXPromiseResolveBlock)resolve 680 rejecter:(EXPromiseRejectBlock)reject) 681{ 682 NSError *error = [self _setAudioMode:mode]; 683 684 if (error) { 685 reject(@"E_AUDIO_AUDIOMODE", nil, error); 686 } else { 687 resolve(nil); 688 } 689} 690 691#pragma mark - Unified playback API - Audio 692 693EX_EXPORT_METHOD_AS(loadForSound, 694 loadForSound:(NSDictionary *)source 695 withStatus:(NSDictionary *)status 696 resolver:(EXPromiseResolveBlock)loadSuccess 697 rejecter:(EXPromiseRejectBlock)loadError) 698{ 699 NSNumber *key = @(_soundDictionaryKeyCount++); 700 701 EX_WEAKIFY(self); 702 EXAVPlayerData *data = [[EXAVPlayerData alloc] initWithEXAV:self 703 withSource:source 704 withStatus:status 705 withLoadFinishBlock:^(BOOL success, NSDictionary *successStatus, NSString *error) { 706 EX_ENSURE_STRONGIFY(self); 707 if (success) { 708 loadSuccess(@[key, successStatus]); 709 } else { 710 [self _removeSoundForKey:key]; 711 loadError(@"EXAV", error, nil); 712 } 713 }]; 714 data.errorCallback = ^(NSString *error) { 715 EX_ENSURE_STRONGIFY(self); 716 [self sendEventWithName:@"ExponentAV.onError" body:@{ 717 @"key": key, 718 @"error": error 719 }]; 720 [self _removeSoundForKey:key]; 721 }; 722 723 data.statusUpdateCallback = ^(NSDictionary *status) { 724 EX_ENSURE_STRONGIFY(self); 725 if (self.isBeingObserved) { 726 NSDictionary<NSString *, id> *response = @{@"key": key, @"status": status}; 727 [self sendEventWithName:EXDidUpdatePlaybackStatusEventName body:response]; 728 } 729 }; 730 731 data.metadataUpdateCallback = ^(NSDictionary *metadata) { 732 EX_ENSURE_STRONGIFY(self); 733 if (self.isBeingObserved) { 734 NSDictionary<NSString *, id> *response = @{@"key": key, @"metadata": metadata}; 735 [self sendEventWithName:EXDidUpdateMetadataEventName body:response]; 736 } 737 }; 738 739 _soundDictionary[key] = data; 740} 741 742- (void)sendEventWithName:(NSString *)eventName body:(NSDictionary *)body 743{ 744 [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)] sendEventWithName:eventName body:body]; 745} 746 747EX_EXPORT_METHOD_AS(unloadForSound, 748 unloadForSound:(NSNumber *)key 749 resolver:(EXPromiseResolveBlock)resolve 750 rejecter:(EXPromiseRejectBlock)reject) 751{ 752 [self _runBlock:^(EXAVPlayerData *data) { 753 [self _removeSoundForKey:key]; 754 resolve([EXAVPlayerData getUnloadedStatus]); 755 } withSoundForKey:key withRejecter:reject]; 756} 757 758EX_EXPORT_METHOD_AS(setStatusForSound, 759 setStatusForSound:(NSNumber *)key 760 withStatus:(NSDictionary *)status 761 resolver:(EXPromiseResolveBlock)resolve 762 rejecter:(EXPromiseRejectBlock)reject) 763{ 764 [self _runBlock:^(EXAVPlayerData *data) { 765 [data setStatus:status 766 resolver:resolve 767 rejecter:reject]; 768 } withSoundForKey:key withRejecter:reject]; 769} 770 771EX_EXPORT_METHOD_AS(getStatusForSound, 772 getStatusForSound:(NSNumber *)key 773 resolver:(EXPromiseResolveBlock)resolve 774 rejecter:(EXPromiseRejectBlock)reject) 775{ 776 [self _runBlock:^(EXAVPlayerData *data) { 777 NSDictionary *status = [data getStatus]; 778 resolve(status); 779 } withSoundForKey:key withRejecter:reject]; 780} 781 782EX_EXPORT_METHOD_AS(replaySound, 783 replaySound:(NSNumber *)key 784 withStatus:(NSDictionary *)status 785 resolver:(EXPromiseResolveBlock)resolve 786 rejecter:(EXPromiseRejectBlock)reject) 787{ 788 [self _runBlock:^(EXAVPlayerData *data) { 789 [data replayWithStatus:status 790 resolver:resolve 791 rejecter:reject]; 792 } withSoundForKey:key withRejecter:reject]; 793} 794 795#pragma mark - Unified playback API - Video 796 797EX_EXPORT_METHOD_AS(loadForVideo, 798 loadForVideo:(NSNumber *)reactTag 799 source:(NSDictionary *)source 800 withStatus:(NSDictionary *)status 801 resolver:(EXPromiseResolveBlock)resolve 802 rejecter:(EXPromiseRejectBlock)reject) 803{ 804 [self _runBlock:^(EXVideoView *view) { 805 [view setSource:source withStatus:status resolver:resolve rejecter:reject]; 806 } withEXVideoViewForTag:reactTag withRejecter:reject]; 807} 808 809EX_EXPORT_METHOD_AS(unloadForVideo, 810 unloadForVideo:(NSNumber *)reactTag 811 resolver:(EXPromiseResolveBlock)resolve 812 rejecter:(EXPromiseRejectBlock)reject) 813{ 814 [self _runBlock:^(EXVideoView *view) { 815 [view setSource:nil withStatus:nil resolver:resolve rejecter:reject]; 816 } withEXVideoViewForTag:reactTag withRejecter:reject]; 817} 818 819EX_EXPORT_METHOD_AS(setStatusForVideo, 820 setStatusForVideo:(NSNumber *)reactTag 821 withStatus:(NSDictionary *)status 822 resolver:(EXPromiseResolveBlock)resolve 823 rejecter:(EXPromiseRejectBlock)reject) 824{ 825 [self _runBlock:^(EXVideoView *view) { 826 [view setStatusFromPlaybackAPI:status resolver:resolve rejecter:reject]; 827 } withEXVideoViewForTag:reactTag withRejecter:reject]; 828} 829 830EX_EXPORT_METHOD_AS(replayVideo, 831 replayVideo:(NSNumber *)reactTag 832 withStatus:(NSDictionary *)status 833 resolver:(EXPromiseResolveBlock)resolve 834 rejecter:(EXPromiseRejectBlock)reject) 835{ 836 [self _runBlock:^(EXVideoView *view) { 837 [view replayWithStatus:status resolver:resolve rejecter:reject]; 838 } withEXVideoViewForTag:reactTag withRejecter:reject]; 839} 840 841EX_EXPORT_METHOD_AS(getStatusForVideo, 842 getStatusForVideo:(NSNumber *)reactTag 843 resolver:(EXPromiseResolveBlock)resolve 844 rejecter:(EXPromiseRejectBlock)reject) 845{ 846 [self _runBlock:^(EXVideoView *view) { 847 resolve(view.status); 848 } withEXVideoViewForTag:reactTag withRejecter:reject]; 849} 850 851// Note that setStatusUpdateCallback happens in the JS for video via onStatusUpdate 852 853#pragma mark - Audio API: Recording 854 855EX_EXPORT_METHOD_AS(getPermissionsAsync, 856 getPermissionsAsync:(EXPromiseResolveBlock)resolve 857 rejecter:(EXPromiseRejectBlock)reject) 858{ 859 [EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager 860 withRequester:[EXAudioRecordingPermissionRequester class] 861 resolve:resolve 862 reject:reject]; 863} 864 865EX_EXPORT_METHOD_AS(requestPermissionsAsync, 866 requestPermissionsAsync:(EXPromiseResolveBlock)resolve 867 rejecter:(EXPromiseRejectBlock)reject) 868{ 869 [EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager 870 withRequester:[EXAudioRecordingPermissionRequester class] 871 resolve:resolve 872 reject:reject]; 873} 874 875EX_EXPORT_METHOD_AS(prepareAudioRecorder, 876 prepareAudioRecorder:(NSDictionary *)options 877 resolver:(EXPromiseResolveBlock)resolve 878 rejecter:(EXPromiseRejectBlock)reject) 879{ 880 _mediaServicesDidReset = false; 881 if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXAudioRecordingPermissionRequester class]]) { 882 reject(@"E_MISSING_PERMISSION", @"Missing audio recording permission.", nil); 883 return; 884 } 885 if (!_allowsAudioRecording) { 886 reject(@"E_AUDIO_AUDIOMODE", @"Recording not allowed on iOS. Enable with Audio.setAudioModeAsync.", nil); 887 return; 888 } 889 890 [self _setNewAudioRecorderFilenameAndSettings:options]; 891 NSError *error = [self _createNewAudioRecorder]; 892 893 if (_audioRecorder && !error) { 894 _audioRecorderIsPreparing = true; 895 error = [self promoteAudioSessionIfNecessary]; 896 if (error) { 897 _audioRecorderIsPreparing = false; 898 [self _removeAudioRecorder:YES]; 899 reject(@"E_AUDIO_RECORDERNOTCREATED", [NSString stringWithFormat:@"Prepare encountered an error: %@", error.description], error); 900 return; 901 } else if (![_audioRecorder prepareToRecord]) { 902 _audioRecorderIsPreparing = false; 903 [self _removeAudioRecorder:YES]; 904 reject(@"E_AUDIO_RECORDERNOTCREATED", @"Prepare encountered an error: recorder not prepared.", nil); 905 return; 906 } 907 if (options[EXAudioRecordingOptionsIsMeteringEnabledKey]) { 908 _audioRecorder.meteringEnabled = true; 909 } 910 911 resolve(@{@"uri": [[_audioRecorder url] absoluteString], 912 @"status": [self _getAudioRecorderStatus]}); 913 _audioRecorderIsPreparing = false; 914 if (!options[EXAudioRecordingOptionsKeepAudioActiveHintKey]) { 915 [self demoteAudioSessionIfPossible]; 916 } 917 } else { 918 reject(@"E_AUDIO_RECORDERNOTCREATED", [NSString stringWithFormat:@"Prepare encountered an error: %@", error.description], error); 919 } 920} 921 922EX_EXPORT_METHOD_AS(startAudioRecording, 923 startAudioRecording:(EXPromiseResolveBlock)resolve 924 rejecter:(EXPromiseRejectBlock)reject) 925{ 926 if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXAudioRecordingPermissionRequester class]]) { 927 reject(@"E_MISSING_PERMISSION", @"Missing audio recording permission.", nil); 928 return; 929 } 930 if ([self _checkAudioRecorderExistsOrReject:reject]) { 931 if (!_allowsAudioRecording) { 932 reject(@"E_AUDIO_AUDIOMODE", @"Recording not allowed on iOS. Enable with Audio.setAudioModeAsync.", nil); 933 } else if (!_audioRecorder.recording) { 934 _audioRecorderShouldBeginRecording = true; 935 NSError *error = [self promoteAudioSessionIfNecessary]; 936 if (!error) { 937 if ([_audioRecorder record]) { 938 _audioRecorderStartTimestamp = (int) (_audioRecorder.deviceCurrentTime * 1000); 939 resolve([self _getAudioRecorderStatus]); 940 } else { 941 reject(@"E_AUDIO_RECORDING", @"Start encountered an error: recording not started.", nil); 942 } 943 } else { 944 reject(@"E_AUDIO_RECORDING", [NSString stringWithFormat:@"Start encountered an error: %@", error.description], error); 945 } 946 } else { 947 resolve([self _getAudioRecorderStatus]); 948 } 949 } 950 _audioRecorderShouldBeginRecording = false; 951} 952 953EX_EXPORT_METHOD_AS(pauseAudioRecording, 954 pauseAudioRecording:(EXPromiseResolveBlock)resolve 955 rejecter:(EXPromiseRejectBlock)reject) 956{ 957 if ([self _checkAudioRecorderExistsOrReject:reject]) { 958 if (_audioRecorder.recording) { 959 [_audioRecorder pause]; 960 int curTime = (int) (_audioRecorder.deviceCurrentTime * 1000); 961 _prevAudioRecorderDurationMillis += (curTime - _audioRecorderStartTimestamp); 962 _audioRecorderStartTimestamp = 0; 963 [self demoteAudioSessionIfPossible]; 964 } 965 resolve([self _getAudioRecorderStatus]); 966 } 967} 968 969EX_EXPORT_METHOD_AS(stopAudioRecording, 970 stopAudioRecording:(EXPromiseResolveBlock)resolve 971 rejecter:(EXPromiseRejectBlock)reject) 972{ 973 if ([self _checkAudioRecorderExistsOrReject:reject]) { 974 _audioRecorderDurationMillis = [self _getDurationMillisOfRecordingAudioRecorder]; 975 if (_audioRecorder.recording) { 976 [_audioRecorder stop]; 977 } 978 _prevAudioRecorderDurationMillis = 0; 979 _audioRecorderStartTimestamp = 0; 980 [self demoteAudioSessionIfPossible]; 981 982 resolve([self _getAudioRecorderStatus]); 983 } 984} 985 986EX_EXPORT_METHOD_AS(getAudioRecordingStatus, 987 getAudioRecordingStatus:(EXPromiseResolveBlock)resolve 988 rejecter:(EXPromiseRejectBlock)reject) 989{ 990 if ([self _checkAudioRecorderExistsOrReject:reject]) { 991 resolve([self _getAudioRecorderStatus]); 992 } 993} 994 995EX_EXPORT_METHOD_AS(unloadAudioRecorder, 996 unloadAudioRecorder:(EXPromiseResolveBlock)resolve 997 rejecter:(EXPromiseRejectBlock)reject) 998{ 999 if ([self _checkAudioRecorderExistsOrReject:reject]) { 1000 [self _removeAudioRecorder:YES]; 1001 resolve(nil); 1002 } 1003} 1004 1005EX_EXPORT_METHOD_AS(getAvailableInputs, 1006 resolver:(UMPromiseResolveBlock)resolve 1007 rejecter:(UMPromiseRejectBlock)reject) 1008{ 1009 NSMutableArray *inputs = [NSMutableArray new]; 1010 for (AVAudioSessionPortDescription *desc in [_kernelAudioSessionManagerDelegate availableInputs]){ 1011 [inputs addObject: @{ 1012 @"name": desc.portName, 1013 @"type": desc.portType, 1014 @"uid": desc.UID, 1015 }]; 1016 } 1017 resolve(inputs); 1018} 1019 1020EX_EXPORT_METHOD_AS(getCurrentInput, 1021 getCurrentInput:(UMPromiseResolveBlock)resolve 1022 rejecter:(UMPromiseRejectBlock)reject) 1023{ 1024 AVAudioSessionPortDescription *desc = [_kernelAudioSessionManagerDelegate activeInput]; 1025 if (desc) { 1026 resolve(@{ 1027 @"name": desc.portName, 1028 @"type": desc.portType, 1029 @"uid": desc.UID, 1030 }); 1031 } else { 1032 reject(@"E_AUDIO_GETCURRENTINPUT", @"No input port found.", nil); 1033 } 1034} 1035 1036EX_EXPORT_METHOD_AS(setInput, 1037 setInput:(NSString*)input 1038 resolver:(UMPromiseResolveBlock)resolve 1039 rejecter:(UMPromiseRejectBlock)reject) 1040{ 1041 AVAudioSessionPortDescription* preferredInput = nil; 1042 for (AVAudioSessionPortDescription *desc in [_kernelAudioSessionManagerDelegate availableInputs]){ 1043 if ([desc.UID isEqualToString:input]) { 1044 preferredInput = desc; 1045 } 1046 } 1047 if (preferredInput != nil) { 1048 [_kernelAudioSessionManagerDelegate setActiveInput:preferredInput]; 1049 resolve(nil); 1050 } else { 1051 reject(@"E_AUDIO_SETINPUT_FAIL", [NSString stringWithFormat:@"Preferred input '%@' not found!", input], nil); 1052 } 1053} 1054 1055- (dispatch_queue_t)methodQueue 1056{ 1057 return dispatch_get_main_queue(); 1058} 1059 1060#pragma mark - Lifecycle 1061 1062- (void)dealloc 1063{ 1064 [_kernelAudioSessionManagerDelegate moduleWillDeallocate:self]; 1065 [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] unregisterAppLifecycleListener:self]; 1066 [[NSNotificationCenter defaultCenter] removeObserver:self]; 1067 1068 // This will clear all @properties and deactivate the audio session: 1069 1070 for (NSObject<EXAVObject> *video in [_videoSet allObjects]) { 1071 [video pauseImmediately]; 1072 [_videoSet removeObject:video]; 1073 } 1074 [self _removeAudioRecorder:YES]; 1075 for (NSNumber *key in [_soundDictionary allKeys]) { 1076 [self _removeSoundForKey:key]; 1077 } 1078} 1079 1080@end 1081