blob: ea7c546e8e9143decad7e60d08c729358ac155ca [file] [log] [blame]
Zeke Chinb3fb71c2016-02-18 15:44:07 -08001/*
2 * Copyright 2016 The WebRTC Project Authors. All rights reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10
11#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h"
12
13#include "webrtc/base/checks.h"
14
15#import "webrtc/base/objc/RTCLogging.h"
16#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h"
17
18NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
19NSInteger const kRTCAudioSessionErrorLockRequired = -1;
20
21// This class needs to be thread-safe because it is accessed from many threads.
22// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
23// lock contention so coarse locks should be fine for now.
24@implementation RTCAudioSession {
25 AVAudioSession *_session;
26 NSHashTable *_delegates;
27 NSInteger _activationCount;
28 BOOL _isActive;
29 BOOL _isLocked;
30}
31
32@synthesize session = _session;
33@synthesize lock = _lock;
34
35+ (instancetype)sharedInstance {
36 static dispatch_once_t onceToken;
37 static RTCAudioSession *sharedInstance = nil;
38 dispatch_once(&onceToken, ^{
39 sharedInstance = [[RTCAudioSession alloc] init];
40 });
41 return sharedInstance;
42}
43
44- (instancetype)init {
45 if (self = [super init]) {
46 _session = [AVAudioSession sharedInstance];
47 _delegates = [NSHashTable weakObjectsHashTable];
48 _lock = [[NSRecursiveLock alloc] init];
49 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
50 [center addObserver:self
51 selector:@selector(handleInterruptionNotification:)
52 name:AVAudioSessionInterruptionNotification
53 object:nil];
54 [center addObserver:self
55 selector:@selector(handleRouteChangeNotification:)
56 name:AVAudioSessionRouteChangeNotification
57 object:nil];
58 // TODO(tkchin): Maybe listen to SilenceSecondaryAudioHintNotification.
59 [center addObserver:self
60 selector:@selector(handleMediaServicesWereLost:)
61 name:AVAudioSessionMediaServicesWereLostNotification
62 object:nil];
63 [center addObserver:self
64 selector:@selector(handleMediaServicesWereReset:)
65 name:AVAudioSessionMediaServicesWereResetNotification
66 object:nil];
67 }
68 return self;
69}
70
71- (void)dealloc {
72 [[NSNotificationCenter defaultCenter] removeObserver:self];
73}
74
75- (void)setIsActive:(BOOL)isActive {
76 @synchronized(self) {
77 _isActive = isActive;
78 }
79}
80
81- (BOOL)isActive {
82 @synchronized(self) {
83 return _isActive;
84 }
85}
86
87- (BOOL)isLocked {
88 @synchronized(self) {
89 return _isLocked;
90 }
91}
92
93- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
94 @synchronized(self) {
95 [_delegates addObject:delegate];
96 }
97}
98
99- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
100 @synchronized(self) {
101 [_delegates removeObject:delegate];
102 }
103}
104
105- (void)lockForConfiguration {
106 [_lock lock];
107 @synchronized(self) {
108 _isLocked = YES;
109 }
110}
111
112- (void)unlockForConfiguration {
113 // Don't let threads other than the one that called lockForConfiguration
114 // unlock.
115 if ([_lock tryLock]) {
116 @synchronized(self) {
117 _isLocked = NO;
118 }
119 // One unlock for the tryLock, and another one to actually unlock. If this
120 // was called without anyone calling lock, the underlying NSRecursiveLock
121 // should spit out an error.
122 [_lock unlock];
123 [_lock unlock];
124 }
125}
126
127#pragma mark - AVAudioSession proxy methods
128
129- (NSString *)category {
130 return self.session.category;
131}
132
133- (AVAudioSessionCategoryOptions)categoryOptions {
134 return self.session.categoryOptions;
135}
136
137- (NSString *)mode {
138 return self.session.mode;
139}
140
141- (BOOL)secondaryAudioShouldBeSilencedHint {
142 return self.session.secondaryAudioShouldBeSilencedHint;
143}
144
145- (AVAudioSessionRouteDescription *)currentRoute {
146 return self.session.currentRoute;
147}
148
149- (NSInteger)maximumInputNumberOfChannels {
150 return self.session.maximumInputNumberOfChannels;
151}
152
153- (NSInteger)maximumOutputNumberOfChannels {
154 return self.session.maximumOutputNumberOfChannels;
155}
156
157- (float)inputGain {
158 return self.session.inputGain;
159}
160
161- (BOOL)inputGainSettable {
162 return self.session.inputGainSettable;
163}
164
165- (BOOL)inputAvailable {
166 return self.session.inputAvailable;
167}
168
169- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
170 return self.session.inputDataSources;
171}
172
173- (AVAudioSessionDataSourceDescription *)inputDataSource {
174 return self.session.inputDataSource;
175}
176
177- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
178 return self.session.outputDataSources;
179}
180
181- (AVAudioSessionDataSourceDescription *)outputDataSource {
182 return self.session.outputDataSource;
183}
184
185- (double)sampleRate {
186 return self.session.sampleRate;
187}
188
189- (NSInteger)inputNumberOfChannels {
190 return self.session.inputNumberOfChannels;
191}
192
193- (NSInteger)outputNumberOfChannels {
194 return self.session.outputNumberOfChannels;
195}
196
197- (float)outputVolume {
198 return self.session.outputVolume;
199}
200
201- (NSTimeInterval)inputLatency {
202 return self.session.inputLatency;
203}
204
205- (NSTimeInterval)outputLatency {
206 return self.session.outputLatency;
207}
208
209- (NSTimeInterval)IOBufferDuration {
210 return self.session.IOBufferDuration;
211}
212
213- (BOOL)setActive:(BOOL)active
214 error:(NSError **)outError {
215 if (![self checkLock:outError]) {
216 return NO;
217 }
218 NSInteger activationCount = self.activationCount;
219 if (!active && activationCount == 0) {
220 RTCLogWarning(@"Attempting to deactivate without prior activation.");
221 }
222 BOOL success = YES;
223 BOOL isActive = self.isActive;
224 // Keep a local error so we can log it.
225 NSError *error = nil;
226 BOOL shouldSetActive =
227 (active && !isActive) || (!active && isActive && activationCount == 1);
228 // Attempt to activate if we're not active.
229 // Attempt to deactivate if we're active and it's the last unbalanced call.
230 if (shouldSetActive) {
231 AVAudioSession *session = self.session;
232 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
233 // that other audio sessions that were interrupted by our session can return
234 // to their active state. It is recommended for VoIP apps to use this
235 // option.
236 AVAudioSessionSetActiveOptions options =
237 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
238 success = [session setActive:active
239 withOptions:options
240 error:&error];
241 if (outError) {
242 *outError = error;
243 }
244 }
245 if (success) {
246 if (shouldSetActive) {
247 self.isActive = active;
248 }
249 if (active) {
250 [self incrementActivationCount];
251 }
252 } else {
253 RTCLogError(@"Failed to setActive:%d. Error: %@", active, error);
254 }
255 // Decrement activation count on deactivation whether or not it succeeded.
256 if (!active) {
257 [self decrementActivationCount];
258 }
259 RTCLog(@"Number of current activations: %ld", (long)self.activationCount);
260 return success;
261}
262
263- (BOOL)setCategory:(NSString *)category
264 withOptions:(AVAudioSessionCategoryOptions)options
265 error:(NSError **)outError {
266 if (![self checkLock:outError]) {
267 return NO;
268 }
269 return [self.session setCategory:category withOptions:options error:outError];
270}
271
272- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
273 if (![self checkLock:outError]) {
274 return NO;
275 }
276 return [self.session setMode:mode error:outError];
277}
278
279- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
280 if (![self checkLock:outError]) {
281 return NO;
282 }
283 return [self.session setInputGain:gain error:outError];
284}
285
286- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
287 if (![self checkLock:outError]) {
288 return NO;
289 }
290 return [self.session setPreferredSampleRate:sampleRate error:outError];
291}
292
293- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
294 error:(NSError **)outError {
295 if (![self checkLock:outError]) {
296 return NO;
297 }
298 return [self.session setPreferredIOBufferDuration:duration error:outError];
299}
300
301- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
302 error:(NSError **)outError {
303 if (![self checkLock:outError]) {
304 return NO;
305 }
306 return [self.session setPreferredInputNumberOfChannels:count error:outError];
307}
308- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
309 error:(NSError **)outError {
310 if (![self checkLock:outError]) {
311 return NO;
312 }
313 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
314}
315
316- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
317 error:(NSError **)outError {
318 if (![self checkLock:outError]) {
319 return NO;
320 }
321 return [self.session overrideOutputAudioPort:portOverride error:outError];
322}
323
324- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
325 error:(NSError **)outError {
326 if (![self checkLock:outError]) {
327 return NO;
328 }
329 return [self.session setPreferredInput:inPort error:outError];
330}
331
332- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
333 error:(NSError **)outError {
334 if (![self checkLock:outError]) {
335 return NO;
336 }
337 return [self.session setInputDataSource:dataSource error:outError];
338}
339
340- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
341 error:(NSError **)outError {
342 if (![self checkLock:outError]) {
343 return NO;
344 }
345 return [self.session setOutputDataSource:dataSource error:outError];
346}
347
348#pragma mark - Notifications
349
350- (void)handleInterruptionNotification:(NSNotification *)notification {
351 NSNumber* typeNumber =
352 notification.userInfo[AVAudioSessionInterruptionTypeKey];
353 AVAudioSessionInterruptionType type =
354 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
355 switch (type) {
356 case AVAudioSessionInterruptionTypeBegan:
357 RTCLog(@"Audio session interruption began.");
358 self.isActive = NO;
359 [self notifyDidBeginInterruption];
360 break;
361 case AVAudioSessionInterruptionTypeEnded: {
362 RTCLog(@"Audio session interruption ended.");
363 [self updateAudioSessionAfterEvent];
364 NSNumber *optionsNumber =
365 notification.userInfo[AVAudioSessionInterruptionOptionKey];
366 AVAudioSessionInterruptionOptions options =
367 optionsNumber.unsignedIntegerValue;
368 BOOL shouldResume =
369 options & AVAudioSessionInterruptionOptionShouldResume;
370 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
371 break;
372 }
373 }
374}
375
376- (void)handleRouteChangeNotification:(NSNotification *)notification {
377 // Get reason for current route change.
378 NSNumber* reasonNumber =
379 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
380 AVAudioSessionRouteChangeReason reason =
381 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
382 RTCLog(@"Audio route changed:");
383 switch (reason) {
384 case AVAudioSessionRouteChangeReasonUnknown:
385 RTCLog(@"Audio route changed: ReasonUnknown");
386 break;
387 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
388 RTCLog(@"Audio route changed: NewDeviceAvailable");
389 break;
390 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
391 RTCLog(@"Audio route changed: OldDeviceUnavailable");
392 break;
393 case AVAudioSessionRouteChangeReasonCategoryChange:
394 RTCLog(@"Audio route changed: CategoryChange to :%@",
395 self.session.category);
396 break;
397 case AVAudioSessionRouteChangeReasonOverride:
398 RTCLog(@"Audio route changed: Override");
399 break;
400 case AVAudioSessionRouteChangeReasonWakeFromSleep:
401 RTCLog(@"Audio route changed: WakeFromSleep");
402 break;
403 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
404 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
405 break;
406 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
407 RTCLog(@"Audio route changed: RouteConfigurationChange");
408 break;
409 }
410 AVAudioSessionRouteDescription* previousRoute =
411 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
412 // Log previous route configuration.
413 RTCLog(@"Previous route: %@\nCurrent route:%@",
414 previousRoute, self.session.currentRoute);
415 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
416}
417
418- (void)handleMediaServicesWereLost:(NSNotification *)notification {
419 RTCLog(@"Media services were lost.");
420 [self updateAudioSessionAfterEvent];
421 [self notifyMediaServicesWereLost];
422}
423
424- (void)handleMediaServicesWereReset:(NSNotification *)notification {
425 RTCLog(@"Media services were reset.");
426 [self updateAudioSessionAfterEvent];
427 [self notifyMediaServicesWereReset];
428}
429
430#pragma mark - Private
431
432+ (NSError *)lockError {
433 NSDictionary *userInfo = @{
434 NSLocalizedDescriptionKey:
435 @"Must call lockForConfiguration before calling this method."
436 };
437 NSError *error =
438 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
439 code:kRTCAudioSessionErrorLockRequired
440 userInfo:userInfo];
441 return error;
442}
443
444- (BOOL)checkLock:(NSError **)outError {
445 // Check ivar instead of trying to acquire lock so that we won't accidentally
446 // acquire lock if it hasn't already been called.
447 if (!self.isLocked) {
448 if (outError) {
449 *outError = [RTCAudioSession lockError];
450 }
451 return NO;
452 }
453 return YES;
454}
455
456- (NSSet *)delegates {
457 @synchronized(self) {
458 return _delegates.setRepresentation;
459 }
460}
461
462- (NSInteger)activationCount {
463 @synchronized(self) {
464 return _activationCount;
465 }
466}
467
468- (NSInteger)incrementActivationCount {
469 RTCLog(@"Incrementing activation count.");
470 @synchronized(self) {
471 return ++_activationCount;
472 }
473}
474
475- (NSInteger)decrementActivationCount {
476 RTCLog(@"Decrementing activation count.");
477 @synchronized(self) {
478 return --_activationCount;
479 }
480}
481
482- (void)updateAudioSessionAfterEvent {
483 BOOL shouldActivate = self.activationCount > 0;
484 AVAudioSessionSetActiveOptions options = shouldActivate ?
485 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
486 NSError *error = nil;
487 if ([self.session setActive:shouldActivate
488 withOptions:options
489 error:&error]) {
490 self.isActive = shouldActivate;
491 } else {
492 RTCLogError(@"Failed to set session active to %d. Error:%@",
493 shouldActivate, error.localizedDescription);
494 }
495}
496
497- (void)notifyDidBeginInterruption {
498 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
499 [delegate audioSessionDidBeginInterruption:self];
500 }
501}
502
503- (void)notifyDidEndInterruptionWithShouldResumeSession:
504 (BOOL)shouldResumeSession {
505 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
506 [delegate audioSessionDidEndInterruption:self
507 shouldResumeSession:shouldResumeSession];
508 }
509
510}
511
512- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
513 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
514 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
515 [delegate audioSessionDidChangeRoute:self
516 reason:reason
517 previousRoute:previousRoute];
518 }
519}
520
521- (void)notifyMediaServicesWereLost {
522 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
523 [delegate audioSessionMediaServicesWereLost:self];
524 }
525}
526
527- (void)notifyMediaServicesWereReset {
528 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
529 [delegate audioSessionMediaServicesWereReset:self];
530 }
531}
532
533@end