blob: 307b97959e4dab45fff46d1e3afb4a23b16e8a8f [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
denicija59ee91b2017-06-05 05:48:47 -070011#import "WebRTC/RTCAudioSession.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080012
tkchin93dd6342016-07-27 10:17:14 -070013#import <UIKit/UIKit.h>
14
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020015#include "rtc_base/atomicops.h"
16#include "rtc_base/checks.h"
17#include "rtc_base/criticalsection.h"
denicija59ee91b2017-06-05 05:48:47 -070018
19#import "WebRTC/RTCAudioSessionConfiguration.h"
tkchin9eeb6242016-04-27 01:54:20 -070020#import "WebRTC/RTCLogging.h"
denicija59ee91b2017-06-05 05:48:47 -070021
22#import "RTCAudioSession+Private.h"
23
Zeke Chinb3fb71c2016-02-18 15:44:07 -080024
25NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
26NSInteger const kRTCAudioSessionErrorLockRequired = -1;
tkchin9f987d32016-03-12 20:06:28 -080027NSInteger const kRTCAudioSessionErrorConfiguration = -2;
jtteh13ae11a2017-05-25 17:52:20 -070028NSString * const kRTCAudioSessionOutputVolumeSelector = @"outputVolume";
Zeke Chinb3fb71c2016-02-18 15:44:07 -080029
30// This class needs to be thread-safe because it is accessed from many threads.
31// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
32// lock contention so coarse locks should be fine for now.
33@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080034 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080035 AVAudioSession *_session;
Tze Kwang Chin307a0922016-03-21 13:57:40 -070036 volatile int _activationCount;
37 volatile int _lockRecursionCount;
38 volatile int _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080039 BOOL _isActive;
tkchind2511962016-05-06 18:54:15 -070040 BOOL _useManualAudio;
41 BOOL _isAudioEnabled;
42 BOOL _canPlayOrRecord;
tkchin93dd6342016-07-27 10:17:14 -070043 BOOL _isInterrupted;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080044}
45
46@synthesize session = _session;
tkchine54467f2016-03-15 16:54:03 -070047@synthesize delegates = _delegates;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080048
49+ (instancetype)sharedInstance {
50 static dispatch_once_t onceToken;
51 static RTCAudioSession *sharedInstance = nil;
52 dispatch_once(&onceToken, ^{
Tze Kwang Chin307a0922016-03-21 13:57:40 -070053 sharedInstance = [[self alloc] init];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080054 });
55 return sharedInstance;
56}
57
58- (instancetype)init {
Peter Hanspers47217362017-10-05 11:39:15 +020059 return [self initWithAudioSession:[AVAudioSession sharedInstance]];
60}
61
62/** This initializer provides a way for unit tests to inject a fake/mock audio session. */
63- (instancetype)initWithAudioSession:(id)audioSession {
Zeke Chinb3fb71c2016-02-18 15:44:07 -080064 if (self = [super init]) {
Peter Hanspers47217362017-10-05 11:39:15 +020065 _session = audioSession;
tkchin0ce3bf92016-03-12 16:52:04 -080066
Zeke Chinb3fb71c2016-02-18 15:44:07 -080067 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
68 [center addObserver:self
69 selector:@selector(handleInterruptionNotification:)
70 name:AVAudioSessionInterruptionNotification
71 object:nil];
72 [center addObserver:self
73 selector:@selector(handleRouteChangeNotification:)
74 name:AVAudioSessionRouteChangeNotification
75 object:nil];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080076 [center addObserver:self
77 selector:@selector(handleMediaServicesWereLost:)
78 name:AVAudioSessionMediaServicesWereLostNotification
79 object:nil];
80 [center addObserver:self
81 selector:@selector(handleMediaServicesWereReset:)
82 name:AVAudioSessionMediaServicesWereResetNotification
83 object:nil];
henrikaf1363fd2016-09-27 06:06:44 -070084 // Posted on the main thread when the primary audio from other applications
85 // starts and stops. Foreground applications may use this notification as a
86 // hint to enable or disable audio that is secondary.
87 [center addObserver:self
88 selector:@selector(handleSilenceSecondaryAudioHintNotification:)
89 name:AVAudioSessionSilenceSecondaryAudioHintNotification
90 object:nil];
tkchin93dd6342016-07-27 10:17:14 -070091 // Also track foreground event in order to deal with interruption ended situation.
92 [center addObserver:self
93 selector:@selector(handleApplicationDidBecomeActive:)
94 name:UIApplicationDidBecomeActiveNotification
95 object:nil];
jtteh13ae11a2017-05-25 17:52:20 -070096 [_session addObserver:self
97 forKeyPath:kRTCAudioSessionOutputVolumeSelector
98 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
Peter Hanspers47217362017-10-05 11:39:15 +020099 context:(__bridge void*)RTCAudioSession.class];
jtteh13ae11a2017-05-25 17:52:20 -0700100
haysc7735b1e2017-03-29 14:53:32 -0700101 RTCLog(@"RTCAudioSession (%p): init.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800102 }
103 return self;
104}
105
106- (void)dealloc {
107 [[NSNotificationCenter defaultCenter] removeObserver:self];
Peter Hanspers47217362017-10-05 11:39:15 +0200108 [_session removeObserver:self
109 forKeyPath:kRTCAudioSessionOutputVolumeSelector
110 context:(__bridge void*)RTCAudioSession.class];
haysc7735b1e2017-03-29 14:53:32 -0700111 RTCLog(@"RTCAudioSession (%p): dealloc.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800112}
113
Zeke Chin1300caa2016-03-18 14:39:11 -0700114- (NSString *)description {
115 NSString *format =
116 @"RTCAudioSession: {\n"
tkchind2511962016-05-06 18:54:15 -0700117 " category: %@\n"
118 " categoryOptions: %ld\n"
119 " mode: %@\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700120 " isActive: %d\n"
121 " sampleRate: %.2f\n"
122 " IOBufferDuration: %f\n"
123 " outputNumberOfChannels: %ld\n"
124 " inputNumberOfChannels: %ld\n"
125 " outputLatency: %f\n"
126 " inputLatency: %f\n"
henrikac5aea652016-09-21 07:45:55 -0700127 " outputVolume: %f\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700128 "}";
129 NSString *description = [NSString stringWithFormat:format,
tkchind2511962016-05-06 18:54:15 -0700130 self.category, (long)self.categoryOptions, self.mode,
Zeke Chin1300caa2016-03-18 14:39:11 -0700131 self.isActive, self.sampleRate, self.IOBufferDuration,
132 self.outputNumberOfChannels, self.inputNumberOfChannels,
henrikac5aea652016-09-21 07:45:55 -0700133 self.outputLatency, self.inputLatency, self.outputVolume];
Zeke Chin1300caa2016-03-18 14:39:11 -0700134 return description;
135}
136
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800137- (void)setIsActive:(BOOL)isActive {
138 @synchronized(self) {
139 _isActive = isActive;
140 }
141}
142
143- (BOOL)isActive {
144 @synchronized(self) {
145 return _isActive;
146 }
147}
148
149- (BOOL)isLocked {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700150 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800151}
152
tkchind2511962016-05-06 18:54:15 -0700153- (void)setUseManualAudio:(BOOL)useManualAudio {
tkchin9f987d32016-03-12 20:06:28 -0800154 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700155 if (_useManualAudio == useManualAudio) {
tkchin9f987d32016-03-12 20:06:28 -0800156 return;
157 }
tkchind2511962016-05-06 18:54:15 -0700158 _useManualAudio = useManualAudio;
159 }
160 [self updateCanPlayOrRecord];
161}
162
163- (BOOL)useManualAudio {
164 @synchronized(self) {
165 return _useManualAudio;
tkchin9f987d32016-03-12 20:06:28 -0800166 }
167}
168
tkchind2511962016-05-06 18:54:15 -0700169- (void)setIsAudioEnabled:(BOOL)isAudioEnabled {
tkchin9f987d32016-03-12 20:06:28 -0800170 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700171 if (_isAudioEnabled == isAudioEnabled) {
172 return;
173 }
174 _isAudioEnabled = isAudioEnabled;
175 }
176 [self updateCanPlayOrRecord];
177}
178
179- (BOOL)isAudioEnabled {
180 @synchronized(self) {
181 return _isAudioEnabled;
tkchin9f987d32016-03-12 20:06:28 -0800182 }
183}
184
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700185// TODO(tkchin): Check for duplicates.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800186- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700187 RTCLog(@"Adding delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700188 if (!delegate) {
189 return;
190 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800191 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700192 _delegates.push_back(delegate);
193 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800194 }
195}
196
197- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700198 RTCLog(@"Removing delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700199 if (!delegate) {
200 return;
201 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800202 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700203 _delegates.erase(std::remove(_delegates.begin(),
204 _delegates.end(),
tkchinefdd9302016-04-11 12:00:59 -0700205 delegate),
206 _delegates.end());
tkchine54467f2016-03-15 16:54:03 -0700207 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800208 }
209}
210
kthelgasonee8b8612017-03-31 04:50:27 -0700211#pragma clang diagnostic push
212#pragma clang diagnostic ignored "-Wthread-safety-analysis"
213
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800214- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800215 _crit.Enter();
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700216 rtc::AtomicOps::Increment(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800217}
218
219- (void)unlockForConfiguration {
220 // Don't let threads other than the one that called lockForConfiguration
221 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800222 if (_crit.TryEnter()) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700223 rtc::AtomicOps::Decrement(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800224 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800225 // was called without anyone calling lock, we will hit an assertion.
226 _crit.Leave();
227 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800228 }
229}
230
kthelgasonee8b8612017-03-31 04:50:27 -0700231#pragma clang diagnostic pop
232
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800233#pragma mark - AVAudioSession proxy methods
234
235- (NSString *)category {
236 return self.session.category;
237}
238
239- (AVAudioSessionCategoryOptions)categoryOptions {
240 return self.session.categoryOptions;
241}
242
243- (NSString *)mode {
244 return self.session.mode;
245}
246
247- (BOOL)secondaryAudioShouldBeSilencedHint {
248 return self.session.secondaryAudioShouldBeSilencedHint;
249}
250
251- (AVAudioSessionRouteDescription *)currentRoute {
252 return self.session.currentRoute;
253}
254
255- (NSInteger)maximumInputNumberOfChannels {
256 return self.session.maximumInputNumberOfChannels;
257}
258
259- (NSInteger)maximumOutputNumberOfChannels {
260 return self.session.maximumOutputNumberOfChannels;
261}
262
263- (float)inputGain {
264 return self.session.inputGain;
265}
266
267- (BOOL)inputGainSettable {
268 return self.session.inputGainSettable;
269}
270
271- (BOOL)inputAvailable {
272 return self.session.inputAvailable;
273}
274
275- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
276 return self.session.inputDataSources;
277}
278
279- (AVAudioSessionDataSourceDescription *)inputDataSource {
280 return self.session.inputDataSource;
281}
282
283- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
284 return self.session.outputDataSources;
285}
286
287- (AVAudioSessionDataSourceDescription *)outputDataSource {
288 return self.session.outputDataSource;
289}
290
291- (double)sampleRate {
292 return self.session.sampleRate;
293}
294
tkchind2511962016-05-06 18:54:15 -0700295- (double)preferredSampleRate {
296 return self.session.preferredSampleRate;
297}
298
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800299- (NSInteger)inputNumberOfChannels {
300 return self.session.inputNumberOfChannels;
301}
302
303- (NSInteger)outputNumberOfChannels {
304 return self.session.outputNumberOfChannels;
305}
306
307- (float)outputVolume {
308 return self.session.outputVolume;
309}
310
311- (NSTimeInterval)inputLatency {
312 return self.session.inputLatency;
313}
314
315- (NSTimeInterval)outputLatency {
316 return self.session.outputLatency;
317}
318
319- (NSTimeInterval)IOBufferDuration {
320 return self.session.IOBufferDuration;
321}
322
tkchind2511962016-05-06 18:54:15 -0700323- (NSTimeInterval)preferredIOBufferDuration {
324 return self.session.preferredIOBufferDuration;
325}
326
tkchine54467f2016-03-15 16:54:03 -0700327// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
328// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800329- (BOOL)setActive:(BOOL)active
330 error:(NSError **)outError {
331 if (![self checkLock:outError]) {
332 return NO;
333 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700334 int activationCount = _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800335 if (!active && activationCount == 0) {
336 RTCLogWarning(@"Attempting to deactivate without prior activation.");
337 }
JT Tehc1f083d2018-04-25 09:19:35 -0700338 [self notifyWillSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800339 BOOL success = YES;
340 BOOL isActive = self.isActive;
341 // Keep a local error so we can log it.
342 NSError *error = nil;
343 BOOL shouldSetActive =
344 (active && !isActive) || (!active && isActive && activationCount == 1);
345 // Attempt to activate if we're not active.
346 // Attempt to deactivate if we're active and it's the last unbalanced call.
347 if (shouldSetActive) {
348 AVAudioSession *session = self.session;
349 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
350 // that other audio sessions that were interrupted by our session can return
351 // to their active state. It is recommended for VoIP apps to use this
352 // option.
353 AVAudioSessionSetActiveOptions options =
354 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
355 success = [session setActive:active
356 withOptions:options
357 error:&error];
358 if (outError) {
359 *outError = error;
360 }
361 }
362 if (success) {
363 if (shouldSetActive) {
364 self.isActive = active;
365 }
366 if (active) {
367 [self incrementActivationCount];
368 }
JT Tehc1f083d2018-04-25 09:19:35 -0700369 [self notifyDidSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800370 } else {
tkchin9f987d32016-03-12 20:06:28 -0800371 RTCLogError(@"Failed to setActive:%d. Error: %@",
372 active, error.localizedDescription);
JT Tehc1f083d2018-04-25 09:19:35 -0700373 [self notifyFailedToSetActive:active error:error];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800374 }
375 // Decrement activation count on deactivation whether or not it succeeded.
376 if (!active) {
377 [self decrementActivationCount];
378 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700379 RTCLog(@"Number of current activations: %d", _activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800380 return success;
381}
382
383- (BOOL)setCategory:(NSString *)category
384 withOptions:(AVAudioSessionCategoryOptions)options
385 error:(NSError **)outError {
386 if (![self checkLock:outError]) {
387 return NO;
388 }
389 return [self.session setCategory:category withOptions:options error:outError];
390}
391
392- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
393 if (![self checkLock:outError]) {
394 return NO;
395 }
396 return [self.session setMode:mode error:outError];
397}
398
399- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
400 if (![self checkLock:outError]) {
401 return NO;
402 }
403 return [self.session setInputGain:gain error:outError];
404}
405
406- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
407 if (![self checkLock:outError]) {
408 return NO;
409 }
410 return [self.session setPreferredSampleRate:sampleRate error:outError];
411}
412
413- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
414 error:(NSError **)outError {
415 if (![self checkLock:outError]) {
416 return NO;
417 }
418 return [self.session setPreferredIOBufferDuration:duration error:outError];
419}
420
421- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
422 error:(NSError **)outError {
423 if (![self checkLock:outError]) {
424 return NO;
425 }
426 return [self.session setPreferredInputNumberOfChannels:count error:outError];
427}
428- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
429 error:(NSError **)outError {
430 if (![self checkLock:outError]) {
431 return NO;
432 }
433 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
434}
435
436- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
437 error:(NSError **)outError {
438 if (![self checkLock:outError]) {
439 return NO;
440 }
441 return [self.session overrideOutputAudioPort:portOverride error:outError];
442}
443
444- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
445 error:(NSError **)outError {
446 if (![self checkLock:outError]) {
447 return NO;
448 }
449 return [self.session setPreferredInput:inPort error:outError];
450}
451
452- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
453 error:(NSError **)outError {
454 if (![self checkLock:outError]) {
455 return NO;
456 }
457 return [self.session setInputDataSource:dataSource error:outError];
458}
459
460- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
461 error:(NSError **)outError {
462 if (![self checkLock:outError]) {
463 return NO;
464 }
465 return [self.session setOutputDataSource:dataSource error:outError];
466}
467
468#pragma mark - Notifications
469
470- (void)handleInterruptionNotification:(NSNotification *)notification {
471 NSNumber* typeNumber =
472 notification.userInfo[AVAudioSessionInterruptionTypeKey];
473 AVAudioSessionInterruptionType type =
474 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
475 switch (type) {
476 case AVAudioSessionInterruptionTypeBegan:
477 RTCLog(@"Audio session interruption began.");
478 self.isActive = NO;
tkchin93dd6342016-07-27 10:17:14 -0700479 self.isInterrupted = YES;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800480 [self notifyDidBeginInterruption];
481 break;
482 case AVAudioSessionInterruptionTypeEnded: {
483 RTCLog(@"Audio session interruption ended.");
tkchin93dd6342016-07-27 10:17:14 -0700484 self.isInterrupted = NO;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800485 [self updateAudioSessionAfterEvent];
486 NSNumber *optionsNumber =
487 notification.userInfo[AVAudioSessionInterruptionOptionKey];
488 AVAudioSessionInterruptionOptions options =
489 optionsNumber.unsignedIntegerValue;
490 BOOL shouldResume =
491 options & AVAudioSessionInterruptionOptionShouldResume;
492 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
493 break;
494 }
495 }
496}
497
498- (void)handleRouteChangeNotification:(NSNotification *)notification {
499 // Get reason for current route change.
500 NSNumber* reasonNumber =
501 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
502 AVAudioSessionRouteChangeReason reason =
503 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
504 RTCLog(@"Audio route changed:");
505 switch (reason) {
506 case AVAudioSessionRouteChangeReasonUnknown:
507 RTCLog(@"Audio route changed: ReasonUnknown");
508 break;
509 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
510 RTCLog(@"Audio route changed: NewDeviceAvailable");
511 break;
512 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
513 RTCLog(@"Audio route changed: OldDeviceUnavailable");
514 break;
515 case AVAudioSessionRouteChangeReasonCategoryChange:
516 RTCLog(@"Audio route changed: CategoryChange to :%@",
517 self.session.category);
518 break;
519 case AVAudioSessionRouteChangeReasonOverride:
520 RTCLog(@"Audio route changed: Override");
521 break;
522 case AVAudioSessionRouteChangeReasonWakeFromSleep:
523 RTCLog(@"Audio route changed: WakeFromSleep");
524 break;
525 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
526 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
527 break;
528 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
529 RTCLog(@"Audio route changed: RouteConfigurationChange");
530 break;
531 }
532 AVAudioSessionRouteDescription* previousRoute =
533 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
534 // Log previous route configuration.
535 RTCLog(@"Previous route: %@\nCurrent route:%@",
536 previousRoute, self.session.currentRoute);
537 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
538}
539
540- (void)handleMediaServicesWereLost:(NSNotification *)notification {
541 RTCLog(@"Media services were lost.");
542 [self updateAudioSessionAfterEvent];
543 [self notifyMediaServicesWereLost];
544}
545
546- (void)handleMediaServicesWereReset:(NSNotification *)notification {
547 RTCLog(@"Media services were reset.");
548 [self updateAudioSessionAfterEvent];
549 [self notifyMediaServicesWereReset];
550}
551
henrikaf1363fd2016-09-27 06:06:44 -0700552- (void)handleSilenceSecondaryAudioHintNotification:(NSNotification *)notification {
553 // TODO(henrika): just adding logs here for now until we know if we are ever
554 // see this notification and might be affected by it or if further actions
555 // are required.
556 NSNumber *typeNumber =
557 notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey];
558 AVAudioSessionSilenceSecondaryAudioHintType type =
559 (AVAudioSessionSilenceSecondaryAudioHintType)typeNumber.unsignedIntegerValue;
560 switch (type) {
561 case AVAudioSessionSilenceSecondaryAudioHintTypeBegin:
562 RTCLog(@"Another application's primary audio has started.");
563 break;
564 case AVAudioSessionSilenceSecondaryAudioHintTypeEnd:
565 RTCLog(@"Another application's primary audio has stopped.");
566 break;
567 }
568}
569
tkchin93dd6342016-07-27 10:17:14 -0700570- (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
Zeke Chin8280a562018-07-10 13:53:55 -0700571 BOOL isInterrupted = self.isInterrupted;
haysc7735b1e2017-03-29 14:53:32 -0700572 RTCLog(@"Application became active after an interruption. Treating as interruption "
Zeke Chin8280a562018-07-10 13:53:55 -0700573 "end. isInterrupted changed from %d to 0.",
574 isInterrupted);
575 if (isInterrupted) {
tkchin93dd6342016-07-27 10:17:14 -0700576 self.isInterrupted = NO;
577 [self updateAudioSessionAfterEvent];
tkchin93dd6342016-07-27 10:17:14 -0700578 }
haysc7735b1e2017-03-29 14:53:32 -0700579 // Always treat application becoming active as an interruption end event.
580 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
tkchin93dd6342016-07-27 10:17:14 -0700581}
582
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800583#pragma mark - Private
584
585+ (NSError *)lockError {
586 NSDictionary *userInfo = @{
587 NSLocalizedDescriptionKey:
588 @"Must call lockForConfiguration before calling this method."
589 };
590 NSError *error =
591 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
592 code:kRTCAudioSessionErrorLockRequired
593 userInfo:userInfo];
594 return error;
595}
596
tkchine54467f2016-03-15 16:54:03 -0700597- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800598 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700599 // Note: this returns a copy.
600 return _delegates;
601 }
602}
603
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700604// TODO(tkchin): check for duplicates.
tkchine54467f2016-03-15 16:54:03 -0700605- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
606 @synchronized(self) {
607 _delegates.insert(_delegates.begin(), delegate);
608 }
609}
610
611- (void)removeZeroedDelegates {
612 @synchronized(self) {
tkchinefdd9302016-04-11 12:00:59 -0700613 _delegates.erase(
614 std::remove_if(_delegates.begin(),
615 _delegates.end(),
616 [](id delegate) -> bool { return delegate == nil; }),
617 _delegates.end());
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800618 }
619}
620
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700621- (int)activationCount {
622 return _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800623}
624
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700625- (int)incrementActivationCount {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800626 RTCLog(@"Incrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700627 return rtc::AtomicOps::Increment(&_activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800628}
629
630- (NSInteger)decrementActivationCount {
631 RTCLog(@"Decrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700632 return rtc::AtomicOps::Decrement(&_activationCount);
633}
634
635- (int)webRTCSessionCount {
636 return _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800637}
638
tkchind2511962016-05-06 18:54:15 -0700639- (BOOL)canPlayOrRecord {
640 return !self.useManualAudio || self.isAudioEnabled;
641}
642
tkchin93dd6342016-07-27 10:17:14 -0700643- (BOOL)isInterrupted {
644 @synchronized(self) {
645 return _isInterrupted;
646 }
647}
648
649- (void)setIsInterrupted:(BOOL)isInterrupted {
650 @synchronized(self) {
651 if (_isInterrupted == isInterrupted) {
652 return;
653 }
654 _isInterrupted = isInterrupted;
655 }
656}
657
tkchin9f987d32016-03-12 20:06:28 -0800658- (BOOL)checkLock:(NSError **)outError {
659 // Check ivar instead of trying to acquire lock so that we won't accidentally
660 // acquire lock if it hasn't already been called.
661 if (!self.isLocked) {
662 if (outError) {
663 *outError = [RTCAudioSession lockError];
664 }
665 return NO;
666 }
667 return YES;
668}
669
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700670- (BOOL)beginWebRTCSession:(NSError **)outError {
671 if (outError) {
672 *outError = nil;
673 }
674 if (![self checkLock:outError]) {
675 return NO;
676 }
tkchind2511962016-05-06 18:54:15 -0700677 rtc::AtomicOps::Increment(&_webRTCSessionCount);
678 [self notifyDidStartPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700679 return YES;
680}
681
682- (BOOL)endWebRTCSession:(NSError **)outError {
683 if (outError) {
684 *outError = nil;
685 }
686 if (![self checkLock:outError]) {
687 return NO;
688 }
tkchind2511962016-05-06 18:54:15 -0700689 rtc::AtomicOps::Decrement(&_webRTCSessionCount);
690 [self notifyDidStopPlayOrRecord];
691 return YES;
692}
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700693
tkchind2511962016-05-06 18:54:15 -0700694- (BOOL)configureWebRTCSession:(NSError **)outError {
695 if (outError) {
696 *outError = nil;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700697 }
tkchind2511962016-05-06 18:54:15 -0700698 if (![self checkLock:outError]) {
699 return NO;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700700 }
tkchind2511962016-05-06 18:54:15 -0700701 RTCLog(@"Configuring audio session for WebRTC.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700702
tkchind2511962016-05-06 18:54:15 -0700703 // Configure the AVAudioSession and activate it.
704 // Provide an error even if there isn't one so we can log it.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700705 NSError *error = nil;
tkchind2511962016-05-06 18:54:15 -0700706 RTCAudioSessionConfiguration *webRTCConfig =
707 [RTCAudioSessionConfiguration webRTCConfiguration];
708 if (![self setConfiguration:webRTCConfig active:YES error:&error]) {
709 RTCLogError(@"Failed to set WebRTC audio configuration: %@",
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700710 error.localizedDescription);
jttehf84c1d62017-04-21 13:56:39 -0700711 // Do not call setActive:NO if setActive:YES failed.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700712 if (outError) {
713 *outError = error;
714 }
715 return NO;
716 }
717
tkchind2511962016-05-06 18:54:15 -0700718 // Ensure that the device currently supports audio input.
719 // TODO(tkchin): Figure out if this is really necessary.
720 if (!self.inputAvailable) {
721 RTCLogError(@"No audio input path is available!");
722 [self unconfigureWebRTCSession:nil];
723 if (outError) {
724 *outError = [self configurationErrorWithDescription:@"No input path."];
725 }
726 return NO;
727 }
728
henrika2d014be2016-06-16 14:26:55 +0200729 // It can happen (e.g. in combination with BT devices) that the attempt to set
730 // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new
731 // configuration attempt using the sample rate that worked using the active
732 // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in
733 // combination with BT headsets. Using this "trick" seems to avoid a state
734 // where Core Audio asks for a different number of audio frames than what the
735 // session's I/O buffer duration corresponds to.
736 // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been
737 // tested on a limited set of iOS devices and BT devices.
738 double sessionSampleRate = self.sampleRate;
739 double preferredSampleRate = webRTCConfig.sampleRate;
740 if (sessionSampleRate != preferredSampleRate) {
741 RTCLogWarning(
742 @"Current sample rate (%.2f) is not the preferred rate (%.2f)",
743 sessionSampleRate, preferredSampleRate);
744 if (![self setPreferredSampleRate:sessionSampleRate
745 error:&error]) {
746 RTCLogError(@"Failed to set preferred sample rate: %@",
747 error.localizedDescription);
748 if (outError) {
749 *outError = error;
750 }
751 }
752 }
753
tkchind2511962016-05-06 18:54:15 -0700754 return YES;
755}
756
757- (BOOL)unconfigureWebRTCSession:(NSError **)outError {
758 if (outError) {
759 *outError = nil;
760 }
761 if (![self checkLock:outError]) {
762 return NO;
763 }
764 RTCLog(@"Unconfiguring audio session for WebRTC.");
765 [self setActive:NO error:outError];
766
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700767 return YES;
768}
769
770- (NSError *)configurationErrorWithDescription:(NSString *)description {
771 NSDictionary* userInfo = @{
772 NSLocalizedDescriptionKey: description,
773 };
774 return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
775 code:kRTCAudioSessionErrorConfiguration
776 userInfo:userInfo];
777}
778
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800779- (void)updateAudioSessionAfterEvent {
780 BOOL shouldActivate = self.activationCount > 0;
781 AVAudioSessionSetActiveOptions options = shouldActivate ?
782 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
783 NSError *error = nil;
784 if ([self.session setActive:shouldActivate
785 withOptions:options
786 error:&error]) {
787 self.isActive = shouldActivate;
788 } else {
789 RTCLogError(@"Failed to set session active to %d. Error:%@",
790 shouldActivate, error.localizedDescription);
791 }
792}
793
tkchind2511962016-05-06 18:54:15 -0700794- (void)updateCanPlayOrRecord {
795 BOOL canPlayOrRecord = NO;
796 BOOL shouldNotify = NO;
797 @synchronized(self) {
798 canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled;
799 if (_canPlayOrRecord == canPlayOrRecord) {
800 return;
801 }
802 _canPlayOrRecord = canPlayOrRecord;
803 shouldNotify = YES;
804 }
805 if (shouldNotify) {
806 [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord];
807 }
808}
809
jtteh3c9a6c02017-04-18 09:09:35 -0700810- (void)audioSessionDidActivate:(AVAudioSession *)session {
811 if (_session != session) {
812 RTCLogError(@"audioSessionDidActivate called on different AVAudioSession");
813 }
Zeke Chin8280a562018-07-10 13:53:55 -0700814 RTCLog(@"Audio session was externally activated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700815 [self incrementActivationCount];
816 self.isActive = YES;
Zeke Chin8280a562018-07-10 13:53:55 -0700817 // When a CallKit call begins, it's possible that we receive an interruption
818 // begin without a corresponding end. Since we know that we have an activated
819 // audio session at this point, just clear any saved interruption flag since
820 // the app may never be foregrounded during the duration of the call.
821 if (self.isInterrupted) {
822 RTCLog(@"Clearing interrupted state due to external activation.");
823 self.isInterrupted = NO;
824 }
825 // Treat external audio session activation as an end interruption event.
826 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
jtteh3c9a6c02017-04-18 09:09:35 -0700827}
828
829- (void)audioSessionDidDeactivate:(AVAudioSession *)session {
830 if (_session != session) {
831 RTCLogError(@"audioSessionDidDeactivate called on different AVAudioSession");
832 }
Zeke Chin8280a562018-07-10 13:53:55 -0700833 RTCLog(@"Audio session was externally deactivated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700834 self.isActive = NO;
835 [self decrementActivationCount];
836}
837
jtteh13ae11a2017-05-25 17:52:20 -0700838- (void)observeValueForKeyPath:(NSString *)keyPath
839 ofObject:(id)object
840 change:(NSDictionary *)change
841 context:(void *)context {
Peter Hanspers47217362017-10-05 11:39:15 +0200842 if (context == (__bridge void*)RTCAudioSession.class) {
843 if (object == _session) {
844 NSNumber *newVolume = change[NSKeyValueChangeNewKey];
845 RTCLog(@"OutputVolumeDidChange to %f", newVolume.floatValue);
846 [self notifyDidChangeOutputVolume:newVolume.floatValue];
847 }
jtteh13ae11a2017-05-25 17:52:20 -0700848 } else {
849 [super observeValueForKeyPath:keyPath
850 ofObject:object
851 change:change
852 context:context];
853 }
854}
855
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800856- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700857 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700858 SEL sel = @selector(audioSessionDidBeginInterruption:);
859 if ([delegate respondsToSelector:sel]) {
860 [delegate audioSessionDidBeginInterruption:self];
861 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800862 }
863}
864
865- (void)notifyDidEndInterruptionWithShouldResumeSession:
866 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700867 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700868 SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:);
869 if ([delegate respondsToSelector:sel]) {
870 [delegate audioSessionDidEndInterruption:self
871 shouldResumeSession:shouldResumeSession];
872 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800873 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800874}
875
876- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
877 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700878 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700879 SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:);
880 if ([delegate respondsToSelector:sel]) {
881 [delegate audioSessionDidChangeRoute:self
882 reason:reason
883 previousRoute:previousRoute];
884 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800885 }
886}
887
888- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700889 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800890 SEL sel = @selector(audioSessionMediaServerTerminated:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700891 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800892 [delegate audioSessionMediaServerTerminated:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700893 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800894 }
895}
896
897- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700898 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800899 SEL sel = @selector(audioSessionMediaServerReset:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700900 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800901 [delegate audioSessionMediaServerReset:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700902 }
903 }
904}
905
tkchind2511962016-05-06 18:54:15 -0700906- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700907 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700908 SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700909 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700910 [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700911 }
912 }
913}
914
tkchind2511962016-05-06 18:54:15 -0700915- (void)notifyDidStartPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700916 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700917 SEL sel = @selector(audioSessionDidStartPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700918 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700919 [delegate audioSessionDidStartPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700920 }
921 }
922}
923
tkchind2511962016-05-06 18:54:15 -0700924- (void)notifyDidStopPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700925 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700926 SEL sel = @selector(audioSessionDidStopPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700927 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700928 [delegate audioSessionDidStopPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700929 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800930 }
931}
932
jtteh13ae11a2017-05-25 17:52:20 -0700933- (void)notifyDidChangeOutputVolume:(float)volume {
934 for (auto delegate : self.delegates) {
935 SEL sel = @selector(audioSession:didChangeOutputVolume:);
936 if ([delegate respondsToSelector:sel]) {
937 [delegate audioSession:self didChangeOutputVolume:volume];
938 }
939 }
940}
941
Anders Carlsson121ea322017-06-26 15:34:47 +0200942- (void)notifyDidDetectPlayoutGlitch:(int64_t)totalNumberOfGlitches {
943 for (auto delegate : self.delegates) {
944 SEL sel = @selector(audioSession:didDetectPlayoutGlitch:);
945 if ([delegate respondsToSelector:sel]) {
946 [delegate audioSession:self didDetectPlayoutGlitch:totalNumberOfGlitches];
947 }
948 }
949}
950
JT Tehc1f083d2018-04-25 09:19:35 -0700951- (void)notifyWillSetActive:(BOOL)active {
952 for (id delegate : self.delegates) {
953 SEL sel = @selector(audioSession:willSetActive:);
954 if ([delegate respondsToSelector:sel]) {
955 [delegate audioSession:self willSetActive:active];
956 }
957 }
958}
959
960- (void)notifyDidSetActive:(BOOL)active {
961 for (id delegate : self.delegates) {
962 SEL sel = @selector(audioSession:didSetActive:);
963 if ([delegate respondsToSelector:sel]) {
964 [delegate audioSession:self didSetActive:active];
965 }
966 }
967}
968
969- (void)notifyFailedToSetActive:(BOOL)active error:(NSError *)error {
970 for (id delegate : self.delegates) {
971 SEL sel = @selector(audioSession:failedToSetActive:error:);
972 if ([delegate respondsToSelector:sel]) {
973 [delegate audioSession:self failedToSetActive:active error:error];
974 }
975 }
976}
977
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800978@end