blob: c2985764d4fada32693d668701fcb4ad0bed8f37 [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"
tkchin0ce3bf92016-03-12 16:52:04 -080014#include "webrtc/base/criticalsection.h"
tkchine54467f2016-03-15 16:54:03 -070015#include "webrtc/modules/audio_device/ios/audio_device_ios.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080016
17#import "webrtc/base/objc/RTCLogging.h"
18#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h"
19
20NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
21NSInteger const kRTCAudioSessionErrorLockRequired = -1;
tkchin9f987d32016-03-12 20:06:28 -080022NSInteger const kRTCAudioSessionErrorConfiguration = -2;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080023
24// This class needs to be thread-safe because it is accessed from many threads.
25// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
26// lock contention so coarse locks should be fine for now.
27@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080028 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080029 AVAudioSession *_session;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080030 NSInteger _activationCount;
tkchin0ce3bf92016-03-12 16:52:04 -080031 NSInteger _lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080032 BOOL _isActive;
tkchin9f987d32016-03-12 20:06:28 -080033 BOOL _shouldDelayAudioConfiguration;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080034}
35
36@synthesize session = _session;
tkchine54467f2016-03-15 16:54:03 -070037@synthesize delegates = _delegates;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080038
39+ (instancetype)sharedInstance {
40 static dispatch_once_t onceToken;
41 static RTCAudioSession *sharedInstance = nil;
42 dispatch_once(&onceToken, ^{
43 sharedInstance = [[RTCAudioSession alloc] init];
44 });
45 return sharedInstance;
46}
47
48- (instancetype)init {
49 if (self = [super init]) {
50 _session = [AVAudioSession sharedInstance];
tkchin0ce3bf92016-03-12 16:52:04 -080051
Zeke Chinb3fb71c2016-02-18 15:44:07 -080052 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
53 [center addObserver:self
54 selector:@selector(handleInterruptionNotification:)
55 name:AVAudioSessionInterruptionNotification
56 object:nil];
57 [center addObserver:self
58 selector:@selector(handleRouteChangeNotification:)
59 name:AVAudioSessionRouteChangeNotification
60 object:nil];
61 // TODO(tkchin): Maybe listen to SilenceSecondaryAudioHintNotification.
62 [center addObserver:self
63 selector:@selector(handleMediaServicesWereLost:)
64 name:AVAudioSessionMediaServicesWereLostNotification
65 object:nil];
66 [center addObserver:self
67 selector:@selector(handleMediaServicesWereReset:)
68 name:AVAudioSessionMediaServicesWereResetNotification
69 object:nil];
70 }
71 return self;
72}
73
74- (void)dealloc {
75 [[NSNotificationCenter defaultCenter] removeObserver:self];
76}
77
78- (void)setIsActive:(BOOL)isActive {
79 @synchronized(self) {
80 _isActive = isActive;
81 }
82}
83
84- (BOOL)isActive {
85 @synchronized(self) {
86 return _isActive;
87 }
88}
89
90- (BOOL)isLocked {
91 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -080092 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080093 }
94}
95
tkchin9f987d32016-03-12 20:06:28 -080096- (void)setShouldDelayAudioConfiguration:(BOOL)shouldDelayAudioConfiguration {
97 @synchronized(self) {
98 if (_shouldDelayAudioConfiguration == shouldDelayAudioConfiguration) {
99 return;
100 }
101 _shouldDelayAudioConfiguration = shouldDelayAudioConfiguration;
102 }
103}
104
105- (BOOL)shouldDelayAudioConfiguration {
106 @synchronized(self) {
107 return _shouldDelayAudioConfiguration;
108 }
109}
110
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800111- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
tkchine54467f2016-03-15 16:54:03 -0700112 if (!delegate) {
113 return;
114 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800115 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700116 _delegates.push_back(delegate);
117 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800118 }
119}
120
121- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
tkchine54467f2016-03-15 16:54:03 -0700122 if (!delegate) {
123 return;
124 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800125 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700126 _delegates.erase(std::remove(_delegates.begin(),
127 _delegates.end(),
128 delegate));
129 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800130 }
131}
132
133- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800134 _crit.Enter();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800135 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800136 ++_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800137 }
138}
139
140- (void)unlockForConfiguration {
141 // Don't let threads other than the one that called lockForConfiguration
142 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800143 if (_crit.TryEnter()) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800144 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800145 --_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800146 }
147 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800148 // was called without anyone calling lock, we will hit an assertion.
149 _crit.Leave();
150 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800151 }
152}
153
154#pragma mark - AVAudioSession proxy methods
155
156- (NSString *)category {
157 return self.session.category;
158}
159
160- (AVAudioSessionCategoryOptions)categoryOptions {
161 return self.session.categoryOptions;
162}
163
164- (NSString *)mode {
165 return self.session.mode;
166}
167
168- (BOOL)secondaryAudioShouldBeSilencedHint {
169 return self.session.secondaryAudioShouldBeSilencedHint;
170}
171
172- (AVAudioSessionRouteDescription *)currentRoute {
173 return self.session.currentRoute;
174}
175
176- (NSInteger)maximumInputNumberOfChannels {
177 return self.session.maximumInputNumberOfChannels;
178}
179
180- (NSInteger)maximumOutputNumberOfChannels {
181 return self.session.maximumOutputNumberOfChannels;
182}
183
184- (float)inputGain {
185 return self.session.inputGain;
186}
187
188- (BOOL)inputGainSettable {
189 return self.session.inputGainSettable;
190}
191
192- (BOOL)inputAvailable {
193 return self.session.inputAvailable;
194}
195
196- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
197 return self.session.inputDataSources;
198}
199
200- (AVAudioSessionDataSourceDescription *)inputDataSource {
201 return self.session.inputDataSource;
202}
203
204- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
205 return self.session.outputDataSources;
206}
207
208- (AVAudioSessionDataSourceDescription *)outputDataSource {
209 return self.session.outputDataSource;
210}
211
212- (double)sampleRate {
213 return self.session.sampleRate;
214}
215
216- (NSInteger)inputNumberOfChannels {
217 return self.session.inputNumberOfChannels;
218}
219
220- (NSInteger)outputNumberOfChannels {
221 return self.session.outputNumberOfChannels;
222}
223
224- (float)outputVolume {
225 return self.session.outputVolume;
226}
227
228- (NSTimeInterval)inputLatency {
229 return self.session.inputLatency;
230}
231
232- (NSTimeInterval)outputLatency {
233 return self.session.outputLatency;
234}
235
236- (NSTimeInterval)IOBufferDuration {
237 return self.session.IOBufferDuration;
238}
239
tkchine54467f2016-03-15 16:54:03 -0700240// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
241// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800242- (BOOL)setActive:(BOOL)active
243 error:(NSError **)outError {
244 if (![self checkLock:outError]) {
245 return NO;
246 }
247 NSInteger activationCount = self.activationCount;
248 if (!active && activationCount == 0) {
249 RTCLogWarning(@"Attempting to deactivate without prior activation.");
250 }
251 BOOL success = YES;
252 BOOL isActive = self.isActive;
253 // Keep a local error so we can log it.
254 NSError *error = nil;
255 BOOL shouldSetActive =
256 (active && !isActive) || (!active && isActive && activationCount == 1);
257 // Attempt to activate if we're not active.
258 // Attempt to deactivate if we're active and it's the last unbalanced call.
259 if (shouldSetActive) {
260 AVAudioSession *session = self.session;
261 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
262 // that other audio sessions that were interrupted by our session can return
263 // to their active state. It is recommended for VoIP apps to use this
264 // option.
265 AVAudioSessionSetActiveOptions options =
266 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
267 success = [session setActive:active
268 withOptions:options
269 error:&error];
270 if (outError) {
271 *outError = error;
272 }
273 }
274 if (success) {
275 if (shouldSetActive) {
276 self.isActive = active;
277 }
278 if (active) {
279 [self incrementActivationCount];
280 }
281 } else {
tkchin9f987d32016-03-12 20:06:28 -0800282 RTCLogError(@"Failed to setActive:%d. Error: %@",
283 active, error.localizedDescription);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800284 }
285 // Decrement activation count on deactivation whether or not it succeeded.
286 if (!active) {
287 [self decrementActivationCount];
288 }
289 RTCLog(@"Number of current activations: %ld", (long)self.activationCount);
290 return success;
291}
292
293- (BOOL)setCategory:(NSString *)category
294 withOptions:(AVAudioSessionCategoryOptions)options
295 error:(NSError **)outError {
296 if (![self checkLock:outError]) {
297 return NO;
298 }
299 return [self.session setCategory:category withOptions:options error:outError];
300}
301
302- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
303 if (![self checkLock:outError]) {
304 return NO;
305 }
306 return [self.session setMode:mode error:outError];
307}
308
309- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
310 if (![self checkLock:outError]) {
311 return NO;
312 }
313 return [self.session setInputGain:gain error:outError];
314}
315
316- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
317 if (![self checkLock:outError]) {
318 return NO;
319 }
320 return [self.session setPreferredSampleRate:sampleRate error:outError];
321}
322
323- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
324 error:(NSError **)outError {
325 if (![self checkLock:outError]) {
326 return NO;
327 }
328 return [self.session setPreferredIOBufferDuration:duration error:outError];
329}
330
331- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
332 error:(NSError **)outError {
333 if (![self checkLock:outError]) {
334 return NO;
335 }
336 return [self.session setPreferredInputNumberOfChannels:count error:outError];
337}
338- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
339 error:(NSError **)outError {
340 if (![self checkLock:outError]) {
341 return NO;
342 }
343 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
344}
345
346- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
347 error:(NSError **)outError {
348 if (![self checkLock:outError]) {
349 return NO;
350 }
351 return [self.session overrideOutputAudioPort:portOverride error:outError];
352}
353
354- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
355 error:(NSError **)outError {
356 if (![self checkLock:outError]) {
357 return NO;
358 }
359 return [self.session setPreferredInput:inPort error:outError];
360}
361
362- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
363 error:(NSError **)outError {
364 if (![self checkLock:outError]) {
365 return NO;
366 }
367 return [self.session setInputDataSource:dataSource error:outError];
368}
369
370- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
371 error:(NSError **)outError {
372 if (![self checkLock:outError]) {
373 return NO;
374 }
375 return [self.session setOutputDataSource:dataSource error:outError];
376}
377
378#pragma mark - Notifications
379
380- (void)handleInterruptionNotification:(NSNotification *)notification {
381 NSNumber* typeNumber =
382 notification.userInfo[AVAudioSessionInterruptionTypeKey];
383 AVAudioSessionInterruptionType type =
384 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
385 switch (type) {
386 case AVAudioSessionInterruptionTypeBegan:
387 RTCLog(@"Audio session interruption began.");
388 self.isActive = NO;
389 [self notifyDidBeginInterruption];
390 break;
391 case AVAudioSessionInterruptionTypeEnded: {
392 RTCLog(@"Audio session interruption ended.");
393 [self updateAudioSessionAfterEvent];
394 NSNumber *optionsNumber =
395 notification.userInfo[AVAudioSessionInterruptionOptionKey];
396 AVAudioSessionInterruptionOptions options =
397 optionsNumber.unsignedIntegerValue;
398 BOOL shouldResume =
399 options & AVAudioSessionInterruptionOptionShouldResume;
400 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
401 break;
402 }
403 }
404}
405
406- (void)handleRouteChangeNotification:(NSNotification *)notification {
407 // Get reason for current route change.
408 NSNumber* reasonNumber =
409 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
410 AVAudioSessionRouteChangeReason reason =
411 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
412 RTCLog(@"Audio route changed:");
413 switch (reason) {
414 case AVAudioSessionRouteChangeReasonUnknown:
415 RTCLog(@"Audio route changed: ReasonUnknown");
416 break;
417 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
418 RTCLog(@"Audio route changed: NewDeviceAvailable");
419 break;
420 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
421 RTCLog(@"Audio route changed: OldDeviceUnavailable");
422 break;
423 case AVAudioSessionRouteChangeReasonCategoryChange:
424 RTCLog(@"Audio route changed: CategoryChange to :%@",
425 self.session.category);
426 break;
427 case AVAudioSessionRouteChangeReasonOverride:
428 RTCLog(@"Audio route changed: Override");
429 break;
430 case AVAudioSessionRouteChangeReasonWakeFromSleep:
431 RTCLog(@"Audio route changed: WakeFromSleep");
432 break;
433 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
434 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
435 break;
436 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
437 RTCLog(@"Audio route changed: RouteConfigurationChange");
438 break;
439 }
440 AVAudioSessionRouteDescription* previousRoute =
441 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
442 // Log previous route configuration.
443 RTCLog(@"Previous route: %@\nCurrent route:%@",
444 previousRoute, self.session.currentRoute);
445 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
446}
447
448- (void)handleMediaServicesWereLost:(NSNotification *)notification {
449 RTCLog(@"Media services were lost.");
450 [self updateAudioSessionAfterEvent];
451 [self notifyMediaServicesWereLost];
452}
453
454- (void)handleMediaServicesWereReset:(NSNotification *)notification {
455 RTCLog(@"Media services were reset.");
456 [self updateAudioSessionAfterEvent];
457 [self notifyMediaServicesWereReset];
458}
459
460#pragma mark - Private
461
462+ (NSError *)lockError {
463 NSDictionary *userInfo = @{
464 NSLocalizedDescriptionKey:
465 @"Must call lockForConfiguration before calling this method."
466 };
467 NSError *error =
468 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
469 code:kRTCAudioSessionErrorLockRequired
470 userInfo:userInfo];
471 return error;
472}
473
tkchine54467f2016-03-15 16:54:03 -0700474- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800475 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700476 // Note: this returns a copy.
477 return _delegates;
478 }
479}
480
481- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
482 @synchronized(self) {
483 _delegates.insert(_delegates.begin(), delegate);
484 }
485}
486
487- (void)removeZeroedDelegates {
488 @synchronized(self) {
489 for (auto it = _delegates.begin(); it != _delegates.end(); ++it) {
490 if (!*it) {
491 _delegates.erase(it);
492 }
493 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800494 }
495}
496
497- (NSInteger)activationCount {
498 @synchronized(self) {
499 return _activationCount;
500 }
501}
502
503- (NSInteger)incrementActivationCount {
504 RTCLog(@"Incrementing activation count.");
505 @synchronized(self) {
506 return ++_activationCount;
507 }
508}
509
510- (NSInteger)decrementActivationCount {
511 RTCLog(@"Decrementing activation count.");
512 @synchronized(self) {
513 return --_activationCount;
514 }
515}
516
tkchin9f987d32016-03-12 20:06:28 -0800517- (BOOL)checkLock:(NSError **)outError {
518 // Check ivar instead of trying to acquire lock so that we won't accidentally
519 // acquire lock if it hasn't already been called.
520 if (!self.isLocked) {
521 if (outError) {
522 *outError = [RTCAudioSession lockError];
523 }
524 return NO;
525 }
526 return YES;
527}
528
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800529- (void)updateAudioSessionAfterEvent {
530 BOOL shouldActivate = self.activationCount > 0;
531 AVAudioSessionSetActiveOptions options = shouldActivate ?
532 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
533 NSError *error = nil;
534 if ([self.session setActive:shouldActivate
535 withOptions:options
536 error:&error]) {
537 self.isActive = shouldActivate;
538 } else {
539 RTCLogError(@"Failed to set session active to %d. Error:%@",
540 shouldActivate, error.localizedDescription);
541 }
542}
543
544- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700545 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800546 [delegate audioSessionDidBeginInterruption:self];
547 }
548}
549
550- (void)notifyDidEndInterruptionWithShouldResumeSession:
551 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700552 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800553 [delegate audioSessionDidEndInterruption:self
554 shouldResumeSession:shouldResumeSession];
555 }
556
557}
558
559- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
560 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700561 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800562 [delegate audioSessionDidChangeRoute:self
563 reason:reason
564 previousRoute:previousRoute];
565 }
566}
567
568- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700569 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800570 [delegate audioSessionMediaServicesWereLost:self];
571 }
572}
573
574- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700575 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800576 [delegate audioSessionMediaServicesWereReset:self];
577 }
578}
579
580@end