blob: 83189b900c8104fbc2bf9d69e79645ea1e291248 [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;
21
22// This class needs to be thread-safe because it is accessed from many threads.
23// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
24// lock contention so coarse locks should be fine for now.
25@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080026 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080027 AVAudioSession *_session;
28 NSHashTable *_delegates;
29 NSInteger _activationCount;
tkchin0ce3bf92016-03-12 16:52:04 -080030 NSInteger _lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080031 BOOL _isActive;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080032}
33
34@synthesize session = _session;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080035
36+ (instancetype)sharedInstance {
37 static dispatch_once_t onceToken;
38 static RTCAudioSession *sharedInstance = nil;
39 dispatch_once(&onceToken, ^{
40 sharedInstance = [[RTCAudioSession alloc] init];
41 });
42 return sharedInstance;
43}
44
45- (instancetype)init {
46 if (self = [super init]) {
47 _session = [AVAudioSession sharedInstance];
48 _delegates = [NSHashTable weakObjectsHashTable];
tkchin0ce3bf92016-03-12 16:52:04 -080049
Zeke Chinb3fb71c2016-02-18 15:44:07 -080050 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
51 [center addObserver:self
52 selector:@selector(handleInterruptionNotification:)
53 name:AVAudioSessionInterruptionNotification
54 object:nil];
55 [center addObserver:self
56 selector:@selector(handleRouteChangeNotification:)
57 name:AVAudioSessionRouteChangeNotification
58 object:nil];
59 // TODO(tkchin): Maybe listen to SilenceSecondaryAudioHintNotification.
60 [center addObserver:self
61 selector:@selector(handleMediaServicesWereLost:)
62 name:AVAudioSessionMediaServicesWereLostNotification
63 object:nil];
64 [center addObserver:self
65 selector:@selector(handleMediaServicesWereReset:)
66 name:AVAudioSessionMediaServicesWereResetNotification
67 object:nil];
68 }
69 return self;
70}
71
72- (void)dealloc {
73 [[NSNotificationCenter defaultCenter] removeObserver:self];
74}
75
76- (void)setIsActive:(BOOL)isActive {
77 @synchronized(self) {
78 _isActive = isActive;
79 }
80}
81
82- (BOOL)isActive {
83 @synchronized(self) {
84 return _isActive;
85 }
86}
87
88- (BOOL)isLocked {
89 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -080090 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080091 }
92}
93
94- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
95 @synchronized(self) {
96 [_delegates addObject:delegate];
97 }
98}
99
100- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
101 @synchronized(self) {
102 [_delegates removeObject:delegate];
103 }
104}
105
106- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800107 _crit.Enter();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800108 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800109 ++_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800110 }
111}
112
113- (void)unlockForConfiguration {
114 // Don't let threads other than the one that called lockForConfiguration
115 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800116 if (_crit.TryEnter()) {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800117 @synchronized(self) {
tkchin0ce3bf92016-03-12 16:52:04 -0800118 --_lockRecursionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800119 }
120 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800121 // was called without anyone calling lock, we will hit an assertion.
122 _crit.Leave();
123 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800124 }
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