blob: 1b2b487c5d309ce81ecef269583fad7b9507498f [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
Anders Carlsson7bca8ca2018-08-30 09:30:29 +020011#import "RTCAudioSession+Private.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080012
tkchin93dd6342016-07-27 10:17:14 -070013#import <UIKit/UIKit.h>
14
Steve Anton10542f22019-01-11 09:11:00 -080015#include "rtc_base/atomic_ops.h"
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020016#include "rtc_base/checks.h"
Steve Anton10542f22019-01-11 09:11:00 -080017#include "rtc_base/critical_section.h"
denicija59ee91b2017-06-05 05:48:47 -070018
Anders Carlsson7bca8ca2018-08-30 09:30:29 +020019#import "RTCAudioSessionConfiguration.h"
20#import "base/RTCLogging.h"
denicija59ee91b2017-06-05 05:48:47 -070021
Zeke Chinb3fb71c2016-02-18 15:44:07 -080022
23NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
24NSInteger const kRTCAudioSessionErrorLockRequired = -1;
tkchin9f987d32016-03-12 20:06:28 -080025NSInteger const kRTCAudioSessionErrorConfiguration = -2;
jtteh13ae11a2017-05-25 17:52:20 -070026NSString * const kRTCAudioSessionOutputVolumeSelector = @"outputVolume";
Zeke Chinb3fb71c2016-02-18 15:44:07 -080027
28// This class needs to be thread-safe because it is accessed from many threads.
29// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
30// lock contention so coarse locks should be fine for now.
31@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080032 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080033 AVAudioSession *_session;
Tze Kwang Chin307a0922016-03-21 13:57:40 -070034 volatile int _activationCount;
35 volatile int _lockRecursionCount;
36 volatile int _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080037 BOOL _isActive;
tkchind2511962016-05-06 18:54:15 -070038 BOOL _useManualAudio;
39 BOOL _isAudioEnabled;
40 BOOL _canPlayOrRecord;
tkchin93dd6342016-07-27 10:17:14 -070041 BOOL _isInterrupted;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080042}
43
44@synthesize session = _session;
tkchine54467f2016-03-15 16:54:03 -070045@synthesize delegates = _delegates;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080046
47+ (instancetype)sharedInstance {
48 static dispatch_once_t onceToken;
49 static RTCAudioSession *sharedInstance = nil;
50 dispatch_once(&onceToken, ^{
Tze Kwang Chin307a0922016-03-21 13:57:40 -070051 sharedInstance = [[self alloc] init];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080052 });
53 return sharedInstance;
54}
55
56- (instancetype)init {
Peter Hanspers47217362017-10-05 11:39:15 +020057 return [self initWithAudioSession:[AVAudioSession sharedInstance]];
58}
59
60/** This initializer provides a way for unit tests to inject a fake/mock audio session. */
61- (instancetype)initWithAudioSession:(id)audioSession {
Zeke Chinb3fb71c2016-02-18 15:44:07 -080062 if (self = [super init]) {
Peter Hanspers47217362017-10-05 11:39:15 +020063 _session = audioSession;
tkchin0ce3bf92016-03-12 16:52:04 -080064
Zeke Chinb3fb71c2016-02-18 15:44:07 -080065 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
66 [center addObserver:self
67 selector:@selector(handleInterruptionNotification:)
68 name:AVAudioSessionInterruptionNotification
69 object:nil];
70 [center addObserver:self
71 selector:@selector(handleRouteChangeNotification:)
72 name:AVAudioSessionRouteChangeNotification
73 object:nil];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080074 [center addObserver:self
75 selector:@selector(handleMediaServicesWereLost:)
76 name:AVAudioSessionMediaServicesWereLostNotification
77 object:nil];
78 [center addObserver:self
79 selector:@selector(handleMediaServicesWereReset:)
80 name:AVAudioSessionMediaServicesWereResetNotification
81 object:nil];
henrikaf1363fd2016-09-27 06:06:44 -070082 // Posted on the main thread when the primary audio from other applications
83 // starts and stops. Foreground applications may use this notification as a
84 // hint to enable or disable audio that is secondary.
85 [center addObserver:self
86 selector:@selector(handleSilenceSecondaryAudioHintNotification:)
87 name:AVAudioSessionSilenceSecondaryAudioHintNotification
88 object:nil];
tkchin93dd6342016-07-27 10:17:14 -070089 // Also track foreground event in order to deal with interruption ended situation.
90 [center addObserver:self
91 selector:@selector(handleApplicationDidBecomeActive:)
92 name:UIApplicationDidBecomeActiveNotification
93 object:nil];
jtteh13ae11a2017-05-25 17:52:20 -070094 [_session addObserver:self
95 forKeyPath:kRTCAudioSessionOutputVolumeSelector
96 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
Peter Hanspers47217362017-10-05 11:39:15 +020097 context:(__bridge void*)RTCAudioSession.class];
jtteh13ae11a2017-05-25 17:52:20 -070098
haysc7735b1e2017-03-29 14:53:32 -070099 RTCLog(@"RTCAudioSession (%p): init.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800100 }
101 return self;
102}
103
104- (void)dealloc {
105 [[NSNotificationCenter defaultCenter] removeObserver:self];
Peter Hanspers47217362017-10-05 11:39:15 +0200106 [_session removeObserver:self
107 forKeyPath:kRTCAudioSessionOutputVolumeSelector
108 context:(__bridge void*)RTCAudioSession.class];
haysc7735b1e2017-03-29 14:53:32 -0700109 RTCLog(@"RTCAudioSession (%p): dealloc.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800110}
111
Zeke Chin1300caa2016-03-18 14:39:11 -0700112- (NSString *)description {
113 NSString *format =
114 @"RTCAudioSession: {\n"
tkchind2511962016-05-06 18:54:15 -0700115 " category: %@\n"
116 " categoryOptions: %ld\n"
117 " mode: %@\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700118 " isActive: %d\n"
119 " sampleRate: %.2f\n"
120 " IOBufferDuration: %f\n"
121 " outputNumberOfChannels: %ld\n"
122 " inputNumberOfChannels: %ld\n"
123 " outputLatency: %f\n"
124 " inputLatency: %f\n"
henrikac5aea652016-09-21 07:45:55 -0700125 " outputVolume: %f\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700126 "}";
127 NSString *description = [NSString stringWithFormat:format,
tkchind2511962016-05-06 18:54:15 -0700128 self.category, (long)self.categoryOptions, self.mode,
Zeke Chin1300caa2016-03-18 14:39:11 -0700129 self.isActive, self.sampleRate, self.IOBufferDuration,
130 self.outputNumberOfChannels, self.inputNumberOfChannels,
henrikac5aea652016-09-21 07:45:55 -0700131 self.outputLatency, self.inputLatency, self.outputVolume];
Zeke Chin1300caa2016-03-18 14:39:11 -0700132 return description;
133}
134
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800135- (void)setIsActive:(BOOL)isActive {
136 @synchronized(self) {
137 _isActive = isActive;
138 }
139}
140
141- (BOOL)isActive {
142 @synchronized(self) {
143 return _isActive;
144 }
145}
146
147- (BOOL)isLocked {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700148 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800149}
150
tkchind2511962016-05-06 18:54:15 -0700151- (void)setUseManualAudio:(BOOL)useManualAudio {
tkchin9f987d32016-03-12 20:06:28 -0800152 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700153 if (_useManualAudio == useManualAudio) {
tkchin9f987d32016-03-12 20:06:28 -0800154 return;
155 }
tkchind2511962016-05-06 18:54:15 -0700156 _useManualAudio = useManualAudio;
157 }
158 [self updateCanPlayOrRecord];
159}
160
161- (BOOL)useManualAudio {
162 @synchronized(self) {
163 return _useManualAudio;
tkchin9f987d32016-03-12 20:06:28 -0800164 }
165}
166
tkchind2511962016-05-06 18:54:15 -0700167- (void)setIsAudioEnabled:(BOOL)isAudioEnabled {
tkchin9f987d32016-03-12 20:06:28 -0800168 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700169 if (_isAudioEnabled == isAudioEnabled) {
170 return;
171 }
172 _isAudioEnabled = isAudioEnabled;
173 }
174 [self updateCanPlayOrRecord];
175}
176
177- (BOOL)isAudioEnabled {
178 @synchronized(self) {
179 return _isAudioEnabled;
tkchin9f987d32016-03-12 20:06:28 -0800180 }
181}
182
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700183// TODO(tkchin): Check for duplicates.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800184- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700185 RTCLog(@"Adding delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700186 if (!delegate) {
187 return;
188 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800189 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700190 _delegates.push_back(delegate);
191 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800192 }
193}
194
195- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700196 RTCLog(@"Removing delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700197 if (!delegate) {
198 return;
199 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800200 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700201 _delegates.erase(std::remove(_delegates.begin(),
202 _delegates.end(),
tkchinefdd9302016-04-11 12:00:59 -0700203 delegate),
204 _delegates.end());
tkchine54467f2016-03-15 16:54:03 -0700205 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800206 }
207}
208
kthelgasonee8b8612017-03-31 04:50:27 -0700209#pragma clang diagnostic push
210#pragma clang diagnostic ignored "-Wthread-safety-analysis"
211
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800212- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800213 _crit.Enter();
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700214 rtc::AtomicOps::Increment(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800215}
216
217- (void)unlockForConfiguration {
218 // Don't let threads other than the one that called lockForConfiguration
219 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800220 if (_crit.TryEnter()) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700221 rtc::AtomicOps::Decrement(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800222 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800223 // was called without anyone calling lock, we will hit an assertion.
224 _crit.Leave();
225 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800226 }
227}
228
kthelgasonee8b8612017-03-31 04:50:27 -0700229#pragma clang diagnostic pop
230
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800231#pragma mark - AVAudioSession proxy methods
232
233- (NSString *)category {
234 return self.session.category;
235}
236
237- (AVAudioSessionCategoryOptions)categoryOptions {
238 return self.session.categoryOptions;
239}
240
241- (NSString *)mode {
242 return self.session.mode;
243}
244
245- (BOOL)secondaryAudioShouldBeSilencedHint {
246 return self.session.secondaryAudioShouldBeSilencedHint;
247}
248
249- (AVAudioSessionRouteDescription *)currentRoute {
250 return self.session.currentRoute;
251}
252
253- (NSInteger)maximumInputNumberOfChannels {
254 return self.session.maximumInputNumberOfChannels;
255}
256
257- (NSInteger)maximumOutputNumberOfChannels {
258 return self.session.maximumOutputNumberOfChannels;
259}
260
261- (float)inputGain {
262 return self.session.inputGain;
263}
264
265- (BOOL)inputGainSettable {
266 return self.session.inputGainSettable;
267}
268
269- (BOOL)inputAvailable {
270 return self.session.inputAvailable;
271}
272
273- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
274 return self.session.inputDataSources;
275}
276
277- (AVAudioSessionDataSourceDescription *)inputDataSource {
278 return self.session.inputDataSource;
279}
280
281- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
282 return self.session.outputDataSources;
283}
284
285- (AVAudioSessionDataSourceDescription *)outputDataSource {
286 return self.session.outputDataSource;
287}
288
289- (double)sampleRate {
290 return self.session.sampleRate;
291}
292
tkchind2511962016-05-06 18:54:15 -0700293- (double)preferredSampleRate {
294 return self.session.preferredSampleRate;
295}
296
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800297- (NSInteger)inputNumberOfChannels {
298 return self.session.inputNumberOfChannels;
299}
300
301- (NSInteger)outputNumberOfChannels {
302 return self.session.outputNumberOfChannels;
303}
304
305- (float)outputVolume {
306 return self.session.outputVolume;
307}
308
309- (NSTimeInterval)inputLatency {
310 return self.session.inputLatency;
311}
312
313- (NSTimeInterval)outputLatency {
314 return self.session.outputLatency;
315}
316
317- (NSTimeInterval)IOBufferDuration {
318 return self.session.IOBufferDuration;
319}
320
tkchind2511962016-05-06 18:54:15 -0700321- (NSTimeInterval)preferredIOBufferDuration {
322 return self.session.preferredIOBufferDuration;
323}
324
tkchine54467f2016-03-15 16:54:03 -0700325// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
326// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800327- (BOOL)setActive:(BOOL)active
328 error:(NSError **)outError {
329 if (![self checkLock:outError]) {
330 return NO;
331 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700332 int activationCount = _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800333 if (!active && activationCount == 0) {
334 RTCLogWarning(@"Attempting to deactivate without prior activation.");
335 }
JT Tehc1f083d2018-04-25 09:19:35 -0700336 [self notifyWillSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800337 BOOL success = YES;
338 BOOL isActive = self.isActive;
339 // Keep a local error so we can log it.
340 NSError *error = nil;
341 BOOL shouldSetActive =
342 (active && !isActive) || (!active && isActive && activationCount == 1);
343 // Attempt to activate if we're not active.
344 // Attempt to deactivate if we're active and it's the last unbalanced call.
345 if (shouldSetActive) {
346 AVAudioSession *session = self.session;
347 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
348 // that other audio sessions that were interrupted by our session can return
349 // to their active state. It is recommended for VoIP apps to use this
350 // option.
351 AVAudioSessionSetActiveOptions options =
352 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
353 success = [session setActive:active
354 withOptions:options
355 error:&error];
356 if (outError) {
357 *outError = error;
358 }
359 }
360 if (success) {
361 if (shouldSetActive) {
362 self.isActive = active;
363 }
364 if (active) {
365 [self incrementActivationCount];
366 }
JT Tehc1f083d2018-04-25 09:19:35 -0700367 [self notifyDidSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800368 } else {
tkchin9f987d32016-03-12 20:06:28 -0800369 RTCLogError(@"Failed to setActive:%d. Error: %@",
370 active, error.localizedDescription);
JT Tehc1f083d2018-04-25 09:19:35 -0700371 [self notifyFailedToSetActive:active error:error];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800372 }
373 // Decrement activation count on deactivation whether or not it succeeded.
374 if (!active) {
375 [self decrementActivationCount];
376 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700377 RTCLog(@"Number of current activations: %d", _activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800378 return success;
379}
380
381- (BOOL)setCategory:(NSString *)category
382 withOptions:(AVAudioSessionCategoryOptions)options
383 error:(NSError **)outError {
384 if (![self checkLock:outError]) {
385 return NO;
386 }
387 return [self.session setCategory:category withOptions:options error:outError];
388}
389
390- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
391 if (![self checkLock:outError]) {
392 return NO;
393 }
394 return [self.session setMode:mode error:outError];
395}
396
397- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
398 if (![self checkLock:outError]) {
399 return NO;
400 }
401 return [self.session setInputGain:gain error:outError];
402}
403
404- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
405 if (![self checkLock:outError]) {
406 return NO;
407 }
408 return [self.session setPreferredSampleRate:sampleRate error:outError];
409}
410
411- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
412 error:(NSError **)outError {
413 if (![self checkLock:outError]) {
414 return NO;
415 }
416 return [self.session setPreferredIOBufferDuration:duration error:outError];
417}
418
419- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
420 error:(NSError **)outError {
421 if (![self checkLock:outError]) {
422 return NO;
423 }
424 return [self.session setPreferredInputNumberOfChannels:count error:outError];
425}
426- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
427 error:(NSError **)outError {
428 if (![self checkLock:outError]) {
429 return NO;
430 }
431 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
432}
433
434- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
435 error:(NSError **)outError {
436 if (![self checkLock:outError]) {
437 return NO;
438 }
439 return [self.session overrideOutputAudioPort:portOverride error:outError];
440}
441
442- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
443 error:(NSError **)outError {
444 if (![self checkLock:outError]) {
445 return NO;
446 }
447 return [self.session setPreferredInput:inPort error:outError];
448}
449
450- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
451 error:(NSError **)outError {
452 if (![self checkLock:outError]) {
453 return NO;
454 }
455 return [self.session setInputDataSource:dataSource error:outError];
456}
457
458- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
459 error:(NSError **)outError {
460 if (![self checkLock:outError]) {
461 return NO;
462 }
463 return [self.session setOutputDataSource:dataSource error:outError];
464}
465
466#pragma mark - Notifications
467
468- (void)handleInterruptionNotification:(NSNotification *)notification {
469 NSNumber* typeNumber =
470 notification.userInfo[AVAudioSessionInterruptionTypeKey];
471 AVAudioSessionInterruptionType type =
472 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
473 switch (type) {
474 case AVAudioSessionInterruptionTypeBegan:
475 RTCLog(@"Audio session interruption began.");
476 self.isActive = NO;
tkchin93dd6342016-07-27 10:17:14 -0700477 self.isInterrupted = YES;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800478 [self notifyDidBeginInterruption];
479 break;
480 case AVAudioSessionInterruptionTypeEnded: {
481 RTCLog(@"Audio session interruption ended.");
tkchin93dd6342016-07-27 10:17:14 -0700482 self.isInterrupted = NO;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800483 [self updateAudioSessionAfterEvent];
484 NSNumber *optionsNumber =
485 notification.userInfo[AVAudioSessionInterruptionOptionKey];
486 AVAudioSessionInterruptionOptions options =
487 optionsNumber.unsignedIntegerValue;
488 BOOL shouldResume =
489 options & AVAudioSessionInterruptionOptionShouldResume;
490 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
491 break;
492 }
493 }
494}
495
496- (void)handleRouteChangeNotification:(NSNotification *)notification {
497 // Get reason for current route change.
498 NSNumber* reasonNumber =
499 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
500 AVAudioSessionRouteChangeReason reason =
501 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
502 RTCLog(@"Audio route changed:");
503 switch (reason) {
504 case AVAudioSessionRouteChangeReasonUnknown:
505 RTCLog(@"Audio route changed: ReasonUnknown");
506 break;
507 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
508 RTCLog(@"Audio route changed: NewDeviceAvailable");
509 break;
510 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
511 RTCLog(@"Audio route changed: OldDeviceUnavailable");
512 break;
513 case AVAudioSessionRouteChangeReasonCategoryChange:
514 RTCLog(@"Audio route changed: CategoryChange to :%@",
515 self.session.category);
516 break;
517 case AVAudioSessionRouteChangeReasonOverride:
518 RTCLog(@"Audio route changed: Override");
519 break;
520 case AVAudioSessionRouteChangeReasonWakeFromSleep:
521 RTCLog(@"Audio route changed: WakeFromSleep");
522 break;
523 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
524 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
525 break;
526 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
527 RTCLog(@"Audio route changed: RouteConfigurationChange");
528 break;
529 }
530 AVAudioSessionRouteDescription* previousRoute =
531 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
532 // Log previous route configuration.
533 RTCLog(@"Previous route: %@\nCurrent route:%@",
534 previousRoute, self.session.currentRoute);
535 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
536}
537
538- (void)handleMediaServicesWereLost:(NSNotification *)notification {
539 RTCLog(@"Media services were lost.");
540 [self updateAudioSessionAfterEvent];
541 [self notifyMediaServicesWereLost];
542}
543
544- (void)handleMediaServicesWereReset:(NSNotification *)notification {
545 RTCLog(@"Media services were reset.");
546 [self updateAudioSessionAfterEvent];
547 [self notifyMediaServicesWereReset];
548}
549
henrikaf1363fd2016-09-27 06:06:44 -0700550- (void)handleSilenceSecondaryAudioHintNotification:(NSNotification *)notification {
551 // TODO(henrika): just adding logs here for now until we know if we are ever
552 // see this notification and might be affected by it or if further actions
553 // are required.
554 NSNumber *typeNumber =
555 notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey];
556 AVAudioSessionSilenceSecondaryAudioHintType type =
557 (AVAudioSessionSilenceSecondaryAudioHintType)typeNumber.unsignedIntegerValue;
558 switch (type) {
559 case AVAudioSessionSilenceSecondaryAudioHintTypeBegin:
560 RTCLog(@"Another application's primary audio has started.");
561 break;
562 case AVAudioSessionSilenceSecondaryAudioHintTypeEnd:
563 RTCLog(@"Another application's primary audio has stopped.");
564 break;
565 }
566}
567
tkchin93dd6342016-07-27 10:17:14 -0700568- (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
Zeke Chin8280a562018-07-10 13:53:55 -0700569 BOOL isInterrupted = self.isInterrupted;
haysc7735b1e2017-03-29 14:53:32 -0700570 RTCLog(@"Application became active after an interruption. Treating as interruption "
Zeke Chin8280a562018-07-10 13:53:55 -0700571 "end. isInterrupted changed from %d to 0.",
572 isInterrupted);
573 if (isInterrupted) {
tkchin93dd6342016-07-27 10:17:14 -0700574 self.isInterrupted = NO;
575 [self updateAudioSessionAfterEvent];
tkchin93dd6342016-07-27 10:17:14 -0700576 }
haysc7735b1e2017-03-29 14:53:32 -0700577 // Always treat application becoming active as an interruption end event.
578 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
tkchin93dd6342016-07-27 10:17:14 -0700579}
580
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800581#pragma mark - Private
582
583+ (NSError *)lockError {
584 NSDictionary *userInfo = @{
585 NSLocalizedDescriptionKey:
586 @"Must call lockForConfiguration before calling this method."
587 };
588 NSError *error =
589 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
590 code:kRTCAudioSessionErrorLockRequired
591 userInfo:userInfo];
592 return error;
593}
594
tkchine54467f2016-03-15 16:54:03 -0700595- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800596 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700597 // Note: this returns a copy.
598 return _delegates;
599 }
600}
601
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700602// TODO(tkchin): check for duplicates.
tkchine54467f2016-03-15 16:54:03 -0700603- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
604 @synchronized(self) {
605 _delegates.insert(_delegates.begin(), delegate);
606 }
607}
608
609- (void)removeZeroedDelegates {
610 @synchronized(self) {
tkchinefdd9302016-04-11 12:00:59 -0700611 _delegates.erase(
612 std::remove_if(_delegates.begin(),
613 _delegates.end(),
614 [](id delegate) -> bool { return delegate == nil; }),
615 _delegates.end());
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800616 }
617}
618
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700619- (int)activationCount {
620 return _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800621}
622
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700623- (int)incrementActivationCount {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800624 RTCLog(@"Incrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700625 return rtc::AtomicOps::Increment(&_activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800626}
627
628- (NSInteger)decrementActivationCount {
629 RTCLog(@"Decrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700630 return rtc::AtomicOps::Decrement(&_activationCount);
631}
632
633- (int)webRTCSessionCount {
634 return _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800635}
636
tkchind2511962016-05-06 18:54:15 -0700637- (BOOL)canPlayOrRecord {
638 return !self.useManualAudio || self.isAudioEnabled;
639}
640
tkchin93dd6342016-07-27 10:17:14 -0700641- (BOOL)isInterrupted {
642 @synchronized(self) {
643 return _isInterrupted;
644 }
645}
646
647- (void)setIsInterrupted:(BOOL)isInterrupted {
648 @synchronized(self) {
649 if (_isInterrupted == isInterrupted) {
650 return;
651 }
652 _isInterrupted = isInterrupted;
653 }
654}
655
tkchin9f987d32016-03-12 20:06:28 -0800656- (BOOL)checkLock:(NSError **)outError {
657 // Check ivar instead of trying to acquire lock so that we won't accidentally
658 // acquire lock if it hasn't already been called.
659 if (!self.isLocked) {
660 if (outError) {
661 *outError = [RTCAudioSession lockError];
662 }
663 return NO;
664 }
665 return YES;
666}
667
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700668- (BOOL)beginWebRTCSession:(NSError **)outError {
669 if (outError) {
670 *outError = nil;
671 }
672 if (![self checkLock:outError]) {
673 return NO;
674 }
tkchind2511962016-05-06 18:54:15 -0700675 rtc::AtomicOps::Increment(&_webRTCSessionCount);
676 [self notifyDidStartPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700677 return YES;
678}
679
680- (BOOL)endWebRTCSession:(NSError **)outError {
681 if (outError) {
682 *outError = nil;
683 }
684 if (![self checkLock:outError]) {
685 return NO;
686 }
tkchind2511962016-05-06 18:54:15 -0700687 rtc::AtomicOps::Decrement(&_webRTCSessionCount);
688 [self notifyDidStopPlayOrRecord];
689 return YES;
690}
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700691
tkchind2511962016-05-06 18:54:15 -0700692- (BOOL)configureWebRTCSession:(NSError **)outError {
693 if (outError) {
694 *outError = nil;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700695 }
tkchind2511962016-05-06 18:54:15 -0700696 if (![self checkLock:outError]) {
697 return NO;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700698 }
tkchind2511962016-05-06 18:54:15 -0700699 RTCLog(@"Configuring audio session for WebRTC.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700700
tkchind2511962016-05-06 18:54:15 -0700701 // Configure the AVAudioSession and activate it.
702 // Provide an error even if there isn't one so we can log it.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700703 NSError *error = nil;
tkchind2511962016-05-06 18:54:15 -0700704 RTCAudioSessionConfiguration *webRTCConfig =
705 [RTCAudioSessionConfiguration webRTCConfiguration];
706 if (![self setConfiguration:webRTCConfig active:YES error:&error]) {
707 RTCLogError(@"Failed to set WebRTC audio configuration: %@",
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700708 error.localizedDescription);
jttehf84c1d62017-04-21 13:56:39 -0700709 // Do not call setActive:NO if setActive:YES failed.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700710 if (outError) {
711 *outError = error;
712 }
713 return NO;
714 }
715
tkchind2511962016-05-06 18:54:15 -0700716 // Ensure that the device currently supports audio input.
717 // TODO(tkchin): Figure out if this is really necessary.
718 if (!self.inputAvailable) {
719 RTCLogError(@"No audio input path is available!");
720 [self unconfigureWebRTCSession:nil];
721 if (outError) {
722 *outError = [self configurationErrorWithDescription:@"No input path."];
723 }
724 return NO;
725 }
726
henrika2d014be2016-06-16 14:26:55 +0200727 // It can happen (e.g. in combination with BT devices) that the attempt to set
728 // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new
729 // configuration attempt using the sample rate that worked using the active
730 // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in
731 // combination with BT headsets. Using this "trick" seems to avoid a state
732 // where Core Audio asks for a different number of audio frames than what the
733 // session's I/O buffer duration corresponds to.
734 // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been
735 // tested on a limited set of iOS devices and BT devices.
736 double sessionSampleRate = self.sampleRate;
737 double preferredSampleRate = webRTCConfig.sampleRate;
738 if (sessionSampleRate != preferredSampleRate) {
739 RTCLogWarning(
740 @"Current sample rate (%.2f) is not the preferred rate (%.2f)",
741 sessionSampleRate, preferredSampleRate);
742 if (![self setPreferredSampleRate:sessionSampleRate
743 error:&error]) {
744 RTCLogError(@"Failed to set preferred sample rate: %@",
745 error.localizedDescription);
746 if (outError) {
747 *outError = error;
748 }
749 }
750 }
751
tkchind2511962016-05-06 18:54:15 -0700752 return YES;
753}
754
755- (BOOL)unconfigureWebRTCSession:(NSError **)outError {
756 if (outError) {
757 *outError = nil;
758 }
759 if (![self checkLock:outError]) {
760 return NO;
761 }
762 RTCLog(@"Unconfiguring audio session for WebRTC.");
763 [self setActive:NO error:outError];
764
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700765 return YES;
766}
767
768- (NSError *)configurationErrorWithDescription:(NSString *)description {
769 NSDictionary* userInfo = @{
770 NSLocalizedDescriptionKey: description,
771 };
772 return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
773 code:kRTCAudioSessionErrorConfiguration
774 userInfo:userInfo];
775}
776
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800777- (void)updateAudioSessionAfterEvent {
778 BOOL shouldActivate = self.activationCount > 0;
779 AVAudioSessionSetActiveOptions options = shouldActivate ?
780 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
781 NSError *error = nil;
782 if ([self.session setActive:shouldActivate
783 withOptions:options
784 error:&error]) {
785 self.isActive = shouldActivate;
786 } else {
787 RTCLogError(@"Failed to set session active to %d. Error:%@",
788 shouldActivate, error.localizedDescription);
789 }
790}
791
tkchind2511962016-05-06 18:54:15 -0700792- (void)updateCanPlayOrRecord {
793 BOOL canPlayOrRecord = NO;
794 BOOL shouldNotify = NO;
795 @synchronized(self) {
796 canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled;
797 if (_canPlayOrRecord == canPlayOrRecord) {
798 return;
799 }
800 _canPlayOrRecord = canPlayOrRecord;
801 shouldNotify = YES;
802 }
803 if (shouldNotify) {
804 [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord];
805 }
806}
807
jtteh3c9a6c02017-04-18 09:09:35 -0700808- (void)audioSessionDidActivate:(AVAudioSession *)session {
809 if (_session != session) {
810 RTCLogError(@"audioSessionDidActivate called on different AVAudioSession");
811 }
Zeke Chin8280a562018-07-10 13:53:55 -0700812 RTCLog(@"Audio session was externally activated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700813 [self incrementActivationCount];
814 self.isActive = YES;
Zeke Chin8280a562018-07-10 13:53:55 -0700815 // When a CallKit call begins, it's possible that we receive an interruption
816 // begin without a corresponding end. Since we know that we have an activated
817 // audio session at this point, just clear any saved interruption flag since
818 // the app may never be foregrounded during the duration of the call.
819 if (self.isInterrupted) {
820 RTCLog(@"Clearing interrupted state due to external activation.");
821 self.isInterrupted = NO;
822 }
823 // Treat external audio session activation as an end interruption event.
824 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
jtteh3c9a6c02017-04-18 09:09:35 -0700825}
826
827- (void)audioSessionDidDeactivate:(AVAudioSession *)session {
828 if (_session != session) {
829 RTCLogError(@"audioSessionDidDeactivate called on different AVAudioSession");
830 }
Zeke Chin8280a562018-07-10 13:53:55 -0700831 RTCLog(@"Audio session was externally deactivated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700832 self.isActive = NO;
833 [self decrementActivationCount];
834}
835
jtteh13ae11a2017-05-25 17:52:20 -0700836- (void)observeValueForKeyPath:(NSString *)keyPath
837 ofObject:(id)object
838 change:(NSDictionary *)change
839 context:(void *)context {
Peter Hanspers47217362017-10-05 11:39:15 +0200840 if (context == (__bridge void*)RTCAudioSession.class) {
841 if (object == _session) {
842 NSNumber *newVolume = change[NSKeyValueChangeNewKey];
843 RTCLog(@"OutputVolumeDidChange to %f", newVolume.floatValue);
844 [self notifyDidChangeOutputVolume:newVolume.floatValue];
845 }
jtteh13ae11a2017-05-25 17:52:20 -0700846 } else {
847 [super observeValueForKeyPath:keyPath
848 ofObject:object
849 change:change
850 context:context];
851 }
852}
853
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800854- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700855 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700856 SEL sel = @selector(audioSessionDidBeginInterruption:);
857 if ([delegate respondsToSelector:sel]) {
858 [delegate audioSessionDidBeginInterruption:self];
859 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800860 }
861}
862
863- (void)notifyDidEndInterruptionWithShouldResumeSession:
864 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700865 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700866 SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:);
867 if ([delegate respondsToSelector:sel]) {
868 [delegate audioSessionDidEndInterruption:self
869 shouldResumeSession:shouldResumeSession];
870 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800871 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800872}
873
874- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
875 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700876 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700877 SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:);
878 if ([delegate respondsToSelector:sel]) {
879 [delegate audioSessionDidChangeRoute:self
880 reason:reason
881 previousRoute:previousRoute];
882 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800883 }
884}
885
886- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700887 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800888 SEL sel = @selector(audioSessionMediaServerTerminated:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700889 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800890 [delegate audioSessionMediaServerTerminated:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700891 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800892 }
893}
894
895- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700896 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800897 SEL sel = @selector(audioSessionMediaServerReset:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700898 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800899 [delegate audioSessionMediaServerReset:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700900 }
901 }
902}
903
tkchind2511962016-05-06 18:54:15 -0700904- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700905 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700906 SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700907 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700908 [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700909 }
910 }
911}
912
tkchind2511962016-05-06 18:54:15 -0700913- (void)notifyDidStartPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700914 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700915 SEL sel = @selector(audioSessionDidStartPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700916 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700917 [delegate audioSessionDidStartPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700918 }
919 }
920}
921
tkchind2511962016-05-06 18:54:15 -0700922- (void)notifyDidStopPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700923 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700924 SEL sel = @selector(audioSessionDidStopPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700925 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700926 [delegate audioSessionDidStopPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700927 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800928 }
929}
930
jtteh13ae11a2017-05-25 17:52:20 -0700931- (void)notifyDidChangeOutputVolume:(float)volume {
932 for (auto delegate : self.delegates) {
933 SEL sel = @selector(audioSession:didChangeOutputVolume:);
934 if ([delegate respondsToSelector:sel]) {
935 [delegate audioSession:self didChangeOutputVolume:volume];
936 }
937 }
938}
939
Anders Carlsson121ea322017-06-26 15:34:47 +0200940- (void)notifyDidDetectPlayoutGlitch:(int64_t)totalNumberOfGlitches {
941 for (auto delegate : self.delegates) {
942 SEL sel = @selector(audioSession:didDetectPlayoutGlitch:);
943 if ([delegate respondsToSelector:sel]) {
944 [delegate audioSession:self didDetectPlayoutGlitch:totalNumberOfGlitches];
945 }
946 }
947}
948
JT Tehc1f083d2018-04-25 09:19:35 -0700949- (void)notifyWillSetActive:(BOOL)active {
950 for (id delegate : self.delegates) {
951 SEL sel = @selector(audioSession:willSetActive:);
952 if ([delegate respondsToSelector:sel]) {
953 [delegate audioSession:self willSetActive:active];
954 }
955 }
956}
957
958- (void)notifyDidSetActive:(BOOL)active {
959 for (id delegate : self.delegates) {
960 SEL sel = @selector(audioSession:didSetActive:);
961 if ([delegate respondsToSelector:sel]) {
962 [delegate audioSession:self didSetActive:active];
963 }
964 }
965}
966
967- (void)notifyFailedToSetActive:(BOOL)active error:(NSError *)error {
968 for (id delegate : self.delegates) {
969 SEL sel = @selector(audioSession:failedToSetActive:error:);
970 if ([delegate respondsToSelector:sel]) {
971 [delegate audioSession:self failedToSetActive:active error:error];
972 }
973 }
974}
975
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800976@end