blob: b80a7fd255e3ac48d11fa3ac68c67e9b706ecbad [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"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080015
16#import "webrtc/base/objc/RTCLogging.h"
17#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h"
18
19NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
20NSInteger const kRTCAudioSessionErrorLockRequired = -1;
tkchin9f987d32016-03-12 20:06:28 -080021NSInteger const kRTCAudioSessionErrorConfiguration = -2;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080022
23// This class needs to be thread-safe because it is accessed from many threads.
24// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
25// lock contention so coarse locks should be fine for now.
26@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080027 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080028 AVAudioSession *_session;
29 NSHashTable *_delegates;
30 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;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080037
38+ (instancetype)sharedInstance {
39 static dispatch_once_t onceToken;
40 static RTCAudioSession *sharedInstance = nil;
41 dispatch_once(&onceToken, ^{
42 sharedInstance = [[RTCAudioSession alloc] init];
43 });
44 return sharedInstance;
45}
46
47- (instancetype)init {
48 if (self = [super init]) {
49 _session = [AVAudioSession sharedInstance];
50 _delegates = [NSHashTable weakObjectsHashTable];
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 {
112 @synchronized(self) {
113 [_delegates addObject:delegate];
114 }
115}
116
117- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
118 @synchronized(self) {
119 [_delegates removeObject:delegate];
120 }
121}
122
123- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800124 _crit.Enter();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800125 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800126 ++_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800127 }
128}
129
130- (void)unlockForConfiguration {
131 // Don't let threads other than the one that called lockForConfiguration
132 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800133 if (_crit.TryEnter()) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800134 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800135 --_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800136 }
137 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800138 // was called without anyone calling lock, we will hit an assertion.
139 _crit.Leave();
140 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800141 }
142}
143
144#pragma mark - AVAudioSession proxy methods
145
146- (NSString *)category {
147 return self.session.category;
148}
149
150- (AVAudioSessionCategoryOptions)categoryOptions {
151 return self.session.categoryOptions;
152}
153
154- (NSString *)mode {
155 return self.session.mode;
156}
157
158- (BOOL)secondaryAudioShouldBeSilencedHint {
159 return self.session.secondaryAudioShouldBeSilencedHint;
160}
161
162- (AVAudioSessionRouteDescription *)currentRoute {
163 return self.session.currentRoute;
164}
165
166- (NSInteger)maximumInputNumberOfChannels {
167 return self.session.maximumInputNumberOfChannels;
168}
169
170- (NSInteger)maximumOutputNumberOfChannels {
171 return self.session.maximumOutputNumberOfChannels;
172}
173
174- (float)inputGain {
175 return self.session.inputGain;
176}
177
178- (BOOL)inputGainSettable {
179 return self.session.inputGainSettable;
180}
181
182- (BOOL)inputAvailable {
183 return self.session.inputAvailable;
184}
185
186- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
187 return self.session.inputDataSources;
188}
189
190- (AVAudioSessionDataSourceDescription *)inputDataSource {
191 return self.session.inputDataSource;
192}
193
194- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
195 return self.session.outputDataSources;
196}
197
198- (AVAudioSessionDataSourceDescription *)outputDataSource {
199 return self.session.outputDataSource;
200}
201
202- (double)sampleRate {
203 return self.session.sampleRate;
204}
205
206- (NSInteger)inputNumberOfChannels {
207 return self.session.inputNumberOfChannels;
208}
209
210- (NSInteger)outputNumberOfChannels {
211 return self.session.outputNumberOfChannels;
212}
213
214- (float)outputVolume {
215 return self.session.outputVolume;
216}
217
218- (NSTimeInterval)inputLatency {
219 return self.session.inputLatency;
220}
221
222- (NSTimeInterval)outputLatency {
223 return self.session.outputLatency;
224}
225
226- (NSTimeInterval)IOBufferDuration {
227 return self.session.IOBufferDuration;
228}
229
230- (BOOL)setActive:(BOOL)active
231 error:(NSError **)outError {
232 if (![self checkLock:outError]) {
233 return NO;
234 }
235 NSInteger activationCount = self.activationCount;
236 if (!active && activationCount == 0) {
237 RTCLogWarning(@"Attempting to deactivate without prior activation.");
238 }
239 BOOL success = YES;
240 BOOL isActive = self.isActive;
241 // Keep a local error so we can log it.
242 NSError *error = nil;
243 BOOL shouldSetActive =
244 (active && !isActive) || (!active && isActive && activationCount == 1);
245 // Attempt to activate if we're not active.
246 // Attempt to deactivate if we're active and it's the last unbalanced call.
247 if (shouldSetActive) {
248 AVAudioSession *session = self.session;
249 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
250 // that other audio sessions that were interrupted by our session can return
251 // to their active state. It is recommended for VoIP apps to use this
252 // option.
253 AVAudioSessionSetActiveOptions options =
254 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
255 success = [session setActive:active
256 withOptions:options
257 error:&error];
258 if (outError) {
259 *outError = error;
260 }
261 }
262 if (success) {
263 if (shouldSetActive) {
264 self.isActive = active;
265 }
266 if (active) {
267 [self incrementActivationCount];
268 }
269 } else {
tkchin9f987d32016-03-12 20:06:28 -0800270 RTCLogError(@"Failed to setActive:%d. Error: %@",
271 active, error.localizedDescription);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800272 }
273 // Decrement activation count on deactivation whether or not it succeeded.
274 if (!active) {
275 [self decrementActivationCount];
276 }
277 RTCLog(@"Number of current activations: %ld", (long)self.activationCount);
278 return success;
279}
280
281- (BOOL)setCategory:(NSString *)category
282 withOptions:(AVAudioSessionCategoryOptions)options
283 error:(NSError **)outError {
284 if (![self checkLock:outError]) {
285 return NO;
286 }
287 return [self.session setCategory:category withOptions:options error:outError];
288}
289
290- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
291 if (![self checkLock:outError]) {
292 return NO;
293 }
294 return [self.session setMode:mode error:outError];
295}
296
297- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
298 if (![self checkLock:outError]) {
299 return NO;
300 }
301 return [self.session setInputGain:gain error:outError];
302}
303
304- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
305 if (![self checkLock:outError]) {
306 return NO;
307 }
308 return [self.session setPreferredSampleRate:sampleRate error:outError];
309}
310
311- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
312 error:(NSError **)outError {
313 if (![self checkLock:outError]) {
314 return NO;
315 }
316 return [self.session setPreferredIOBufferDuration:duration error:outError];
317}
318
319- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
320 error:(NSError **)outError {
321 if (![self checkLock:outError]) {
322 return NO;
323 }
324 return [self.session setPreferredInputNumberOfChannels:count error:outError];
325}
326- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
327 error:(NSError **)outError {
328 if (![self checkLock:outError]) {
329 return NO;
330 }
331 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
332}
333
334- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
335 error:(NSError **)outError {
336 if (![self checkLock:outError]) {
337 return NO;
338 }
339 return [self.session overrideOutputAudioPort:portOverride error:outError];
340}
341
342- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
343 error:(NSError **)outError {
344 if (![self checkLock:outError]) {
345 return NO;
346 }
347 return [self.session setPreferredInput:inPort error:outError];
348}
349
350- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
351 error:(NSError **)outError {
352 if (![self checkLock:outError]) {
353 return NO;
354 }
355 return [self.session setInputDataSource:dataSource error:outError];
356}
357
358- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
359 error:(NSError **)outError {
360 if (![self checkLock:outError]) {
361 return NO;
362 }
363 return [self.session setOutputDataSource:dataSource error:outError];
364}
365
366#pragma mark - Notifications
367
368- (void)handleInterruptionNotification:(NSNotification *)notification {
369 NSNumber* typeNumber =
370 notification.userInfo[AVAudioSessionInterruptionTypeKey];
371 AVAudioSessionInterruptionType type =
372 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
373 switch (type) {
374 case AVAudioSessionInterruptionTypeBegan:
375 RTCLog(@"Audio session interruption began.");
376 self.isActive = NO;
377 [self notifyDidBeginInterruption];
378 break;
379 case AVAudioSessionInterruptionTypeEnded: {
380 RTCLog(@"Audio session interruption ended.");
381 [self updateAudioSessionAfterEvent];
382 NSNumber *optionsNumber =
383 notification.userInfo[AVAudioSessionInterruptionOptionKey];
384 AVAudioSessionInterruptionOptions options =
385 optionsNumber.unsignedIntegerValue;
386 BOOL shouldResume =
387 options & AVAudioSessionInterruptionOptionShouldResume;
388 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
389 break;
390 }
391 }
392}
393
394- (void)handleRouteChangeNotification:(NSNotification *)notification {
395 // Get reason for current route change.
396 NSNumber* reasonNumber =
397 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
398 AVAudioSessionRouteChangeReason reason =
399 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
400 RTCLog(@"Audio route changed:");
401 switch (reason) {
402 case AVAudioSessionRouteChangeReasonUnknown:
403 RTCLog(@"Audio route changed: ReasonUnknown");
404 break;
405 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
406 RTCLog(@"Audio route changed: NewDeviceAvailable");
407 break;
408 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
409 RTCLog(@"Audio route changed: OldDeviceUnavailable");
410 break;
411 case AVAudioSessionRouteChangeReasonCategoryChange:
412 RTCLog(@"Audio route changed: CategoryChange to :%@",
413 self.session.category);
414 break;
415 case AVAudioSessionRouteChangeReasonOverride:
416 RTCLog(@"Audio route changed: Override");
417 break;
418 case AVAudioSessionRouteChangeReasonWakeFromSleep:
419 RTCLog(@"Audio route changed: WakeFromSleep");
420 break;
421 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
422 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
423 break;
424 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
425 RTCLog(@"Audio route changed: RouteConfigurationChange");
426 break;
427 }
428 AVAudioSessionRouteDescription* previousRoute =
429 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
430 // Log previous route configuration.
431 RTCLog(@"Previous route: %@\nCurrent route:%@",
432 previousRoute, self.session.currentRoute);
433 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
434}
435
436- (void)handleMediaServicesWereLost:(NSNotification *)notification {
437 RTCLog(@"Media services were lost.");
438 [self updateAudioSessionAfterEvent];
439 [self notifyMediaServicesWereLost];
440}
441
442- (void)handleMediaServicesWereReset:(NSNotification *)notification {
443 RTCLog(@"Media services were reset.");
444 [self updateAudioSessionAfterEvent];
445 [self notifyMediaServicesWereReset];
446}
447
448#pragma mark - Private
449
450+ (NSError *)lockError {
451 NSDictionary *userInfo = @{
452 NSLocalizedDescriptionKey:
453 @"Must call lockForConfiguration before calling this method."
454 };
455 NSError *error =
456 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
457 code:kRTCAudioSessionErrorLockRequired
458 userInfo:userInfo];
459 return error;
460}
461
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800462- (NSSet *)delegates {
463 @synchronized(self) {
464 return _delegates.setRepresentation;
465 }
466}
467
468- (NSInteger)activationCount {
469 @synchronized(self) {
470 return _activationCount;
471 }
472}
473
474- (NSInteger)incrementActivationCount {
475 RTCLog(@"Incrementing activation count.");
476 @synchronized(self) {
477 return ++_activationCount;
478 }
479}
480
481- (NSInteger)decrementActivationCount {
482 RTCLog(@"Decrementing activation count.");
483 @synchronized(self) {
484 return --_activationCount;
485 }
486}
487
tkchin9f987d32016-03-12 20:06:28 -0800488- (BOOL)checkLock:(NSError **)outError {
489 // Check ivar instead of trying to acquire lock so that we won't accidentally
490 // acquire lock if it hasn't already been called.
491 if (!self.isLocked) {
492 if (outError) {
493 *outError = [RTCAudioSession lockError];
494 }
495 return NO;
496 }
497 return YES;
498}
499
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800500- (void)updateAudioSessionAfterEvent {
501 BOOL shouldActivate = self.activationCount > 0;
502 AVAudioSessionSetActiveOptions options = shouldActivate ?
503 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
504 NSError *error = nil;
505 if ([self.session setActive:shouldActivate
506 withOptions:options
507 error:&error]) {
508 self.isActive = shouldActivate;
509 } else {
510 RTCLogError(@"Failed to set session active to %d. Error:%@",
511 shouldActivate, error.localizedDescription);
512 }
513}
514
515- (void)notifyDidBeginInterruption {
516 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
517 [delegate audioSessionDidBeginInterruption:self];
518 }
519}
520
521- (void)notifyDidEndInterruptionWithShouldResumeSession:
522 (BOOL)shouldResumeSession {
523 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
524 [delegate audioSessionDidEndInterruption:self
525 shouldResumeSession:shouldResumeSession];
526 }
527
528}
529
530- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
531 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
532 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
533 [delegate audioSessionDidChangeRoute:self
534 reason:reason
535 previousRoute:previousRoute];
536 }
537}
538
539- (void)notifyMediaServicesWereLost {
540 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
541 [delegate audioSessionMediaServicesWereLost:self];
542 }
543}
544
545- (void)notifyMediaServicesWereReset {
546 for (id<RTCAudioSessionDelegate> delegate in self.delegates) {
547 [delegate audioSessionMediaServicesWereReset:self];
548 }
549}
550
551@end