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