xref: /expo/packages/expo-av/ios/EXAV/EXAV.m (revision 6887043a)
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