blob: 05e9cb50c5070328323de70930956a06ad406ba3 [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
Zeke Chin1300caa2016-03-18 14:39:11 -070078- (NSString *)description {
79 NSString *format =
80 @"RTCAudioSession: {\n"
81 " isActive: %d\n"
82 " sampleRate: %.2f\n"
83 " IOBufferDuration: %f\n"
84 " outputNumberOfChannels: %ld\n"
85 " inputNumberOfChannels: %ld\n"
86 " outputLatency: %f\n"
87 " inputLatency: %f\n"
88 "}";
89 NSString *description = [NSString stringWithFormat:format,
90 self.isActive, self.sampleRate, self.IOBufferDuration,
91 self.outputNumberOfChannels, self.inputNumberOfChannels,
92 self.outputLatency, self.inputLatency];
93 return description;
94}
95
Zeke Chinb3fb71c2016-02-18 15:44:07 -080096- (void)setIsActive:(BOOL)isActive {
97 @synchronized(self) {
98 _isActive = isActive;
99 }
100}
101
102- (BOOL)isActive {
103 @synchronized(self) {
104 return _isActive;
105 }
106}
107
108- (BOOL)isLocked {
109 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800110 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800111 }
112}
113
tkchin9f987d32016-03-12 20:06:28 -0800114- (void)setShouldDelayAudioConfiguration:(BOOL)shouldDelayAudioConfiguration {
115 @synchronized(self) {
116 if (_shouldDelayAudioConfiguration == shouldDelayAudioConfiguration) {
117 return;
118 }
119 _shouldDelayAudioConfiguration = shouldDelayAudioConfiguration;
120 }
121}
122
123- (BOOL)shouldDelayAudioConfiguration {
124 @synchronized(self) {
125 return _shouldDelayAudioConfiguration;
126 }
127}
128
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800129- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
tkchine54467f2016-03-15 16:54:03 -0700130 if (!delegate) {
131 return;
132 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800133 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700134 _delegates.push_back(delegate);
135 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800136 }
137}
138
139- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
tkchine54467f2016-03-15 16:54:03 -0700140 if (!delegate) {
141 return;
142 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800143 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700144 _delegates.erase(std::remove(_delegates.begin(),
145 _delegates.end(),
146 delegate));
147 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800148 }
149}
150
151- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800152 _crit.Enter();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800153 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800154 ++_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800155 }
156}
157
158- (void)unlockForConfiguration {
159 // Don't let threads other than the one that called lockForConfiguration
160 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800161 if (_crit.TryEnter()) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800162 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800163 --_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800164 }
165 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800166 // was called without anyone calling lock, we will hit an assertion.
167 _crit.Leave();
168 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800169 }
170}
171
172#pragma mark - AVAudioSession proxy methods
173
174- (NSString *)category {
175 return self.session.category;
176}
177
178- (AVAudioSessionCategoryOptions)categoryOptions {
179 return self.session.categoryOptions;
180}
181
182- (NSString *)mode {
183 return self.session.mode;
184}
185
186- (BOOL)secondaryAudioShouldBeSilencedHint {
187 return self.session.secondaryAudioShouldBeSilencedHint;
188}
189
190- (AVAudioSessionRouteDescription *)currentRoute {
191 return self.session.currentRoute;
192}
193
194- (NSInteger)maximumInputNumberOfChannels {
195 return self.session.maximumInputNumberOfChannels;
196}
197
198- (NSInteger)maximumOutputNumberOfChannels {
199 return self.session.maximumOutputNumberOfChannels;
200}
201
202- (float)inputGain {
203 return self.session.inputGain;
204}
205
206- (BOOL)inputGainSettable {
207 return self.session.inputGainSettable;
208}
209
210- (BOOL)inputAvailable {
211 return self.session.inputAvailable;
212}
213
214- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
215 return self.session.inputDataSources;
216}
217
218- (AVAudioSessionDataSourceDescription *)inputDataSource {
219 return self.session.inputDataSource;
220}
221
222- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
223 return self.session.outputDataSources;
224}
225
226- (AVAudioSessionDataSourceDescription *)outputDataSource {
227 return self.session.outputDataSource;
228}
229
230- (double)sampleRate {
231 return self.session.sampleRate;
232}
233
234- (NSInteger)inputNumberOfChannels {
235 return self.session.inputNumberOfChannels;
236}
237
238- (NSInteger)outputNumberOfChannels {
239 return self.session.outputNumberOfChannels;
240}
241
242- (float)outputVolume {
243 return self.session.outputVolume;
244}
245
246- (NSTimeInterval)inputLatency {
247 return self.session.inputLatency;
248}
249
250- (NSTimeInterval)outputLatency {
251 return self.session.outputLatency;
252}
253
254- (NSTimeInterval)IOBufferDuration {
255 return self.session.IOBufferDuration;
256}
257
tkchine54467f2016-03-15 16:54:03 -0700258// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
259// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800260- (BOOL)setActive:(BOOL)active
261 error:(NSError **)outError {
262 if (![self checkLock:outError]) {
263 return NO;
264 }
265 NSInteger activationCount = self.activationCount;
266 if (!active && activationCount == 0) {
267 RTCLogWarning(@"Attempting to deactivate without prior activation.");
268 }
269 BOOL success = YES;
270 BOOL isActive = self.isActive;
271 // Keep a local error so we can log it.
272 NSError *error = nil;
273 BOOL shouldSetActive =
274 (active && !isActive) || (!active && isActive && activationCount == 1);
275 // Attempt to activate if we're not active.
276 // Attempt to deactivate if we're active and it's the last unbalanced call.
277 if (shouldSetActive) {
278 AVAudioSession *session = self.session;
279 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
280 // that other audio sessions that were interrupted by our session can return
281 // to their active state. It is recommended for VoIP apps to use this
282 // option.
283 AVAudioSessionSetActiveOptions options =
284 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
285 success = [session setActive:active
286 withOptions:options
287 error:&error];
288 if (outError) {
289 *outError = error;
290 }
291 }
292 if (success) {
293 if (shouldSetActive) {
294 self.isActive = active;
295 }
296 if (active) {
297 [self incrementActivationCount];
298 }
299 } else {
tkchin9f987d32016-03-12 20:06:28 -0800300 RTCLogError(@"Failed to setActive:%d. Error: %@",
301 active, error.localizedDescription);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800302 }
303 // Decrement activation count on deactivation whether or not it succeeded.
304 if (!active) {
305 [self decrementActivationCount];
306 }
307 RTCLog(@"Number of current activations: %ld", (long)self.activationCount);
308 return success;
309}
310
311- (BOOL)setCategory:(NSString *)category
312 withOptions:(AVAudioSessionCategoryOptions)options
313 error:(NSError **)outError {
314 if (![self checkLock:outError]) {
315 return NO;
316 }
317 return [self.session setCategory:category withOptions:options error:outError];
318}
319
320- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
321 if (![self checkLock:outError]) {
322 return NO;
323 }
324 return [self.session setMode:mode error:outError];
325}
326
327- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
328 if (![self checkLock:outError]) {
329 return NO;
330 }
331 return [self.session setInputGain:gain error:outError];
332}
333
334- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
335 if (![self checkLock:outError]) {
336 return NO;
337 }
338 return [self.session setPreferredSampleRate:sampleRate error:outError];
339}
340
341- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
342 error:(NSError **)outError {
343 if (![self checkLock:outError]) {
344 return NO;
345 }
346 return [self.session setPreferredIOBufferDuration:duration error:outError];
347}
348
349- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
350 error:(NSError **)outError {
351 if (![self checkLock:outError]) {
352 return NO;
353 }
354 return [self.session setPreferredInputNumberOfChannels:count error:outError];
355}
356- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
357 error:(NSError **)outError {
358 if (![self checkLock:outError]) {
359 return NO;
360 }
361 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
362}
363
364- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
365 error:(NSError **)outError {
366 if (![self checkLock:outError]) {
367 return NO;
368 }
369 return [self.session overrideOutputAudioPort:portOverride error:outError];
370}
371
372- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
373 error:(NSError **)outError {
374 if (![self checkLock:outError]) {
375 return NO;
376 }
377 return [self.session setPreferredInput:inPort error:outError];
378}
379
380- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
381 error:(NSError **)outError {
382 if (![self checkLock:outError]) {
383 return NO;
384 }
385 return [self.session setInputDataSource:dataSource error:outError];
386}
387
388- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
389 error:(NSError **)outError {
390 if (![self checkLock:outError]) {
391 return NO;
392 }
393 return [self.session setOutputDataSource:dataSource error:outError];
394}
395
396#pragma mark - Notifications
397
398- (void)handleInterruptionNotification:(NSNotification *)notification {
399 NSNumber* typeNumber =
400 notification.userInfo[AVAudioSessionInterruptionTypeKey];
401 AVAudioSessionInterruptionType type =
402 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
403 switch (type) {
404 case AVAudioSessionInterruptionTypeBegan:
405 RTCLog(@"Audio session interruption began.");
406 self.isActive = NO;
407 [self notifyDidBeginInterruption];
408 break;
409 case AVAudioSessionInterruptionTypeEnded: {
410 RTCLog(@"Audio session interruption ended.");
411 [self updateAudioSessionAfterEvent];
412 NSNumber *optionsNumber =
413 notification.userInfo[AVAudioSessionInterruptionOptionKey];
414 AVAudioSessionInterruptionOptions options =
415 optionsNumber.unsignedIntegerValue;
416 BOOL shouldResume =
417 options & AVAudioSessionInterruptionOptionShouldResume;
418 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
419 break;
420 }
421 }
422}
423
424- (void)handleRouteChangeNotification:(NSNotification *)notification {
425 // Get reason for current route change.
426 NSNumber* reasonNumber =
427 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
428 AVAudioSessionRouteChangeReason reason =
429 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
430 RTCLog(@"Audio route changed:");
431 switch (reason) {
432 case AVAudioSessionRouteChangeReasonUnknown:
433 RTCLog(@"Audio route changed: ReasonUnknown");
434 break;
435 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
436 RTCLog(@"Audio route changed: NewDeviceAvailable");
437 break;
438 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
439 RTCLog(@"Audio route changed: OldDeviceUnavailable");
440 break;
441 case AVAudioSessionRouteChangeReasonCategoryChange:
442 RTCLog(@"Audio route changed: CategoryChange to :%@",
443 self.session.category);
444 break;
445 case AVAudioSessionRouteChangeReasonOverride:
446 RTCLog(@"Audio route changed: Override");
447 break;
448 case AVAudioSessionRouteChangeReasonWakeFromSleep:
449 RTCLog(@"Audio route changed: WakeFromSleep");
450 break;
451 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
452 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
453 break;
454 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
455 RTCLog(@"Audio route changed: RouteConfigurationChange");
456 break;
457 }
458 AVAudioSessionRouteDescription* previousRoute =
459 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
460 // Log previous route configuration.
461 RTCLog(@"Previous route: %@\nCurrent route:%@",
462 previousRoute, self.session.currentRoute);
463 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
464}
465
466- (void)handleMediaServicesWereLost:(NSNotification *)notification {
467 RTCLog(@"Media services were lost.");
468 [self updateAudioSessionAfterEvent];
469 [self notifyMediaServicesWereLost];
470}
471
472- (void)handleMediaServicesWereReset:(NSNotification *)notification {
473 RTCLog(@"Media services were reset.");
474 [self updateAudioSessionAfterEvent];
475 [self notifyMediaServicesWereReset];
476}
477
478#pragma mark - Private
479
480+ (NSError *)lockError {
481 NSDictionary *userInfo = @{
482 NSLocalizedDescriptionKey:
483 @"Must call lockForConfiguration before calling this method."
484 };
485 NSError *error =
486 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
487 code:kRTCAudioSessionErrorLockRequired
488 userInfo:userInfo];
489 return error;
490}
491
tkchine54467f2016-03-15 16:54:03 -0700492- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800493 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700494 // Note: this returns a copy.
495 return _delegates;
496 }
497}
498
499- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
500 @synchronized(self) {
501 _delegates.insert(_delegates.begin(), delegate);
502 }
503}
504
505- (void)removeZeroedDelegates {
506 @synchronized(self) {
507 for (auto it = _delegates.begin(); it != _delegates.end(); ++it) {
508 if (!*it) {
509 _delegates.erase(it);
510 }
511 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800512 }
513}
514
515- (NSInteger)activationCount {
516 @synchronized(self) {
517 return _activationCount;
518 }
519}
520
521- (NSInteger)incrementActivationCount {
522 RTCLog(@"Incrementing activation count.");
523 @synchronized(self) {
524 return ++_activationCount;
525 }
526}
527
528- (NSInteger)decrementActivationCount {
529 RTCLog(@"Decrementing activation count.");
530 @synchronized(self) {
531 return --_activationCount;
532 }
533}
534
tkchin9f987d32016-03-12 20:06:28 -0800535- (BOOL)checkLock:(NSError **)outError {
536 // Check ivar instead of trying to acquire lock so that we won't accidentally
537 // acquire lock if it hasn't already been called.
538 if (!self.isLocked) {
539 if (outError) {
540 *outError = [RTCAudioSession lockError];
541 }
542 return NO;
543 }
544 return YES;
545}
546
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800547- (void)updateAudioSessionAfterEvent {
548 BOOL shouldActivate = self.activationCount > 0;
549 AVAudioSessionSetActiveOptions options = shouldActivate ?
550 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
551 NSError *error = nil;
552 if ([self.session setActive:shouldActivate
553 withOptions:options
554 error:&error]) {
555 self.isActive = shouldActivate;
556 } else {
557 RTCLogError(@"Failed to set session active to %d. Error:%@",
558 shouldActivate, error.localizedDescription);
559 }
560}
561
562- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700563 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800564 [delegate audioSessionDidBeginInterruption:self];
565 }
566}
567
568- (void)notifyDidEndInterruptionWithShouldResumeSession:
569 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700570 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800571 [delegate audioSessionDidEndInterruption:self
572 shouldResumeSession:shouldResumeSession];
573 }
574
575}
576
577- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
578 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700579 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800580 [delegate audioSessionDidChangeRoute:self
581 reason:reason
582 previousRoute:previousRoute];
583 }
584}
585
586- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700587 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800588 [delegate audioSessionMediaServicesWereLost:self];
589 }
590}
591
592- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700593 for (auto delegate : self.delegates) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800594 [delegate audioSessionMediaServicesWereReset:self];
595 }
596}
597
598@end