blob: 09ffa16fcc90a305c9d0d1861d35f881ca71f4df [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;
Joe Chen0c05b1a2019-05-07 10:46:22 -070046@synthesize ignoresPreferredAttributeConfigurationErrors =
47 _ignoresPreferredAttributeConfigurationErrors;
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
Joe Chen0c05b1a2019-05-07 10:46:22 -0700185- (void)setIgnoresPreferredAttributeConfigurationErrors:
186 (BOOL)ignoresPreferredAttributeConfigurationErrors {
187 @synchronized(self) {
188 if (_ignoresPreferredAttributeConfigurationErrors ==
189 ignoresPreferredAttributeConfigurationErrors) {
190 return;
191 }
192 _ignoresPreferredAttributeConfigurationErrors = ignoresPreferredAttributeConfigurationErrors;
193 }
194}
195
196- (BOOL)ignoresPreferredAttributeConfigurationErrors {
197 @synchronized(self) {
198 return _ignoresPreferredAttributeConfigurationErrors;
199 }
200}
201
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700202// TODO(tkchin): Check for duplicates.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800203- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700204 RTCLog(@"Adding delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700205 if (!delegate) {
206 return;
207 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800208 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700209 _delegates.push_back(delegate);
210 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800211 }
212}
213
214- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700215 RTCLog(@"Removing delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700216 if (!delegate) {
217 return;
218 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800219 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700220 _delegates.erase(std::remove(_delegates.begin(),
221 _delegates.end(),
tkchinefdd9302016-04-11 12:00:59 -0700222 delegate),
223 _delegates.end());
tkchine54467f2016-03-15 16:54:03 -0700224 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800225 }
226}
227
kthelgasonee8b8612017-03-31 04:50:27 -0700228#pragma clang diagnostic push
229#pragma clang diagnostic ignored "-Wthread-safety-analysis"
230
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800231- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800232 _crit.Enter();
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700233 rtc::AtomicOps::Increment(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800234}
235
236- (void)unlockForConfiguration {
237 // Don't let threads other than the one that called lockForConfiguration
238 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800239 if (_crit.TryEnter()) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700240 rtc::AtomicOps::Decrement(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800241 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800242 // was called without anyone calling lock, we will hit an assertion.
243 _crit.Leave();
244 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800245 }
246}
247
kthelgasonee8b8612017-03-31 04:50:27 -0700248#pragma clang diagnostic pop
249
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800250#pragma mark - AVAudioSession proxy methods
251
252- (NSString *)category {
253 return self.session.category;
254}
255
256- (AVAudioSessionCategoryOptions)categoryOptions {
257 return self.session.categoryOptions;
258}
259
260- (NSString *)mode {
261 return self.session.mode;
262}
263
264- (BOOL)secondaryAudioShouldBeSilencedHint {
265 return self.session.secondaryAudioShouldBeSilencedHint;
266}
267
268- (AVAudioSessionRouteDescription *)currentRoute {
269 return self.session.currentRoute;
270}
271
272- (NSInteger)maximumInputNumberOfChannels {
273 return self.session.maximumInputNumberOfChannels;
274}
275
276- (NSInteger)maximumOutputNumberOfChannels {
277 return self.session.maximumOutputNumberOfChannels;
278}
279
280- (float)inputGain {
281 return self.session.inputGain;
282}
283
284- (BOOL)inputGainSettable {
285 return self.session.inputGainSettable;
286}
287
288- (BOOL)inputAvailable {
289 return self.session.inputAvailable;
290}
291
292- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
293 return self.session.inputDataSources;
294}
295
296- (AVAudioSessionDataSourceDescription *)inputDataSource {
297 return self.session.inputDataSource;
298}
299
300- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
301 return self.session.outputDataSources;
302}
303
304- (AVAudioSessionDataSourceDescription *)outputDataSource {
305 return self.session.outputDataSource;
306}
307
308- (double)sampleRate {
309 return self.session.sampleRate;
310}
311
tkchind2511962016-05-06 18:54:15 -0700312- (double)preferredSampleRate {
313 return self.session.preferredSampleRate;
314}
315
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800316- (NSInteger)inputNumberOfChannels {
317 return self.session.inputNumberOfChannels;
318}
319
320- (NSInteger)outputNumberOfChannels {
321 return self.session.outputNumberOfChannels;
322}
323
324- (float)outputVolume {
325 return self.session.outputVolume;
326}
327
328- (NSTimeInterval)inputLatency {
329 return self.session.inputLatency;
330}
331
332- (NSTimeInterval)outputLatency {
333 return self.session.outputLatency;
334}
335
336- (NSTimeInterval)IOBufferDuration {
337 return self.session.IOBufferDuration;
338}
339
tkchind2511962016-05-06 18:54:15 -0700340- (NSTimeInterval)preferredIOBufferDuration {
341 return self.session.preferredIOBufferDuration;
342}
343
tkchine54467f2016-03-15 16:54:03 -0700344// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
345// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800346- (BOOL)setActive:(BOOL)active
347 error:(NSError **)outError {
348 if (![self checkLock:outError]) {
349 return NO;
350 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700351 int activationCount = _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800352 if (!active && activationCount == 0) {
353 RTCLogWarning(@"Attempting to deactivate without prior activation.");
354 }
JT Tehc1f083d2018-04-25 09:19:35 -0700355 [self notifyWillSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800356 BOOL success = YES;
357 BOOL isActive = self.isActive;
358 // Keep a local error so we can log it.
359 NSError *error = nil;
360 BOOL shouldSetActive =
361 (active && !isActive) || (!active && isActive && activationCount == 1);
362 // Attempt to activate if we're not active.
363 // Attempt to deactivate if we're active and it's the last unbalanced call.
364 if (shouldSetActive) {
365 AVAudioSession *session = self.session;
366 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
367 // that other audio sessions that were interrupted by our session can return
368 // to their active state. It is recommended for VoIP apps to use this
369 // option.
370 AVAudioSessionSetActiveOptions options =
371 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
372 success = [session setActive:active
373 withOptions:options
374 error:&error];
375 if (outError) {
376 *outError = error;
377 }
378 }
379 if (success) {
380 if (shouldSetActive) {
381 self.isActive = active;
382 }
383 if (active) {
384 [self incrementActivationCount];
385 }
JT Tehc1f083d2018-04-25 09:19:35 -0700386 [self notifyDidSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800387 } else {
tkchin9f987d32016-03-12 20:06:28 -0800388 RTCLogError(@"Failed to setActive:%d. Error: %@",
389 active, error.localizedDescription);
JT Tehc1f083d2018-04-25 09:19:35 -0700390 [self notifyFailedToSetActive:active error:error];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800391 }
392 // Decrement activation count on deactivation whether or not it succeeded.
393 if (!active) {
394 [self decrementActivationCount];
395 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700396 RTCLog(@"Number of current activations: %d", _activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800397 return success;
398}
399
400- (BOOL)setCategory:(NSString *)category
401 withOptions:(AVAudioSessionCategoryOptions)options
402 error:(NSError **)outError {
403 if (![self checkLock:outError]) {
404 return NO;
405 }
406 return [self.session setCategory:category withOptions:options error:outError];
407}
408
409- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
410 if (![self checkLock:outError]) {
411 return NO;
412 }
413 return [self.session setMode:mode error:outError];
414}
415
416- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
417 if (![self checkLock:outError]) {
418 return NO;
419 }
420 return [self.session setInputGain:gain error:outError];
421}
422
423- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
424 if (![self checkLock:outError]) {
425 return NO;
426 }
427 return [self.session setPreferredSampleRate:sampleRate error:outError];
428}
429
430- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
431 error:(NSError **)outError {
432 if (![self checkLock:outError]) {
433 return NO;
434 }
435 return [self.session setPreferredIOBufferDuration:duration error:outError];
436}
437
438- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
439 error:(NSError **)outError {
440 if (![self checkLock:outError]) {
441 return NO;
442 }
443 return [self.session setPreferredInputNumberOfChannels:count error:outError];
444}
445- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
446 error:(NSError **)outError {
447 if (![self checkLock:outError]) {
448 return NO;
449 }
450 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
451}
452
453- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
454 error:(NSError **)outError {
455 if (![self checkLock:outError]) {
456 return NO;
457 }
458 return [self.session overrideOutputAudioPort:portOverride error:outError];
459}
460
461- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
462 error:(NSError **)outError {
463 if (![self checkLock:outError]) {
464 return NO;
465 }
466 return [self.session setPreferredInput:inPort error:outError];
467}
468
469- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
470 error:(NSError **)outError {
471 if (![self checkLock:outError]) {
472 return NO;
473 }
474 return [self.session setInputDataSource:dataSource error:outError];
475}
476
477- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
478 error:(NSError **)outError {
479 if (![self checkLock:outError]) {
480 return NO;
481 }
482 return [self.session setOutputDataSource:dataSource error:outError];
483}
484
485#pragma mark - Notifications
486
487- (void)handleInterruptionNotification:(NSNotification *)notification {
488 NSNumber* typeNumber =
489 notification.userInfo[AVAudioSessionInterruptionTypeKey];
490 AVAudioSessionInterruptionType type =
491 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
492 switch (type) {
493 case AVAudioSessionInterruptionTypeBegan:
494 RTCLog(@"Audio session interruption began.");
495 self.isActive = NO;
tkchin93dd6342016-07-27 10:17:14 -0700496 self.isInterrupted = YES;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800497 [self notifyDidBeginInterruption];
498 break;
499 case AVAudioSessionInterruptionTypeEnded: {
500 RTCLog(@"Audio session interruption ended.");
tkchin93dd6342016-07-27 10:17:14 -0700501 self.isInterrupted = NO;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800502 [self updateAudioSessionAfterEvent];
503 NSNumber *optionsNumber =
504 notification.userInfo[AVAudioSessionInterruptionOptionKey];
505 AVAudioSessionInterruptionOptions options =
506 optionsNumber.unsignedIntegerValue;
507 BOOL shouldResume =
508 options & AVAudioSessionInterruptionOptionShouldResume;
509 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
510 break;
511 }
512 }
513}
514
515- (void)handleRouteChangeNotification:(NSNotification *)notification {
516 // Get reason for current route change.
517 NSNumber* reasonNumber =
518 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
519 AVAudioSessionRouteChangeReason reason =
520 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
521 RTCLog(@"Audio route changed:");
522 switch (reason) {
523 case AVAudioSessionRouteChangeReasonUnknown:
524 RTCLog(@"Audio route changed: ReasonUnknown");
525 break;
526 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
527 RTCLog(@"Audio route changed: NewDeviceAvailable");
528 break;
529 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
530 RTCLog(@"Audio route changed: OldDeviceUnavailable");
531 break;
532 case AVAudioSessionRouteChangeReasonCategoryChange:
533 RTCLog(@"Audio route changed: CategoryChange to :%@",
534 self.session.category);
535 break;
536 case AVAudioSessionRouteChangeReasonOverride:
537 RTCLog(@"Audio route changed: Override");
538 break;
539 case AVAudioSessionRouteChangeReasonWakeFromSleep:
540 RTCLog(@"Audio route changed: WakeFromSleep");
541 break;
542 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
543 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
544 break;
545 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
546 RTCLog(@"Audio route changed: RouteConfigurationChange");
547 break;
548 }
549 AVAudioSessionRouteDescription* previousRoute =
550 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
551 // Log previous route configuration.
552 RTCLog(@"Previous route: %@\nCurrent route:%@",
553 previousRoute, self.session.currentRoute);
554 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
555}
556
557- (void)handleMediaServicesWereLost:(NSNotification *)notification {
558 RTCLog(@"Media services were lost.");
559 [self updateAudioSessionAfterEvent];
560 [self notifyMediaServicesWereLost];
561}
562
563- (void)handleMediaServicesWereReset:(NSNotification *)notification {
564 RTCLog(@"Media services were reset.");
565 [self updateAudioSessionAfterEvent];
566 [self notifyMediaServicesWereReset];
567}
568
henrikaf1363fd2016-09-27 06:06:44 -0700569- (void)handleSilenceSecondaryAudioHintNotification:(NSNotification *)notification {
570 // TODO(henrika): just adding logs here for now until we know if we are ever
571 // see this notification and might be affected by it or if further actions
572 // are required.
573 NSNumber *typeNumber =
574 notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey];
575 AVAudioSessionSilenceSecondaryAudioHintType type =
576 (AVAudioSessionSilenceSecondaryAudioHintType)typeNumber.unsignedIntegerValue;
577 switch (type) {
578 case AVAudioSessionSilenceSecondaryAudioHintTypeBegin:
579 RTCLog(@"Another application's primary audio has started.");
580 break;
581 case AVAudioSessionSilenceSecondaryAudioHintTypeEnd:
582 RTCLog(@"Another application's primary audio has stopped.");
583 break;
584 }
585}
586
tkchin93dd6342016-07-27 10:17:14 -0700587- (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
Zeke Chin8280a562018-07-10 13:53:55 -0700588 BOOL isInterrupted = self.isInterrupted;
haysc7735b1e2017-03-29 14:53:32 -0700589 RTCLog(@"Application became active after an interruption. Treating as interruption "
Zeke Chin8280a562018-07-10 13:53:55 -0700590 "end. isInterrupted changed from %d to 0.",
591 isInterrupted);
592 if (isInterrupted) {
tkchin93dd6342016-07-27 10:17:14 -0700593 self.isInterrupted = NO;
594 [self updateAudioSessionAfterEvent];
tkchin93dd6342016-07-27 10:17:14 -0700595 }
haysc7735b1e2017-03-29 14:53:32 -0700596 // Always treat application becoming active as an interruption end event.
597 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
tkchin93dd6342016-07-27 10:17:14 -0700598}
599
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800600#pragma mark - Private
601
602+ (NSError *)lockError {
603 NSDictionary *userInfo = @{
604 NSLocalizedDescriptionKey:
605 @"Must call lockForConfiguration before calling this method."
606 };
607 NSError *error =
608 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
609 code:kRTCAudioSessionErrorLockRequired
610 userInfo:userInfo];
611 return error;
612}
613
tkchine54467f2016-03-15 16:54:03 -0700614- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800615 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700616 // Note: this returns a copy.
617 return _delegates;
618 }
619}
620
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700621// TODO(tkchin): check for duplicates.
tkchine54467f2016-03-15 16:54:03 -0700622- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
623 @synchronized(self) {
624 _delegates.insert(_delegates.begin(), delegate);
625 }
626}
627
628- (void)removeZeroedDelegates {
629 @synchronized(self) {
tkchinefdd9302016-04-11 12:00:59 -0700630 _delegates.erase(
631 std::remove_if(_delegates.begin(),
632 _delegates.end(),
633 [](id delegate) -> bool { return delegate == nil; }),
634 _delegates.end());
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800635 }
636}
637
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700638- (int)activationCount {
639 return _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800640}
641
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700642- (int)incrementActivationCount {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800643 RTCLog(@"Incrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700644 return rtc::AtomicOps::Increment(&_activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800645}
646
647- (NSInteger)decrementActivationCount {
648 RTCLog(@"Decrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700649 return rtc::AtomicOps::Decrement(&_activationCount);
650}
651
652- (int)webRTCSessionCount {
653 return _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800654}
655
tkchind2511962016-05-06 18:54:15 -0700656- (BOOL)canPlayOrRecord {
657 return !self.useManualAudio || self.isAudioEnabled;
658}
659
tkchin93dd6342016-07-27 10:17:14 -0700660- (BOOL)isInterrupted {
661 @synchronized(self) {
662 return _isInterrupted;
663 }
664}
665
666- (void)setIsInterrupted:(BOOL)isInterrupted {
667 @synchronized(self) {
668 if (_isInterrupted == isInterrupted) {
669 return;
670 }
671 _isInterrupted = isInterrupted;
672 }
673}
674
tkchin9f987d32016-03-12 20:06:28 -0800675- (BOOL)checkLock:(NSError **)outError {
676 // Check ivar instead of trying to acquire lock so that we won't accidentally
677 // acquire lock if it hasn't already been called.
678 if (!self.isLocked) {
679 if (outError) {
680 *outError = [RTCAudioSession lockError];
681 }
682 return NO;
683 }
684 return YES;
685}
686
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700687- (BOOL)beginWebRTCSession:(NSError **)outError {
688 if (outError) {
689 *outError = nil;
690 }
691 if (![self checkLock:outError]) {
692 return NO;
693 }
tkchind2511962016-05-06 18:54:15 -0700694 rtc::AtomicOps::Increment(&_webRTCSessionCount);
695 [self notifyDidStartPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700696 return YES;
697}
698
699- (BOOL)endWebRTCSession:(NSError **)outError {
700 if (outError) {
701 *outError = nil;
702 }
703 if (![self checkLock:outError]) {
704 return NO;
705 }
tkchind2511962016-05-06 18:54:15 -0700706 rtc::AtomicOps::Decrement(&_webRTCSessionCount);
707 [self notifyDidStopPlayOrRecord];
708 return YES;
709}
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700710
tkchind2511962016-05-06 18:54:15 -0700711- (BOOL)configureWebRTCSession:(NSError **)outError {
712 if (outError) {
713 *outError = nil;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700714 }
tkchind2511962016-05-06 18:54:15 -0700715 if (![self checkLock:outError]) {
716 return NO;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700717 }
tkchind2511962016-05-06 18:54:15 -0700718 RTCLog(@"Configuring audio session for WebRTC.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700719
tkchind2511962016-05-06 18:54:15 -0700720 // Configure the AVAudioSession and activate it.
721 // Provide an error even if there isn't one so we can log it.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700722 NSError *error = nil;
tkchind2511962016-05-06 18:54:15 -0700723 RTCAudioSessionConfiguration *webRTCConfig =
724 [RTCAudioSessionConfiguration webRTCConfiguration];
725 if (![self setConfiguration:webRTCConfig active:YES error:&error]) {
726 RTCLogError(@"Failed to set WebRTC audio configuration: %@",
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700727 error.localizedDescription);
jttehf84c1d62017-04-21 13:56:39 -0700728 // Do not call setActive:NO if setActive:YES failed.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700729 if (outError) {
730 *outError = error;
731 }
732 return NO;
733 }
734
tkchind2511962016-05-06 18:54:15 -0700735 // Ensure that the device currently supports audio input.
736 // TODO(tkchin): Figure out if this is really necessary.
737 if (!self.inputAvailable) {
738 RTCLogError(@"No audio input path is available!");
739 [self unconfigureWebRTCSession:nil];
740 if (outError) {
741 *outError = [self configurationErrorWithDescription:@"No input path."];
742 }
743 return NO;
744 }
745
henrika2d014be2016-06-16 14:26:55 +0200746 // It can happen (e.g. in combination with BT devices) that the attempt to set
747 // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new
748 // configuration attempt using the sample rate that worked using the active
749 // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in
750 // combination with BT headsets. Using this "trick" seems to avoid a state
751 // where Core Audio asks for a different number of audio frames than what the
752 // session's I/O buffer duration corresponds to.
753 // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been
754 // tested on a limited set of iOS devices and BT devices.
755 double sessionSampleRate = self.sampleRate;
756 double preferredSampleRate = webRTCConfig.sampleRate;
757 if (sessionSampleRate != preferredSampleRate) {
758 RTCLogWarning(
759 @"Current sample rate (%.2f) is not the preferred rate (%.2f)",
760 sessionSampleRate, preferredSampleRate);
761 if (![self setPreferredSampleRate:sessionSampleRate
762 error:&error]) {
763 RTCLogError(@"Failed to set preferred sample rate: %@",
764 error.localizedDescription);
765 if (outError) {
766 *outError = error;
767 }
768 }
769 }
770
tkchind2511962016-05-06 18:54:15 -0700771 return YES;
772}
773
774- (BOOL)unconfigureWebRTCSession:(NSError **)outError {
775 if (outError) {
776 *outError = nil;
777 }
778 if (![self checkLock:outError]) {
779 return NO;
780 }
781 RTCLog(@"Unconfiguring audio session for WebRTC.");
782 [self setActive:NO error:outError];
783
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700784 return YES;
785}
786
787- (NSError *)configurationErrorWithDescription:(NSString *)description {
788 NSDictionary* userInfo = @{
789 NSLocalizedDescriptionKey: description,
790 };
791 return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
792 code:kRTCAudioSessionErrorConfiguration
793 userInfo:userInfo];
794}
795
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800796- (void)updateAudioSessionAfterEvent {
797 BOOL shouldActivate = self.activationCount > 0;
798 AVAudioSessionSetActiveOptions options = shouldActivate ?
799 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
800 NSError *error = nil;
801 if ([self.session setActive:shouldActivate
802 withOptions:options
803 error:&error]) {
804 self.isActive = shouldActivate;
805 } else {
806 RTCLogError(@"Failed to set session active to %d. Error:%@",
807 shouldActivate, error.localizedDescription);
808 }
809}
810
tkchind2511962016-05-06 18:54:15 -0700811- (void)updateCanPlayOrRecord {
812 BOOL canPlayOrRecord = NO;
813 BOOL shouldNotify = NO;
814 @synchronized(self) {
815 canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled;
816 if (_canPlayOrRecord == canPlayOrRecord) {
817 return;
818 }
819 _canPlayOrRecord = canPlayOrRecord;
820 shouldNotify = YES;
821 }
822 if (shouldNotify) {
823 [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord];
824 }
825}
826
jtteh3c9a6c02017-04-18 09:09:35 -0700827- (void)audioSessionDidActivate:(AVAudioSession *)session {
828 if (_session != session) {
829 RTCLogError(@"audioSessionDidActivate called on different AVAudioSession");
830 }
Zeke Chin8280a562018-07-10 13:53:55 -0700831 RTCLog(@"Audio session was externally activated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700832 [self incrementActivationCount];
833 self.isActive = YES;
Zeke Chin8280a562018-07-10 13:53:55 -0700834 // When a CallKit call begins, it's possible that we receive an interruption
835 // begin without a corresponding end. Since we know that we have an activated
836 // audio session at this point, just clear any saved interruption flag since
837 // the app may never be foregrounded during the duration of the call.
838 if (self.isInterrupted) {
839 RTCLog(@"Clearing interrupted state due to external activation.");
840 self.isInterrupted = NO;
841 }
842 // Treat external audio session activation as an end interruption event.
843 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
jtteh3c9a6c02017-04-18 09:09:35 -0700844}
845
846- (void)audioSessionDidDeactivate:(AVAudioSession *)session {
847 if (_session != session) {
848 RTCLogError(@"audioSessionDidDeactivate called on different AVAudioSession");
849 }
Zeke Chin8280a562018-07-10 13:53:55 -0700850 RTCLog(@"Audio session was externally deactivated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700851 self.isActive = NO;
852 [self decrementActivationCount];
853}
854
jtteh13ae11a2017-05-25 17:52:20 -0700855- (void)observeValueForKeyPath:(NSString *)keyPath
856 ofObject:(id)object
857 change:(NSDictionary *)change
858 context:(void *)context {
Peter Hanspers47217362017-10-05 11:39:15 +0200859 if (context == (__bridge void*)RTCAudioSession.class) {
860 if (object == _session) {
861 NSNumber *newVolume = change[NSKeyValueChangeNewKey];
862 RTCLog(@"OutputVolumeDidChange to %f", newVolume.floatValue);
863 [self notifyDidChangeOutputVolume:newVolume.floatValue];
864 }
jtteh13ae11a2017-05-25 17:52:20 -0700865 } else {
866 [super observeValueForKeyPath:keyPath
867 ofObject:object
868 change:change
869 context:context];
870 }
871}
872
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800873- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700874 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700875 SEL sel = @selector(audioSessionDidBeginInterruption:);
876 if ([delegate respondsToSelector:sel]) {
877 [delegate audioSessionDidBeginInterruption:self];
878 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800879 }
880}
881
882- (void)notifyDidEndInterruptionWithShouldResumeSession:
883 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700884 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700885 SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:);
886 if ([delegate respondsToSelector:sel]) {
887 [delegate audioSessionDidEndInterruption:self
888 shouldResumeSession:shouldResumeSession];
889 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800890 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800891}
892
893- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
894 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700895 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700896 SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:);
897 if ([delegate respondsToSelector:sel]) {
898 [delegate audioSessionDidChangeRoute:self
899 reason:reason
900 previousRoute:previousRoute];
901 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800902 }
903}
904
905- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700906 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800907 SEL sel = @selector(audioSessionMediaServerTerminated:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700908 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800909 [delegate audioSessionMediaServerTerminated:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700910 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800911 }
912}
913
914- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700915 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800916 SEL sel = @selector(audioSessionMediaServerReset:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700917 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800918 [delegate audioSessionMediaServerReset:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700919 }
920 }
921}
922
tkchind2511962016-05-06 18:54:15 -0700923- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700924 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700925 SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700926 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700927 [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700928 }
929 }
930}
931
tkchind2511962016-05-06 18:54:15 -0700932- (void)notifyDidStartPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700933 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700934 SEL sel = @selector(audioSessionDidStartPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700935 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700936 [delegate audioSessionDidStartPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700937 }
938 }
939}
940
tkchind2511962016-05-06 18:54:15 -0700941- (void)notifyDidStopPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700942 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700943 SEL sel = @selector(audioSessionDidStopPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700944 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700945 [delegate audioSessionDidStopPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700946 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800947 }
948}
949
jtteh13ae11a2017-05-25 17:52:20 -0700950- (void)notifyDidChangeOutputVolume:(float)volume {
951 for (auto delegate : self.delegates) {
952 SEL sel = @selector(audioSession:didChangeOutputVolume:);
953 if ([delegate respondsToSelector:sel]) {
954 [delegate audioSession:self didChangeOutputVolume:volume];
955 }
956 }
957}
958
Anders Carlsson121ea322017-06-26 15:34:47 +0200959- (void)notifyDidDetectPlayoutGlitch:(int64_t)totalNumberOfGlitches {
960 for (auto delegate : self.delegates) {
961 SEL sel = @selector(audioSession:didDetectPlayoutGlitch:);
962 if ([delegate respondsToSelector:sel]) {
963 [delegate audioSession:self didDetectPlayoutGlitch:totalNumberOfGlitches];
964 }
965 }
966}
967
JT Tehc1f083d2018-04-25 09:19:35 -0700968- (void)notifyWillSetActive:(BOOL)active {
969 for (id delegate : self.delegates) {
970 SEL sel = @selector(audioSession:willSetActive:);
971 if ([delegate respondsToSelector:sel]) {
972 [delegate audioSession:self willSetActive:active];
973 }
974 }
975}
976
977- (void)notifyDidSetActive:(BOOL)active {
978 for (id delegate : self.delegates) {
979 SEL sel = @selector(audioSession:didSetActive:);
980 if ([delegate respondsToSelector:sel]) {
981 [delegate audioSession:self didSetActive:active];
982 }
983 }
984}
985
986- (void)notifyFailedToSetActive:(BOOL)active error:(NSError *)error {
987 for (id delegate : self.delegates) {
988 SEL sel = @selector(audioSession:failedToSetActive:error:);
989 if ([delegate respondsToSelector:sel]) {
990 [delegate audioSession:self failedToSetActive:active error:error];
991 }
992 }
993}
994
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800995@end