1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import <AVFoundation/AVFoundation.h> 4 5#import <ExpoModulesCore/EXDefines.h> 6 7#import <EXAV/EXAudioSessionManager.h> 8 9#import <EXAV/EXAV.h> 10 11@interface EXAudioSessionManager () 12 13@property (nonatomic, assign) BOOL sessionIsActive; 14@property (nonatomic, strong) NSString *activeCategory; 15@property (nonatomic, assign) AVAudioSessionCategoryOptions activeOptions; 16 17@property (nonatomic, strong) NSPointerArray *foregroundedModules; 18@property (nonatomic, strong) NSMapTable<id, NSNumber *> *activeModules; 19@property (nonatomic, strong) NSMapTable<id, NSString *> *moduleCategory; 20@property (nonatomic, strong) NSMapTable<id, NSNumber *> *moduleCategoryOptions; 21@property (nonatomic, strong) NSArray<AVAudioSessionPortDescription *>* availableInputs; 22@property (nonatomic, strong) AVAudioSessionPortDescription *activeInput; 23 24@end 25 26@implementation EXAudioSessionManager 27 28EX_REGISTER_SINGLETON_MODULE(AudioSessionManager); 29 30- (instancetype)init 31{ 32 if ((self = [super init])) { 33 _sessionIsActive = NO; 34 _foregroundedModules = [NSPointerArray weakObjectsPointerArray]; 35 _activeModules = [NSMapTable weakToStrongObjectsMapTable]; 36 _moduleCategory = [NSMapTable weakToStrongObjectsMapTable]; 37 _moduleCategoryOptions = [NSMapTable weakToStrongObjectsMapTable]; 38 39 AVAudioSession *session = [AVAudioSession sharedInstance]; 40 [[NSNotificationCenter defaultCenter] addObserver:self 41 selector:@selector(handleAudioSessionInterruption:) 42 name:AVAudioSessionInterruptionNotification 43 object:session]; 44 [[NSNotificationCenter defaultCenter] addObserver:self 45 selector:@selector(handleMediaServicesReset:) 46 name:AVAudioSessionMediaServicesWereResetNotification 47 object:session]; 48 } 49 return self; 50} 51 52- (void)dealloc 53{ 54 [[NSNotificationCenter defaultCenter] removeObserver:self]; 55} 56 57- (void)handleAudioSessionInterruption:(NSNotification *)notification 58{ 59 for (id foregroundedModule in [_foregroundedModules allObjects]) { 60 [foregroundedModule handleAudioSessionInterruption:notification]; 61 } 62} 63 64- (void)handleMediaServicesReset:(NSNotification *)notification 65{ 66 for (id foregroundedModule in [_foregroundedModules allObjects]) { 67 [foregroundedModule handleMediaServicesReset:notification]; 68 } 69} 70 71#pragma mark - EXAVScopedModuleDelegate 72 73- (BOOL)isActiveForModule:(id)module { 74 return [[_activeModules objectForKey:module] boolValue]; 75} 76 77- (NSString *)activeCategory 78{ 79 return [[AVAudioSession sharedInstance] category]; 80} 81 82- (AVAudioSessionCategoryOptions)activeCategoryOptions 83{ 84 return [[AVAudioSession sharedInstance] categoryOptions]; 85} 86 87- (NSError *)setActive:(BOOL)active forModule:(id)module { 88 BOOL prevActive = [self _shouldBeActive]; 89 90 if (active) { 91 [_activeModules setObject:@(YES) forKey:module]; 92 } else { 93 [_activeModules removeObjectForKey:module]; 94 } 95 96 if (active != prevActive) { 97 NSError *error = [self _updateSessionConfiguration]; 98 if (error && active) { 99 [_activeModules removeObjectForKey:module]; 100 } 101 return error; 102 } 103 return nil; 104} 105 106- (NSError *)setCategory:(NSString *)category withOptions:(AVAudioSessionCategoryOptions)options forModule:(id)module { 107 NSString *overridenCategory = [_moduleCategory objectForKey:module]; 108 NSNumber *overridenOptions = [_moduleCategoryOptions objectForKey:module]; 109 110 [_moduleCategory setObject:category forKey:module]; 111 [_moduleCategoryOptions setObject:@(options) forKey:module]; 112 113 BOOL oldSettingsWerePresent = overridenCategory && overridenOptions; 114 BOOL newSettingsAreDifferent = !oldSettingsWerePresent || ![overridenCategory isEqualToString:category] || ([overridenOptions unsignedIntegerValue] != options); 115 116 if ([[_foregroundedModules allObjects] containsObject:module] && newSettingsAreDifferent) { 117 return [self _updateSessionConfiguration]; 118 } 119 return nil; 120} 121 122- (NSArray<AVAudioSessionPortDescription *>*)availableInputs 123{ 124 return [[AVAudioSession sharedInstance] availableInputs]; 125} 126 127- (void)setActiveInput:(AVAudioSessionPortDescription *)activeInput 128{ 129 NSError *error; 130 [[AVAudioSession sharedInstance] setPreferredInput:activeInput error:&error]; 131} 132 133- (AVAudioSessionPortDescription *)activeInput 134{ 135 // If a current route exists for this recording, return the initial route input. 136 AVAudioSessionRouteDescription *currentRoute = [[AVAudioSession sharedInstance] currentRoute]; 137 NSArray *inputsForRoute = currentRoute.inputs; 138 if ([inputsForRoute count] > 0) { 139 return [inputsForRoute objectAtIndex:0]; 140 } 141 142 // If we've already set a preferred input, return it 143 AVAudioSessionPortDescription *preferredInput = [[AVAudioSession sharedInstance] preferredInput]; 144 if (preferredInput != Nil) { 145 return preferredInput; 146 } 147 148 // If we don't have a current route, select the first available input and set it as preferred input. 149 NSArray *availInputs = [[AVAudioSession sharedInstance] availableInputs]; 150 if ([availInputs count] > 0) { 151 AVAudioSessionPortDescription *defaultInput = [availInputs objectAtIndex:0]; 152 [self setActiveInput:defaultInput]; 153 return defaultInput; 154 } 155 156 return nil; 157} 158 159- (void)moduleDidBackground:(id)backgroundingModule 160{ 161 for (int i = 0; i < _foregroundedModules.count; i++) { 162 id pointer = [_foregroundedModules pointerAtIndex:i]; 163 if (pointer == (__bridge void * _Nullable)(backgroundingModule) || !pointer) { 164 [_foregroundedModules removePointerAtIndex:i]; 165 i--; 166 } 167 } 168 // compact doesn't work, that's why we need the `|| !pointer` above 169 // http://www.openradar.me/15396578 170 [_foregroundedModules compact]; 171 172 // Any possible failures are silent 173 [self _updateSessionConfiguration]; 174} 175 176- (void)moduleDidForeground:(id)module 177{ 178 // Check if module was already foregrounded 179 for (int i = 0; i < _foregroundedModules.count; i++) { 180 id pointer = [_foregroundedModules pointerAtIndex:i]; 181 if (pointer == (__bridge void * _Nullable)(module)) { 182 return; 183 } 184 } 185 186 [_foregroundedModules addPointer:(__bridge void * _Nullable)(module)]; 187 188 // Any possible failures are silent 189 [self _updateSessionConfiguration]; 190} 191 192- (void)moduleWillDeallocate:(id)module 193{ 194 // In case of reloading we won't get a notification about backgrounding, 195 // but still we would like to start afresh with an inactive audio session. 196 [self moduleDidBackground:module]; 197 [_activeModules removeObjectForKey:module]; 198 [_moduleCategory removeObjectForKey:module]; 199 [_moduleCategoryOptions removeObjectForKey:module]; 200} 201 202# pragma mark - Utilities 203 204- (NSError *)_updateSessionConfiguration 205{ 206 AVAudioSession *session = [AVAudioSession sharedInstance]; 207 NSError *error; 208 209 BOOL shouldBeActive = [self _shouldBeActive]; 210 NSString *category = [self _getRequestedCategory]; 211 AVAudioSessionCategoryOptions options = [self _getCategoryOptions]; 212 213 // If the session ought to be deactivated let's deactivate it and then configure. 214 // And if the session should be activated, let's configure it first! 215 216 // After ample discussion, testing and consideration, it makes no sense to constantly 217 // disable AVAudioSession as it is a synchronous and blocking process. There were no regressions 218 // in various tests. With this change, pauseAsync() function calls are effectively instant 219 // without frame drops. To avoid triggering unnecessary setActive:YES calls, we still set the variable 220 // without actually disabling the session. We agreed to go this way and in case of 221 // doubt look for another approach in a regression. 222 // See https://github.com/expo/expo/issues/15873 223 224 if (!shouldBeActive && _sessionIsActive) { 225 _sessionIsActive = NO; 226 } 227 228 /* 229 if (!shouldBeActive && _sessionIsActive) { 230 [session setActive:NO error:&error]; 231 if (!error) { 232 _sessionIsActive = NO; 233 } 234 } 235 236 if (error) { 237 return error; 238 } 239 */ 240 241 if (!_activeCategory || ![category isEqualToString:_activeCategory] || options != _activeOptions) { 242 [session setCategory:category withOptions:options error:&error]; 243 if (!error) { 244 _activeOptions = options; 245 _activeCategory = category; 246 } 247 } 248 249 if (error) { 250 return error; 251 } 252 253 if (shouldBeActive && !_sessionIsActive) { 254 [session setActive:YES error:&error]; 255 if (!error) { 256 _sessionIsActive = YES; 257 } 258 } 259 260 if (error) { 261 return error; 262 } 263 264 return nil; 265} 266 267- (BOOL)_shouldBeActive 268{ 269 for (id module in [_foregroundedModules allObjects]) { 270 if ([[_activeModules objectForKey:module] boolValue]) { 271 return YES; 272 } 273 } 274 return NO; 275} 276 277- (NSString *)_getRequestedCategory 278{ 279 NSArray *foregroundedModules = [_foregroundedModules allObjects]; 280 for (int i = (int) foregroundedModules.count - 1; i >= 0; i--) { 281 NSString *category = [_moduleCategory objectForKey:foregroundedModules[i]]; 282 if (category) { 283 return category; 284 } 285 } 286 287 return AVAudioSessionCategoryAmbient; 288} 289 290- (AVAudioSessionCategoryOptions)_getCategoryOptions 291{ 292 NSArray *foregroundedModules = [_foregroundedModules allObjects]; 293 for (int i = (int) foregroundedModules.count - 1; i >= 0; i--) { 294 NSNumber *categoryOptions = [_moduleCategoryOptions objectForKey:foregroundedModules[i]]; 295 if (categoryOptions) { 296 return [categoryOptions unsignedIntegerValue]; 297 } 298 } 299 300 return 0; 301} 302 303@end 304 305