blob: 229a6ea1e49fc0e4f9640350e5d2799ac84def96 [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
Joe Chen0b3a6e32019-12-26 23:01:42 -080015#include <vector>
16
Steve Anton10542f22019-01-11 09:11:00 -080017#include "rtc_base/atomic_ops.h"
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020018#include "rtc_base/checks.h"
Steve Anton10542f22019-01-11 09:11:00 -080019#include "rtc_base/critical_section.h"
denicija59ee91b2017-06-05 05:48:47 -070020
Anders Carlsson7bca8ca2018-08-30 09:30:29 +020021#import "RTCAudioSessionConfiguration.h"
22#import "base/RTCLogging.h"
denicija59ee91b2017-06-05 05:48:47 -070023
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
Joe Chen0b3a6e32019-12-26 23:01:42 -080030@interface RTCAudioSession ()
31@property(nonatomic, readonly) std::vector<__weak id<RTCAudioSessionDelegate> > delegates;
32@end
33
Zeke Chinb3fb71c2016-02-18 15:44:07 -080034// This class needs to be thread-safe because it is accessed from many threads.
35// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
36// lock contention so coarse locks should be fine for now.
37@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080038 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080039 AVAudioSession *_session;
Tze Kwang Chin307a0922016-03-21 13:57:40 -070040 volatile int _activationCount;
41 volatile int _lockRecursionCount;
42 volatile int _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080043 BOOL _isActive;
tkchind2511962016-05-06 18:54:15 -070044 BOOL _useManualAudio;
45 BOOL _isAudioEnabled;
46 BOOL _canPlayOrRecord;
tkchin93dd6342016-07-27 10:17:14 -070047 BOOL _isInterrupted;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080048}
49
50@synthesize session = _session;
tkchine54467f2016-03-15 16:54:03 -070051@synthesize delegates = _delegates;
Joe Chen0c05b1a2019-05-07 10:46:22 -070052@synthesize ignoresPreferredAttributeConfigurationErrors =
53 _ignoresPreferredAttributeConfigurationErrors;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080054
55+ (instancetype)sharedInstance {
56 static dispatch_once_t onceToken;
57 static RTCAudioSession *sharedInstance = nil;
58 dispatch_once(&onceToken, ^{
Tze Kwang Chin307a0922016-03-21 13:57:40 -070059 sharedInstance = [[self alloc] init];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080060 });
61 return sharedInstance;
62}
63
64- (instancetype)init {
Peter Hanspers47217362017-10-05 11:39:15 +020065 return [self initWithAudioSession:[AVAudioSession sharedInstance]];
66}
67
68/** This initializer provides a way for unit tests to inject a fake/mock audio session. */
69- (instancetype)initWithAudioSession:(id)audioSession {
Zeke Chinb3fb71c2016-02-18 15:44:07 -080070 if (self = [super init]) {
Peter Hanspers47217362017-10-05 11:39:15 +020071 _session = audioSession;
tkchin0ce3bf92016-03-12 16:52:04 -080072
Zeke Chinb3fb71c2016-02-18 15:44:07 -080073 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
74 [center addObserver:self
75 selector:@selector(handleInterruptionNotification:)
76 name:AVAudioSessionInterruptionNotification
77 object:nil];
78 [center addObserver:self
79 selector:@selector(handleRouteChangeNotification:)
80 name:AVAudioSessionRouteChangeNotification
81 object:nil];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080082 [center addObserver:self
83 selector:@selector(handleMediaServicesWereLost:)
84 name:AVAudioSessionMediaServicesWereLostNotification
85 object:nil];
86 [center addObserver:self
87 selector:@selector(handleMediaServicesWereReset:)
88 name:AVAudioSessionMediaServicesWereResetNotification
89 object:nil];
henrikaf1363fd2016-09-27 06:06:44 -070090 // Posted on the main thread when the primary audio from other applications
91 // starts and stops. Foreground applications may use this notification as a
92 // hint to enable or disable audio that is secondary.
93 [center addObserver:self
94 selector:@selector(handleSilenceSecondaryAudioHintNotification:)
95 name:AVAudioSessionSilenceSecondaryAudioHintNotification
96 object:nil];
tkchin93dd6342016-07-27 10:17:14 -070097 // Also track foreground event in order to deal with interruption ended situation.
98 [center addObserver:self
99 selector:@selector(handleApplicationDidBecomeActive:)
100 name:UIApplicationDidBecomeActiveNotification
101 object:nil];
jtteh13ae11a2017-05-25 17:52:20 -0700102 [_session addObserver:self
103 forKeyPath:kRTCAudioSessionOutputVolumeSelector
104 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
Peter Hanspers47217362017-10-05 11:39:15 +0200105 context:(__bridge void*)RTCAudioSession.class];
jtteh13ae11a2017-05-25 17:52:20 -0700106
haysc7735b1e2017-03-29 14:53:32 -0700107 RTCLog(@"RTCAudioSession (%p): init.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800108 }
109 return self;
110}
111
112- (void)dealloc {
113 [[NSNotificationCenter defaultCenter] removeObserver:self];
Peter Hanspers47217362017-10-05 11:39:15 +0200114 [_session removeObserver:self
115 forKeyPath:kRTCAudioSessionOutputVolumeSelector
116 context:(__bridge void*)RTCAudioSession.class];
haysc7735b1e2017-03-29 14:53:32 -0700117 RTCLog(@"RTCAudioSession (%p): dealloc.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800118}
119
Zeke Chin1300caa2016-03-18 14:39:11 -0700120- (NSString *)description {
121 NSString *format =
122 @"RTCAudioSession: {\n"
tkchind2511962016-05-06 18:54:15 -0700123 " category: %@\n"
124 " categoryOptions: %ld\n"
125 " mode: %@\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700126 " isActive: %d\n"
127 " sampleRate: %.2f\n"
128 " IOBufferDuration: %f\n"
129 " outputNumberOfChannels: %ld\n"
130 " inputNumberOfChannels: %ld\n"
131 " outputLatency: %f\n"
132 " inputLatency: %f\n"
henrikac5aea652016-09-21 07:45:55 -0700133 " outputVolume: %f\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700134 "}";
135 NSString *description = [NSString stringWithFormat:format,
tkchind2511962016-05-06 18:54:15 -0700136 self.category, (long)self.categoryOptions, self.mode,
Zeke Chin1300caa2016-03-18 14:39:11 -0700137 self.isActive, self.sampleRate, self.IOBufferDuration,
138 self.outputNumberOfChannels, self.inputNumberOfChannels,
henrikac5aea652016-09-21 07:45:55 -0700139 self.outputLatency, self.inputLatency, self.outputVolume];
Zeke Chin1300caa2016-03-18 14:39:11 -0700140 return description;
141}
142
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800143- (void)setIsActive:(BOOL)isActive {
144 @synchronized(self) {
145 _isActive = isActive;
146 }
147}
148
149- (BOOL)isActive {
150 @synchronized(self) {
151 return _isActive;
152 }
153}
154
155- (BOOL)isLocked {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700156 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800157}
158
tkchind2511962016-05-06 18:54:15 -0700159- (void)setUseManualAudio:(BOOL)useManualAudio {
tkchin9f987d32016-03-12 20:06:28 -0800160 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700161 if (_useManualAudio == useManualAudio) {
tkchin9f987d32016-03-12 20:06:28 -0800162 return;
163 }
tkchind2511962016-05-06 18:54:15 -0700164 _useManualAudio = useManualAudio;
165 }
166 [self updateCanPlayOrRecord];
167}
168
169- (BOOL)useManualAudio {
170 @synchronized(self) {
171 return _useManualAudio;
tkchin9f987d32016-03-12 20:06:28 -0800172 }
173}
174
tkchind2511962016-05-06 18:54:15 -0700175- (void)setIsAudioEnabled:(BOOL)isAudioEnabled {
tkchin9f987d32016-03-12 20:06:28 -0800176 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700177 if (_isAudioEnabled == isAudioEnabled) {
178 return;
179 }
180 _isAudioEnabled = isAudioEnabled;
181 }
182 [self updateCanPlayOrRecord];
183}
184
185- (BOOL)isAudioEnabled {
186 @synchronized(self) {
187 return _isAudioEnabled;
tkchin9f987d32016-03-12 20:06:28 -0800188 }
189}
190
Joe Chen0c05b1a2019-05-07 10:46:22 -0700191- (void)setIgnoresPreferredAttributeConfigurationErrors:
192 (BOOL)ignoresPreferredAttributeConfigurationErrors {
193 @synchronized(self) {
194 if (_ignoresPreferredAttributeConfigurationErrors ==
195 ignoresPreferredAttributeConfigurationErrors) {
196 return;
197 }
198 _ignoresPreferredAttributeConfigurationErrors = ignoresPreferredAttributeConfigurationErrors;
199 }
200}
201
202- (BOOL)ignoresPreferredAttributeConfigurationErrors {
203 @synchronized(self) {
204 return _ignoresPreferredAttributeConfigurationErrors;
205 }
206}
207
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700208// TODO(tkchin): Check for duplicates.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800209- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700210 RTCLog(@"Adding delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700211 if (!delegate) {
212 return;
213 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800214 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700215 _delegates.push_back(delegate);
216 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800217 }
218}
219
220- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700221 RTCLog(@"Removing delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700222 if (!delegate) {
223 return;
224 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800225 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700226 _delegates.erase(std::remove(_delegates.begin(),
227 _delegates.end(),
tkchinefdd9302016-04-11 12:00:59 -0700228 delegate),
229 _delegates.end());
tkchine54467f2016-03-15 16:54:03 -0700230 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800231 }
232}
233
kthelgasonee8b8612017-03-31 04:50:27 -0700234#pragma clang diagnostic push
235#pragma clang diagnostic ignored "-Wthread-safety-analysis"
236
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800237- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800238 _crit.Enter();
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700239 rtc::AtomicOps::Increment(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800240}
241
242- (void)unlockForConfiguration {
243 // Don't let threads other than the one that called lockForConfiguration
244 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800245 if (_crit.TryEnter()) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700246 rtc::AtomicOps::Decrement(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800247 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800248 // was called without anyone calling lock, we will hit an assertion.
249 _crit.Leave();
250 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800251 }
252}
253
kthelgasonee8b8612017-03-31 04:50:27 -0700254#pragma clang diagnostic pop
255
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800256#pragma mark - AVAudioSession proxy methods
257
258- (NSString *)category {
259 return self.session.category;
260}
261
262- (AVAudioSessionCategoryOptions)categoryOptions {
263 return self.session.categoryOptions;
264}
265
266- (NSString *)mode {
267 return self.session.mode;
268}
269
270- (BOOL)secondaryAudioShouldBeSilencedHint {
271 return self.session.secondaryAudioShouldBeSilencedHint;
272}
273
274- (AVAudioSessionRouteDescription *)currentRoute {
275 return self.session.currentRoute;
276}
277
278- (NSInteger)maximumInputNumberOfChannels {
279 return self.session.maximumInputNumberOfChannels;
280}
281
282- (NSInteger)maximumOutputNumberOfChannels {
283 return self.session.maximumOutputNumberOfChannels;
284}
285
286- (float)inputGain {
287 return self.session.inputGain;
288}
289
290- (BOOL)inputGainSettable {
291 return self.session.inputGainSettable;
292}
293
294- (BOOL)inputAvailable {
295 return self.session.inputAvailable;
296}
297
298- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
299 return self.session.inputDataSources;
300}
301
302- (AVAudioSessionDataSourceDescription *)inputDataSource {
303 return self.session.inputDataSource;
304}
305
306- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
307 return self.session.outputDataSources;
308}
309
310- (AVAudioSessionDataSourceDescription *)outputDataSource {
311 return self.session.outputDataSource;
312}
313
314- (double)sampleRate {
315 return self.session.sampleRate;
316}
317
tkchind2511962016-05-06 18:54:15 -0700318- (double)preferredSampleRate {
319 return self.session.preferredSampleRate;
320}
321
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800322- (NSInteger)inputNumberOfChannels {
323 return self.session.inputNumberOfChannels;
324}
325
326- (NSInteger)outputNumberOfChannels {
327 return self.session.outputNumberOfChannels;
328}
329
330- (float)outputVolume {
331 return self.session.outputVolume;
332}
333
334- (NSTimeInterval)inputLatency {
335 return self.session.inputLatency;
336}
337
338- (NSTimeInterval)outputLatency {
339 return self.session.outputLatency;
340}
341
342- (NSTimeInterval)IOBufferDuration {
343 return self.session.IOBufferDuration;
344}
345
tkchind2511962016-05-06 18:54:15 -0700346- (NSTimeInterval)preferredIOBufferDuration {
347 return self.session.preferredIOBufferDuration;
348}
349
tkchine54467f2016-03-15 16:54:03 -0700350// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
351// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800352- (BOOL)setActive:(BOOL)active
353 error:(NSError **)outError {
354 if (![self checkLock:outError]) {
355 return NO;
356 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700357 int activationCount = _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800358 if (!active && activationCount == 0) {
359 RTCLogWarning(@"Attempting to deactivate without prior activation.");
360 }
JT Tehc1f083d2018-04-25 09:19:35 -0700361 [self notifyWillSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800362 BOOL success = YES;
363 BOOL isActive = self.isActive;
364 // Keep a local error so we can log it.
365 NSError *error = nil;
366 BOOL shouldSetActive =
367 (active && !isActive) || (!active && isActive && activationCount == 1);
368 // Attempt to activate if we're not active.
369 // Attempt to deactivate if we're active and it's the last unbalanced call.
370 if (shouldSetActive) {
371 AVAudioSession *session = self.session;
372 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
373 // that other audio sessions that were interrupted by our session can return
374 // to their active state. It is recommended for VoIP apps to use this
375 // option.
376 AVAudioSessionSetActiveOptions options =
377 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
378 success = [session setActive:active
379 withOptions:options
380 error:&error];
381 if (outError) {
382 *outError = error;
383 }
384 }
385 if (success) {
386 if (shouldSetActive) {
387 self.isActive = active;
388 }
389 if (active) {
390 [self incrementActivationCount];
391 }
JT Tehc1f083d2018-04-25 09:19:35 -0700392 [self notifyDidSetActive:active];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800393 } else {
tkchin9f987d32016-03-12 20:06:28 -0800394 RTCLogError(@"Failed to setActive:%d. Error: %@",
395 active, error.localizedDescription);
JT Tehc1f083d2018-04-25 09:19:35 -0700396 [self notifyFailedToSetActive:active error:error];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800397 }
398 // Decrement activation count on deactivation whether or not it succeeded.
399 if (!active) {
400 [self decrementActivationCount];
401 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700402 RTCLog(@"Number of current activations: %d", _activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800403 return success;
404}
405
406- (BOOL)setCategory:(NSString *)category
407 withOptions:(AVAudioSessionCategoryOptions)options
408 error:(NSError **)outError {
409 if (![self checkLock:outError]) {
410 return NO;
411 }
412 return [self.session setCategory:category withOptions:options error:outError];
413}
414
415- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
416 if (![self checkLock:outError]) {
417 return NO;
418 }
419 return [self.session setMode:mode error:outError];
420}
421
422- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
423 if (![self checkLock:outError]) {
424 return NO;
425 }
426 return [self.session setInputGain:gain error:outError];
427}
428
429- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
430 if (![self checkLock:outError]) {
431 return NO;
432 }
433 return [self.session setPreferredSampleRate:sampleRate error:outError];
434}
435
436- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
437 error:(NSError **)outError {
438 if (![self checkLock:outError]) {
439 return NO;
440 }
441 return [self.session setPreferredIOBufferDuration:duration error:outError];
442}
443
444- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
445 error:(NSError **)outError {
446 if (![self checkLock:outError]) {
447 return NO;
448 }
449 return [self.session setPreferredInputNumberOfChannels:count error:outError];
450}
451- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
452 error:(NSError **)outError {
453 if (![self checkLock:outError]) {
454 return NO;
455 }
456 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
457}
458
459- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
460 error:(NSError **)outError {
461 if (![self checkLock:outError]) {
462 return NO;
463 }
464 return [self.session overrideOutputAudioPort:portOverride error:outError];
465}
466
467- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
468 error:(NSError **)outError {
469 if (![self checkLock:outError]) {
470 return NO;
471 }
472 return [self.session setPreferredInput:inPort error:outError];
473}
474
475- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
476 error:(NSError **)outError {
477 if (![self checkLock:outError]) {
478 return NO;
479 }
480 return [self.session setInputDataSource:dataSource error:outError];
481}
482
483- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
484 error:(NSError **)outError {
485 if (![self checkLock:outError]) {
486 return NO;
487 }
488 return [self.session setOutputDataSource:dataSource error:outError];
489}
490
491#pragma mark - Notifications
492
493- (void)handleInterruptionNotification:(NSNotification *)notification {
494 NSNumber* typeNumber =
495 notification.userInfo[AVAudioSessionInterruptionTypeKey];
496 AVAudioSessionInterruptionType type =
497 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
498 switch (type) {
499 case AVAudioSessionInterruptionTypeBegan:
500 RTCLog(@"Audio session interruption began.");
501 self.isActive = NO;
tkchin93dd6342016-07-27 10:17:14 -0700502 self.isInterrupted = YES;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800503 [self notifyDidBeginInterruption];
504 break;
505 case AVAudioSessionInterruptionTypeEnded: {
506 RTCLog(@"Audio session interruption ended.");
tkchin93dd6342016-07-27 10:17:14 -0700507 self.isInterrupted = NO;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800508 [self updateAudioSessionAfterEvent];
509 NSNumber *optionsNumber =
510 notification.userInfo[AVAudioSessionInterruptionOptionKey];
511 AVAudioSessionInterruptionOptions options =
512 optionsNumber.unsignedIntegerValue;
513 BOOL shouldResume =
514 options & AVAudioSessionInterruptionOptionShouldResume;
515 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
516 break;
517 }
518 }
519}
520
521- (void)handleRouteChangeNotification:(NSNotification *)notification {
522 // Get reason for current route change.
523 NSNumber* reasonNumber =
524 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
525 AVAudioSessionRouteChangeReason reason =
526 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
527 RTCLog(@"Audio route changed:");
528 switch (reason) {
529 case AVAudioSessionRouteChangeReasonUnknown:
530 RTCLog(@"Audio route changed: ReasonUnknown");
531 break;
532 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
533 RTCLog(@"Audio route changed: NewDeviceAvailable");
534 break;
535 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
536 RTCLog(@"Audio route changed: OldDeviceUnavailable");
537 break;
538 case AVAudioSessionRouteChangeReasonCategoryChange:
539 RTCLog(@"Audio route changed: CategoryChange to :%@",
540 self.session.category);
541 break;
542 case AVAudioSessionRouteChangeReasonOverride:
543 RTCLog(@"Audio route changed: Override");
544 break;
545 case AVAudioSessionRouteChangeReasonWakeFromSleep:
546 RTCLog(@"Audio route changed: WakeFromSleep");
547 break;
548 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
549 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
550 break;
551 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
552 RTCLog(@"Audio route changed: RouteConfigurationChange");
553 break;
554 }
555 AVAudioSessionRouteDescription* previousRoute =
556 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
557 // Log previous route configuration.
558 RTCLog(@"Previous route: %@\nCurrent route:%@",
559 previousRoute, self.session.currentRoute);
560 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
561}
562
563- (void)handleMediaServicesWereLost:(NSNotification *)notification {
564 RTCLog(@"Media services were lost.");
565 [self updateAudioSessionAfterEvent];
566 [self notifyMediaServicesWereLost];
567}
568
569- (void)handleMediaServicesWereReset:(NSNotification *)notification {
570 RTCLog(@"Media services were reset.");
571 [self updateAudioSessionAfterEvent];
572 [self notifyMediaServicesWereReset];
573}
574
henrikaf1363fd2016-09-27 06:06:44 -0700575- (void)handleSilenceSecondaryAudioHintNotification:(NSNotification *)notification {
576 // TODO(henrika): just adding logs here for now until we know if we are ever
577 // see this notification and might be affected by it or if further actions
578 // are required.
579 NSNumber *typeNumber =
580 notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey];
581 AVAudioSessionSilenceSecondaryAudioHintType type =
582 (AVAudioSessionSilenceSecondaryAudioHintType)typeNumber.unsignedIntegerValue;
583 switch (type) {
584 case AVAudioSessionSilenceSecondaryAudioHintTypeBegin:
585 RTCLog(@"Another application's primary audio has started.");
586 break;
587 case AVAudioSessionSilenceSecondaryAudioHintTypeEnd:
588 RTCLog(@"Another application's primary audio has stopped.");
589 break;
590 }
591}
592
tkchin93dd6342016-07-27 10:17:14 -0700593- (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
Zeke Chin8280a562018-07-10 13:53:55 -0700594 BOOL isInterrupted = self.isInterrupted;
haysc7735b1e2017-03-29 14:53:32 -0700595 RTCLog(@"Application became active after an interruption. Treating as interruption "
Zeke Chin8280a562018-07-10 13:53:55 -0700596 "end. isInterrupted changed from %d to 0.",
597 isInterrupted);
598 if (isInterrupted) {
tkchin93dd6342016-07-27 10:17:14 -0700599 self.isInterrupted = NO;
600 [self updateAudioSessionAfterEvent];
tkchin93dd6342016-07-27 10:17:14 -0700601 }
haysc7735b1e2017-03-29 14:53:32 -0700602 // Always treat application becoming active as an interruption end event.
603 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
tkchin93dd6342016-07-27 10:17:14 -0700604}
605
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800606#pragma mark - Private
607
608+ (NSError *)lockError {
609 NSDictionary *userInfo = @{
610 NSLocalizedDescriptionKey:
611 @"Must call lockForConfiguration before calling this method."
612 };
613 NSError *error =
614 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
615 code:kRTCAudioSessionErrorLockRequired
616 userInfo:userInfo];
617 return error;
618}
619
tkchine54467f2016-03-15 16:54:03 -0700620- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800621 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700622 // Note: this returns a copy.
623 return _delegates;
624 }
625}
626
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700627// TODO(tkchin): check for duplicates.
tkchine54467f2016-03-15 16:54:03 -0700628- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
629 @synchronized(self) {
630 _delegates.insert(_delegates.begin(), delegate);
631 }
632}
633
634- (void)removeZeroedDelegates {
635 @synchronized(self) {
tkchinefdd9302016-04-11 12:00:59 -0700636 _delegates.erase(
637 std::remove_if(_delegates.begin(),
638 _delegates.end(),
639 [](id delegate) -> bool { return delegate == nil; }),
640 _delegates.end());
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800641 }
642}
643
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700644- (int)activationCount {
645 return _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800646}
647
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700648- (int)incrementActivationCount {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800649 RTCLog(@"Incrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700650 return rtc::AtomicOps::Increment(&_activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800651}
652
653- (NSInteger)decrementActivationCount {
654 RTCLog(@"Decrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700655 return rtc::AtomicOps::Decrement(&_activationCount);
656}
657
658- (int)webRTCSessionCount {
659 return _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800660}
661
tkchind2511962016-05-06 18:54:15 -0700662- (BOOL)canPlayOrRecord {
663 return !self.useManualAudio || self.isAudioEnabled;
664}
665
tkchin93dd6342016-07-27 10:17:14 -0700666- (BOOL)isInterrupted {
667 @synchronized(self) {
668 return _isInterrupted;
669 }
670}
671
672- (void)setIsInterrupted:(BOOL)isInterrupted {
673 @synchronized(self) {
674 if (_isInterrupted == isInterrupted) {
675 return;
676 }
677 _isInterrupted = isInterrupted;
678 }
679}
680
tkchin9f987d32016-03-12 20:06:28 -0800681- (BOOL)checkLock:(NSError **)outError {
682 // Check ivar instead of trying to acquire lock so that we won't accidentally
683 // acquire lock if it hasn't already been called.
684 if (!self.isLocked) {
685 if (outError) {
686 *outError = [RTCAudioSession lockError];
687 }
688 return NO;
689 }
690 return YES;
691}
692
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700693- (BOOL)beginWebRTCSession:(NSError **)outError {
694 if (outError) {
695 *outError = nil;
696 }
697 if (![self checkLock:outError]) {
698 return NO;
699 }
tkchind2511962016-05-06 18:54:15 -0700700 rtc::AtomicOps::Increment(&_webRTCSessionCount);
701 [self notifyDidStartPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700702 return YES;
703}
704
705- (BOOL)endWebRTCSession:(NSError **)outError {
706 if (outError) {
707 *outError = nil;
708 }
709 if (![self checkLock:outError]) {
710 return NO;
711 }
tkchind2511962016-05-06 18:54:15 -0700712 rtc::AtomicOps::Decrement(&_webRTCSessionCount);
713 [self notifyDidStopPlayOrRecord];
714 return YES;
715}
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700716
tkchind2511962016-05-06 18:54:15 -0700717- (BOOL)configureWebRTCSession:(NSError **)outError {
718 if (outError) {
719 *outError = nil;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700720 }
tkchind2511962016-05-06 18:54:15 -0700721 if (![self checkLock:outError]) {
722 return NO;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700723 }
tkchind2511962016-05-06 18:54:15 -0700724 RTCLog(@"Configuring audio session for WebRTC.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700725
tkchind2511962016-05-06 18:54:15 -0700726 // Configure the AVAudioSession and activate it.
727 // Provide an error even if there isn't one so we can log it.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700728 NSError *error = nil;
tkchind2511962016-05-06 18:54:15 -0700729 RTCAudioSessionConfiguration *webRTCConfig =
730 [RTCAudioSessionConfiguration webRTCConfiguration];
731 if (![self setConfiguration:webRTCConfig active:YES error:&error]) {
732 RTCLogError(@"Failed to set WebRTC audio configuration: %@",
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700733 error.localizedDescription);
jttehf84c1d62017-04-21 13:56:39 -0700734 // Do not call setActive:NO if setActive:YES failed.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700735 if (outError) {
736 *outError = error;
737 }
738 return NO;
739 }
740
tkchind2511962016-05-06 18:54:15 -0700741 // Ensure that the device currently supports audio input.
742 // TODO(tkchin): Figure out if this is really necessary.
743 if (!self.inputAvailable) {
744 RTCLogError(@"No audio input path is available!");
745 [self unconfigureWebRTCSession:nil];
746 if (outError) {
747 *outError = [self configurationErrorWithDescription:@"No input path."];
748 }
749 return NO;
750 }
751
henrika2d014be2016-06-16 14:26:55 +0200752 // It can happen (e.g. in combination with BT devices) that the attempt to set
753 // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new
754 // configuration attempt using the sample rate that worked using the active
755 // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in
756 // combination with BT headsets. Using this "trick" seems to avoid a state
757 // where Core Audio asks for a different number of audio frames than what the
758 // session's I/O buffer duration corresponds to.
759 // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been
760 // tested on a limited set of iOS devices and BT devices.
761 double sessionSampleRate = self.sampleRate;
762 double preferredSampleRate = webRTCConfig.sampleRate;
763 if (sessionSampleRate != preferredSampleRate) {
764 RTCLogWarning(
765 @"Current sample rate (%.2f) is not the preferred rate (%.2f)",
766 sessionSampleRate, preferredSampleRate);
767 if (![self setPreferredSampleRate:sessionSampleRate
768 error:&error]) {
769 RTCLogError(@"Failed to set preferred sample rate: %@",
770 error.localizedDescription);
771 if (outError) {
772 *outError = error;
773 }
774 }
775 }
776
tkchind2511962016-05-06 18:54:15 -0700777 return YES;
778}
779
780- (BOOL)unconfigureWebRTCSession:(NSError **)outError {
781 if (outError) {
782 *outError = nil;
783 }
784 if (![self checkLock:outError]) {
785 return NO;
786 }
787 RTCLog(@"Unconfiguring audio session for WebRTC.");
788 [self setActive:NO error:outError];
789
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700790 return YES;
791}
792
793- (NSError *)configurationErrorWithDescription:(NSString *)description {
794 NSDictionary* userInfo = @{
795 NSLocalizedDescriptionKey: description,
796 };
797 return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
798 code:kRTCAudioSessionErrorConfiguration
799 userInfo:userInfo];
800}
801
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800802- (void)updateAudioSessionAfterEvent {
803 BOOL shouldActivate = self.activationCount > 0;
804 AVAudioSessionSetActiveOptions options = shouldActivate ?
805 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
806 NSError *error = nil;
807 if ([self.session setActive:shouldActivate
808 withOptions:options
809 error:&error]) {
810 self.isActive = shouldActivate;
811 } else {
812 RTCLogError(@"Failed to set session active to %d. Error:%@",
813 shouldActivate, error.localizedDescription);
814 }
815}
816
tkchind2511962016-05-06 18:54:15 -0700817- (void)updateCanPlayOrRecord {
818 BOOL canPlayOrRecord = NO;
819 BOOL shouldNotify = NO;
820 @synchronized(self) {
821 canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled;
822 if (_canPlayOrRecord == canPlayOrRecord) {
823 return;
824 }
825 _canPlayOrRecord = canPlayOrRecord;
826 shouldNotify = YES;
827 }
828 if (shouldNotify) {
829 [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord];
830 }
831}
832
jtteh3c9a6c02017-04-18 09:09:35 -0700833- (void)audioSessionDidActivate:(AVAudioSession *)session {
834 if (_session != session) {
835 RTCLogError(@"audioSessionDidActivate called on different AVAudioSession");
836 }
Zeke Chin8280a562018-07-10 13:53:55 -0700837 RTCLog(@"Audio session was externally activated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700838 [self incrementActivationCount];
839 self.isActive = YES;
Zeke Chin8280a562018-07-10 13:53:55 -0700840 // When a CallKit call begins, it's possible that we receive an interruption
841 // begin without a corresponding end. Since we know that we have an activated
842 // audio session at this point, just clear any saved interruption flag since
843 // the app may never be foregrounded during the duration of the call.
844 if (self.isInterrupted) {
845 RTCLog(@"Clearing interrupted state due to external activation.");
846 self.isInterrupted = NO;
847 }
848 // Treat external audio session activation as an end interruption event.
849 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
jtteh3c9a6c02017-04-18 09:09:35 -0700850}
851
852- (void)audioSessionDidDeactivate:(AVAudioSession *)session {
853 if (_session != session) {
854 RTCLogError(@"audioSessionDidDeactivate called on different AVAudioSession");
855 }
Zeke Chin8280a562018-07-10 13:53:55 -0700856 RTCLog(@"Audio session was externally deactivated.");
jtteh3c9a6c02017-04-18 09:09:35 -0700857 self.isActive = NO;
858 [self decrementActivationCount];
859}
860
jtteh13ae11a2017-05-25 17:52:20 -0700861- (void)observeValueForKeyPath:(NSString *)keyPath
862 ofObject:(id)object
863 change:(NSDictionary *)change
864 context:(void *)context {
Peter Hanspers47217362017-10-05 11:39:15 +0200865 if (context == (__bridge void*)RTCAudioSession.class) {
866 if (object == _session) {
867 NSNumber *newVolume = change[NSKeyValueChangeNewKey];
868 RTCLog(@"OutputVolumeDidChange to %f", newVolume.floatValue);
869 [self notifyDidChangeOutputVolume:newVolume.floatValue];
870 }
jtteh13ae11a2017-05-25 17:52:20 -0700871 } else {
872 [super observeValueForKeyPath:keyPath
873 ofObject:object
874 change:change
875 context:context];
876 }
877}
878
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800879- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700880 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700881 SEL sel = @selector(audioSessionDidBeginInterruption:);
882 if ([delegate respondsToSelector:sel]) {
883 [delegate audioSessionDidBeginInterruption:self];
884 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800885 }
886}
887
888- (void)notifyDidEndInterruptionWithShouldResumeSession:
889 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700890 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700891 SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:);
892 if ([delegate respondsToSelector:sel]) {
893 [delegate audioSessionDidEndInterruption:self
894 shouldResumeSession:shouldResumeSession];
895 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800896 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800897}
898
899- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
900 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700901 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700902 SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:);
903 if ([delegate respondsToSelector:sel]) {
904 [delegate audioSessionDidChangeRoute:self
905 reason:reason
906 previousRoute:previousRoute];
907 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800908 }
909}
910
911- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700912 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800913 SEL sel = @selector(audioSessionMediaServerTerminated:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700914 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800915 [delegate audioSessionMediaServerTerminated:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700916 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800917 }
918}
919
920- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700921 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800922 SEL sel = @selector(audioSessionMediaServerReset:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700923 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800924 [delegate audioSessionMediaServerReset:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700925 }
926 }
927}
928
tkchind2511962016-05-06 18:54:15 -0700929- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700930 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700931 SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700932 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700933 [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700934 }
935 }
936}
937
tkchind2511962016-05-06 18:54:15 -0700938- (void)notifyDidStartPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700939 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700940 SEL sel = @selector(audioSessionDidStartPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700941 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700942 [delegate audioSessionDidStartPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700943 }
944 }
945}
946
tkchind2511962016-05-06 18:54:15 -0700947- (void)notifyDidStopPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700948 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700949 SEL sel = @selector(audioSessionDidStopPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700950 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700951 [delegate audioSessionDidStopPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700952 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800953 }
954}
955
jtteh13ae11a2017-05-25 17:52:20 -0700956- (void)notifyDidChangeOutputVolume:(float)volume {
957 for (auto delegate : self.delegates) {
958 SEL sel = @selector(audioSession:didChangeOutputVolume:);
959 if ([delegate respondsToSelector:sel]) {
960 [delegate audioSession:self didChangeOutputVolume:volume];
961 }
962 }
963}
964
Anders Carlsson121ea322017-06-26 15:34:47 +0200965- (void)notifyDidDetectPlayoutGlitch:(int64_t)totalNumberOfGlitches {
966 for (auto delegate : self.delegates) {
967 SEL sel = @selector(audioSession:didDetectPlayoutGlitch:);
968 if ([delegate respondsToSelector:sel]) {
969 [delegate audioSession:self didDetectPlayoutGlitch:totalNumberOfGlitches];
970 }
971 }
972}
973
JT Tehc1f083d2018-04-25 09:19:35 -0700974- (void)notifyWillSetActive:(BOOL)active {
975 for (id delegate : self.delegates) {
976 SEL sel = @selector(audioSession:willSetActive:);
977 if ([delegate respondsToSelector:sel]) {
978 [delegate audioSession:self willSetActive:active];
979 }
980 }
981}
982
983- (void)notifyDidSetActive:(BOOL)active {
984 for (id delegate : self.delegates) {
985 SEL sel = @selector(audioSession:didSetActive:);
986 if ([delegate respondsToSelector:sel]) {
987 [delegate audioSession:self didSetActive:active];
988 }
989 }
990}
991
992- (void)notifyFailedToSetActive:(BOOL)active error:(NSError *)error {
993 for (id delegate : self.delegates) {
994 SEL sel = @selector(audioSession:failedToSetActive:error:);
995 if ([delegate respondsToSelector:sel]) {
996 [delegate audioSession:self failedToSetActive:active error:error];
997 }
998 }
999}
1000
Zeke Chinb3fb71c2016-02-18 15:44:07 -08001001@end