blob: 67a70da80024146eb11e9b38558ee23a84fb22d7 [file] [log] [blame]
Zeke Chinb3fb71c2016-02-18 15:44:07 -08001/*
2 * Copyright 2016 The WebRTC Project Authors. All rights reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10
denicija59ee91b2017-06-05 05:48:47 -070011#import "WebRTC/RTCAudioSession.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080012
tkchin93dd6342016-07-27 10:17:14 -070013#import <UIKit/UIKit.h>
14
Mirko Bonadei92ea95e2017-09-15 06:47:31 +020015#include "rtc_base/atomicops.h"
16#include "rtc_base/checks.h"
17#include "rtc_base/criticalsection.h"
denicija59ee91b2017-06-05 05:48:47 -070018
19#import "WebRTC/RTCAudioSessionConfiguration.h"
tkchin9eeb6242016-04-27 01:54:20 -070020#import "WebRTC/RTCLogging.h"
denicija59ee91b2017-06-05 05:48:47 -070021
22#import "RTCAudioSession+Private.h"
23
Zeke Chinb3fb71c2016-02-18 15:44:07 -080024
25NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
26NSInteger const kRTCAudioSessionErrorLockRequired = -1;
tkchin9f987d32016-03-12 20:06:28 -080027NSInteger const kRTCAudioSessionErrorConfiguration = -2;
jtteh13ae11a2017-05-25 17:52:20 -070028NSString * const kRTCAudioSessionOutputVolumeSelector = @"outputVolume";
Zeke Chinb3fb71c2016-02-18 15:44:07 -080029
30// This class needs to be thread-safe because it is accessed from many threads.
31// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
32// lock contention so coarse locks should be fine for now.
33@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080034 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080035 AVAudioSession *_session;
Tze Kwang Chin307a0922016-03-21 13:57:40 -070036 volatile int _activationCount;
37 volatile int _lockRecursionCount;
38 volatile int _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080039 BOOL _isActive;
tkchind2511962016-05-06 18:54:15 -070040 BOOL _useManualAudio;
41 BOOL _isAudioEnabled;
42 BOOL _canPlayOrRecord;
tkchin93dd6342016-07-27 10:17:14 -070043 BOOL _isInterrupted;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080044}
45
46@synthesize session = _session;
tkchine54467f2016-03-15 16:54:03 -070047@synthesize delegates = _delegates;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080048
49+ (instancetype)sharedInstance {
50 static dispatch_once_t onceToken;
51 static RTCAudioSession *sharedInstance = nil;
52 dispatch_once(&onceToken, ^{
Tze Kwang Chin307a0922016-03-21 13:57:40 -070053 sharedInstance = [[self alloc] init];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080054 });
55 return sharedInstance;
56}
57
58- (instancetype)init {
Peter Hanspers47217362017-10-05 11:39:15 +020059 return [self initWithAudioSession:[AVAudioSession sharedInstance]];
60}
61
62/** This initializer provides a way for unit tests to inject a fake/mock audio session. */
63- (instancetype)initWithAudioSession:(id)audioSession {
Zeke Chinb3fb71c2016-02-18 15:44:07 -080064 if (self = [super init]) {
Peter Hanspers47217362017-10-05 11:39:15 +020065 _session = audioSession;
tkchin0ce3bf92016-03-12 16:52:04 -080066
Zeke Chinb3fb71c2016-02-18 15:44:07 -080067 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
68 [center addObserver:self
69 selector:@selector(handleInterruptionNotification:)
70 name:AVAudioSessionInterruptionNotification
71 object:nil];
72 [center addObserver:self
73 selector:@selector(handleRouteChangeNotification:)
74 name:AVAudioSessionRouteChangeNotification
75 object:nil];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080076 [center addObserver:self
77 selector:@selector(handleMediaServicesWereLost:)
78 name:AVAudioSessionMediaServicesWereLostNotification
79 object:nil];
80 [center addObserver:self
81 selector:@selector(handleMediaServicesWereReset:)
82 name:AVAudioSessionMediaServicesWereResetNotification
83 object:nil];
henrikaf1363fd2016-09-27 06:06:44 -070084 // Posted on the main thread when the primary audio from other applications
85 // starts and stops. Foreground applications may use this notification as a
86 // hint to enable or disable audio that is secondary.
87 [center addObserver:self
88 selector:@selector(handleSilenceSecondaryAudioHintNotification:)
89 name:AVAudioSessionSilenceSecondaryAudioHintNotification
90 object:nil];
tkchin93dd6342016-07-27 10:17:14 -070091 // Also track foreground event in order to deal with interruption ended situation.
92 [center addObserver:self
93 selector:@selector(handleApplicationDidBecomeActive:)
94 name:UIApplicationDidBecomeActiveNotification
95 object:nil];
jtteh13ae11a2017-05-25 17:52:20 -070096 [_session addObserver:self
97 forKeyPath:kRTCAudioSessionOutputVolumeSelector
98 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
Peter Hanspers47217362017-10-05 11:39:15 +020099 context:(__bridge void*)RTCAudioSession.class];
jtteh13ae11a2017-05-25 17:52:20 -0700100
haysc7735b1e2017-03-29 14:53:32 -0700101 RTCLog(@"RTCAudioSession (%p): init.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800102 }
103 return self;
104}
105
106- (void)dealloc {
107 [[NSNotificationCenter defaultCenter] removeObserver:self];
Peter Hanspers47217362017-10-05 11:39:15 +0200108 [_session removeObserver:self
109 forKeyPath:kRTCAudioSessionOutputVolumeSelector
110 context:(__bridge void*)RTCAudioSession.class];
haysc7735b1e2017-03-29 14:53:32 -0700111 RTCLog(@"RTCAudioSession (%p): dealloc.", self);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800112}
113
Zeke Chin1300caa2016-03-18 14:39:11 -0700114- (NSString *)description {
115 NSString *format =
116 @"RTCAudioSession: {\n"
tkchind2511962016-05-06 18:54:15 -0700117 " category: %@\n"
118 " categoryOptions: %ld\n"
119 " mode: %@\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700120 " isActive: %d\n"
121 " sampleRate: %.2f\n"
122 " IOBufferDuration: %f\n"
123 " outputNumberOfChannels: %ld\n"
124 " inputNumberOfChannels: %ld\n"
125 " outputLatency: %f\n"
126 " inputLatency: %f\n"
henrikac5aea652016-09-21 07:45:55 -0700127 " outputVolume: %f\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700128 "}";
129 NSString *description = [NSString stringWithFormat:format,
tkchind2511962016-05-06 18:54:15 -0700130 self.category, (long)self.categoryOptions, self.mode,
Zeke Chin1300caa2016-03-18 14:39:11 -0700131 self.isActive, self.sampleRate, self.IOBufferDuration,
132 self.outputNumberOfChannels, self.inputNumberOfChannels,
henrikac5aea652016-09-21 07:45:55 -0700133 self.outputLatency, self.inputLatency, self.outputVolume];
Zeke Chin1300caa2016-03-18 14:39:11 -0700134 return description;
135}
136
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800137- (void)setIsActive:(BOOL)isActive {
138 @synchronized(self) {
139 _isActive = isActive;
140 }
141}
142
143- (BOOL)isActive {
144 @synchronized(self) {
145 return _isActive;
146 }
147}
148
149- (BOOL)isLocked {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700150 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800151}
152
tkchind2511962016-05-06 18:54:15 -0700153- (void)setUseManualAudio:(BOOL)useManualAudio {
tkchin9f987d32016-03-12 20:06:28 -0800154 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700155 if (_useManualAudio == useManualAudio) {
tkchin9f987d32016-03-12 20:06:28 -0800156 return;
157 }
tkchind2511962016-05-06 18:54:15 -0700158 _useManualAudio = useManualAudio;
159 }
160 [self updateCanPlayOrRecord];
161}
162
163- (BOOL)useManualAudio {
164 @synchronized(self) {
165 return _useManualAudio;
tkchin9f987d32016-03-12 20:06:28 -0800166 }
167}
168
tkchind2511962016-05-06 18:54:15 -0700169- (void)setIsAudioEnabled:(BOOL)isAudioEnabled {
tkchin9f987d32016-03-12 20:06:28 -0800170 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700171 if (_isAudioEnabled == isAudioEnabled) {
172 return;
173 }
174 _isAudioEnabled = isAudioEnabled;
175 }
176 [self updateCanPlayOrRecord];
177}
178
179- (BOOL)isAudioEnabled {
180 @synchronized(self) {
181 return _isAudioEnabled;
tkchin9f987d32016-03-12 20:06:28 -0800182 }
183}
184
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700185// TODO(tkchin): Check for duplicates.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800186- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700187 RTCLog(@"Adding delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700188 if (!delegate) {
189 return;
190 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800191 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700192 _delegates.push_back(delegate);
193 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800194 }
195}
196
197- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
haysc7735b1e2017-03-29 14:53:32 -0700198 RTCLog(@"Removing delegate: (%p)", delegate);
tkchine54467f2016-03-15 16:54:03 -0700199 if (!delegate) {
200 return;
201 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800202 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700203 _delegates.erase(std::remove(_delegates.begin(),
204 _delegates.end(),
tkchinefdd9302016-04-11 12:00:59 -0700205 delegate),
206 _delegates.end());
tkchine54467f2016-03-15 16:54:03 -0700207 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800208 }
209}
210
kthelgasonee8b8612017-03-31 04:50:27 -0700211#pragma clang diagnostic push
212#pragma clang diagnostic ignored "-Wthread-safety-analysis"
213
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800214- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800215 _crit.Enter();
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700216 rtc::AtomicOps::Increment(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800217}
218
219- (void)unlockForConfiguration {
220 // Don't let threads other than the one that called lockForConfiguration
221 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800222 if (_crit.TryEnter()) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700223 rtc::AtomicOps::Decrement(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800224 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800225 // was called without anyone calling lock, we will hit an assertion.
226 _crit.Leave();
227 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800228 }
229}
230
kthelgasonee8b8612017-03-31 04:50:27 -0700231#pragma clang diagnostic pop
232
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800233#pragma mark - AVAudioSession proxy methods
234
235- (NSString *)category {
236 return self.session.category;
237}
238
239- (AVAudioSessionCategoryOptions)categoryOptions {
240 return self.session.categoryOptions;
241}
242
243- (NSString *)mode {
244 return self.session.mode;
245}
246
247- (BOOL)secondaryAudioShouldBeSilencedHint {
248 return self.session.secondaryAudioShouldBeSilencedHint;
249}
250
251- (AVAudioSessionRouteDescription *)currentRoute {
252 return self.session.currentRoute;
253}
254
255- (NSInteger)maximumInputNumberOfChannels {
256 return self.session.maximumInputNumberOfChannels;
257}
258
259- (NSInteger)maximumOutputNumberOfChannels {
260 return self.session.maximumOutputNumberOfChannels;
261}
262
263- (float)inputGain {
264 return self.session.inputGain;
265}
266
267- (BOOL)inputGainSettable {
268 return self.session.inputGainSettable;
269}
270
271- (BOOL)inputAvailable {
272 return self.session.inputAvailable;
273}
274
275- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
276 return self.session.inputDataSources;
277}
278
279- (AVAudioSessionDataSourceDescription *)inputDataSource {
280 return self.session.inputDataSource;
281}
282
283- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
284 return self.session.outputDataSources;
285}
286
287- (AVAudioSessionDataSourceDescription *)outputDataSource {
288 return self.session.outputDataSource;
289}
290
291- (double)sampleRate {
292 return self.session.sampleRate;
293}
294
tkchind2511962016-05-06 18:54:15 -0700295- (double)preferredSampleRate {
296 return self.session.preferredSampleRate;
297}
298
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800299- (NSInteger)inputNumberOfChannels {
300 return self.session.inputNumberOfChannels;
301}
302
303- (NSInteger)outputNumberOfChannels {
304 return self.session.outputNumberOfChannels;
305}
306
307- (float)outputVolume {
308 return self.session.outputVolume;
309}
310
311- (NSTimeInterval)inputLatency {
312 return self.session.inputLatency;
313}
314
315- (NSTimeInterval)outputLatency {
316 return self.session.outputLatency;
317}
318
319- (NSTimeInterval)IOBufferDuration {
320 return self.session.IOBufferDuration;
321}
322
tkchind2511962016-05-06 18:54:15 -0700323- (NSTimeInterval)preferredIOBufferDuration {
324 return self.session.preferredIOBufferDuration;
325}
326
tkchine54467f2016-03-15 16:54:03 -0700327// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
328// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800329- (BOOL)setActive:(BOOL)active
330 error:(NSError **)outError {
331 if (![self checkLock:outError]) {
332 return NO;
333 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700334 int activationCount = _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800335 if (!active && activationCount == 0) {
336 RTCLogWarning(@"Attempting to deactivate without prior activation.");
337 }
338 BOOL success = YES;
339 BOOL isActive = self.isActive;
340 // Keep a local error so we can log it.
341 NSError *error = nil;
342 BOOL shouldSetActive =
343 (active && !isActive) || (!active && isActive && activationCount == 1);
344 // Attempt to activate if we're not active.
345 // Attempt to deactivate if we're active and it's the last unbalanced call.
346 if (shouldSetActive) {
347 AVAudioSession *session = self.session;
348 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
349 // that other audio sessions that were interrupted by our session can return
350 // to their active state. It is recommended for VoIP apps to use this
351 // option.
352 AVAudioSessionSetActiveOptions options =
353 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
354 success = [session setActive:active
355 withOptions:options
356 error:&error];
357 if (outError) {
358 *outError = error;
359 }
360 }
361 if (success) {
362 if (shouldSetActive) {
363 self.isActive = active;
364 }
365 if (active) {
366 [self incrementActivationCount];
367 }
368 } else {
tkchin9f987d32016-03-12 20:06:28 -0800369 RTCLogError(@"Failed to setActive:%d. Error: %@",
370 active, error.localizedDescription);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800371 }
372 // Decrement activation count on deactivation whether or not it succeeded.
373 if (!active) {
374 [self decrementActivationCount];
375 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700376 RTCLog(@"Number of current activations: %d", _activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800377 return success;
378}
379
380- (BOOL)setCategory:(NSString *)category
381 withOptions:(AVAudioSessionCategoryOptions)options
382 error:(NSError **)outError {
383 if (![self checkLock:outError]) {
384 return NO;
385 }
386 return [self.session setCategory:category withOptions:options error:outError];
387}
388
389- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
390 if (![self checkLock:outError]) {
391 return NO;
392 }
393 return [self.session setMode:mode error:outError];
394}
395
396- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
397 if (![self checkLock:outError]) {
398 return NO;
399 }
400 return [self.session setInputGain:gain error:outError];
401}
402
403- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
404 if (![self checkLock:outError]) {
405 return NO;
406 }
407 return [self.session setPreferredSampleRate:sampleRate error:outError];
408}
409
410- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
411 error:(NSError **)outError {
412 if (![self checkLock:outError]) {
413 return NO;
414 }
415 return [self.session setPreferredIOBufferDuration:duration error:outError];
416}
417
418- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
419 error:(NSError **)outError {
420 if (![self checkLock:outError]) {
421 return NO;
422 }
423 return [self.session setPreferredInputNumberOfChannels:count error:outError];
424}
425- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
426 error:(NSError **)outError {
427 if (![self checkLock:outError]) {
428 return NO;
429 }
430 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
431}
432
433- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
434 error:(NSError **)outError {
435 if (![self checkLock:outError]) {
436 return NO;
437 }
438 return [self.session overrideOutputAudioPort:portOverride error:outError];
439}
440
441- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
442 error:(NSError **)outError {
443 if (![self checkLock:outError]) {
444 return NO;
445 }
446 return [self.session setPreferredInput:inPort error:outError];
447}
448
449- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
450 error:(NSError **)outError {
451 if (![self checkLock:outError]) {
452 return NO;
453 }
454 return [self.session setInputDataSource:dataSource error:outError];
455}
456
457- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
458 error:(NSError **)outError {
459 if (![self checkLock:outError]) {
460 return NO;
461 }
462 return [self.session setOutputDataSource:dataSource error:outError];
463}
464
465#pragma mark - Notifications
466
467- (void)handleInterruptionNotification:(NSNotification *)notification {
468 NSNumber* typeNumber =
469 notification.userInfo[AVAudioSessionInterruptionTypeKey];
470 AVAudioSessionInterruptionType type =
471 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
472 switch (type) {
473 case AVAudioSessionInterruptionTypeBegan:
474 RTCLog(@"Audio session interruption began.");
475 self.isActive = NO;
tkchin93dd6342016-07-27 10:17:14 -0700476 self.isInterrupted = YES;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800477 [self notifyDidBeginInterruption];
478 break;
479 case AVAudioSessionInterruptionTypeEnded: {
480 RTCLog(@"Audio session interruption ended.");
tkchin93dd6342016-07-27 10:17:14 -0700481 self.isInterrupted = NO;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800482 [self updateAudioSessionAfterEvent];
483 NSNumber *optionsNumber =
484 notification.userInfo[AVAudioSessionInterruptionOptionKey];
485 AVAudioSessionInterruptionOptions options =
486 optionsNumber.unsignedIntegerValue;
487 BOOL shouldResume =
488 options & AVAudioSessionInterruptionOptionShouldResume;
489 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
490 break;
491 }
492 }
493}
494
495- (void)handleRouteChangeNotification:(NSNotification *)notification {
496 // Get reason for current route change.
497 NSNumber* reasonNumber =
498 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
499 AVAudioSessionRouteChangeReason reason =
500 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
501 RTCLog(@"Audio route changed:");
502 switch (reason) {
503 case AVAudioSessionRouteChangeReasonUnknown:
504 RTCLog(@"Audio route changed: ReasonUnknown");
505 break;
506 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
507 RTCLog(@"Audio route changed: NewDeviceAvailable");
508 break;
509 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
510 RTCLog(@"Audio route changed: OldDeviceUnavailable");
511 break;
512 case AVAudioSessionRouteChangeReasonCategoryChange:
513 RTCLog(@"Audio route changed: CategoryChange to :%@",
514 self.session.category);
515 break;
516 case AVAudioSessionRouteChangeReasonOverride:
517 RTCLog(@"Audio route changed: Override");
518 break;
519 case AVAudioSessionRouteChangeReasonWakeFromSleep:
520 RTCLog(@"Audio route changed: WakeFromSleep");
521 break;
522 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
523 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
524 break;
525 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
526 RTCLog(@"Audio route changed: RouteConfigurationChange");
527 break;
528 }
529 AVAudioSessionRouteDescription* previousRoute =
530 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
531 // Log previous route configuration.
532 RTCLog(@"Previous route: %@\nCurrent route:%@",
533 previousRoute, self.session.currentRoute);
534 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
535}
536
537- (void)handleMediaServicesWereLost:(NSNotification *)notification {
538 RTCLog(@"Media services were lost.");
539 [self updateAudioSessionAfterEvent];
540 [self notifyMediaServicesWereLost];
541}
542
543- (void)handleMediaServicesWereReset:(NSNotification *)notification {
544 RTCLog(@"Media services were reset.");
545 [self updateAudioSessionAfterEvent];
546 [self notifyMediaServicesWereReset];
547}
548
henrikaf1363fd2016-09-27 06:06:44 -0700549- (void)handleSilenceSecondaryAudioHintNotification:(NSNotification *)notification {
550 // TODO(henrika): just adding logs here for now until we know if we are ever
551 // see this notification and might be affected by it or if further actions
552 // are required.
553 NSNumber *typeNumber =
554 notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey];
555 AVAudioSessionSilenceSecondaryAudioHintType type =
556 (AVAudioSessionSilenceSecondaryAudioHintType)typeNumber.unsignedIntegerValue;
557 switch (type) {
558 case AVAudioSessionSilenceSecondaryAudioHintTypeBegin:
559 RTCLog(@"Another application's primary audio has started.");
560 break;
561 case AVAudioSessionSilenceSecondaryAudioHintTypeEnd:
562 RTCLog(@"Another application's primary audio has stopped.");
563 break;
564 }
565}
566
tkchin93dd6342016-07-27 10:17:14 -0700567- (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
haysc7735b1e2017-03-29 14:53:32 -0700568 RTCLog(@"Application became active after an interruption. Treating as interruption "
569 " end. isInterrupted changed from %d to 0.", self.isInterrupted);
tkchin93dd6342016-07-27 10:17:14 -0700570 if (self.isInterrupted) {
tkchin93dd6342016-07-27 10:17:14 -0700571 self.isInterrupted = NO;
572 [self updateAudioSessionAfterEvent];
tkchin93dd6342016-07-27 10:17:14 -0700573 }
haysc7735b1e2017-03-29 14:53:32 -0700574 // Always treat application becoming active as an interruption end event.
575 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
tkchin93dd6342016-07-27 10:17:14 -0700576}
577
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800578#pragma mark - Private
579
580+ (NSError *)lockError {
581 NSDictionary *userInfo = @{
582 NSLocalizedDescriptionKey:
583 @"Must call lockForConfiguration before calling this method."
584 };
585 NSError *error =
586 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
587 code:kRTCAudioSessionErrorLockRequired
588 userInfo:userInfo];
589 return error;
590}
591
tkchine54467f2016-03-15 16:54:03 -0700592- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800593 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700594 // Note: this returns a copy.
595 return _delegates;
596 }
597}
598
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700599// TODO(tkchin): check for duplicates.
tkchine54467f2016-03-15 16:54:03 -0700600- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
601 @synchronized(self) {
602 _delegates.insert(_delegates.begin(), delegate);
603 }
604}
605
606- (void)removeZeroedDelegates {
607 @synchronized(self) {
tkchinefdd9302016-04-11 12:00:59 -0700608 _delegates.erase(
609 std::remove_if(_delegates.begin(),
610 _delegates.end(),
611 [](id delegate) -> bool { return delegate == nil; }),
612 _delegates.end());
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800613 }
614}
615
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700616- (int)activationCount {
617 return _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800618}
619
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700620- (int)incrementActivationCount {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800621 RTCLog(@"Incrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700622 return rtc::AtomicOps::Increment(&_activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800623}
624
625- (NSInteger)decrementActivationCount {
626 RTCLog(@"Decrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700627 return rtc::AtomicOps::Decrement(&_activationCount);
628}
629
630- (int)webRTCSessionCount {
631 return _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800632}
633
tkchind2511962016-05-06 18:54:15 -0700634- (BOOL)canPlayOrRecord {
635 return !self.useManualAudio || self.isAudioEnabled;
636}
637
tkchin93dd6342016-07-27 10:17:14 -0700638- (BOOL)isInterrupted {
639 @synchronized(self) {
640 return _isInterrupted;
641 }
642}
643
644- (void)setIsInterrupted:(BOOL)isInterrupted {
645 @synchronized(self) {
646 if (_isInterrupted == isInterrupted) {
647 return;
648 }
649 _isInterrupted = isInterrupted;
650 }
651}
652
tkchin9f987d32016-03-12 20:06:28 -0800653- (BOOL)checkLock:(NSError **)outError {
654 // Check ivar instead of trying to acquire lock so that we won't accidentally
655 // acquire lock if it hasn't already been called.
656 if (!self.isLocked) {
657 if (outError) {
658 *outError = [RTCAudioSession lockError];
659 }
660 return NO;
661 }
662 return YES;
663}
664
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700665- (BOOL)beginWebRTCSession:(NSError **)outError {
666 if (outError) {
667 *outError = nil;
668 }
669 if (![self checkLock:outError]) {
670 return NO;
671 }
tkchind2511962016-05-06 18:54:15 -0700672 rtc::AtomicOps::Increment(&_webRTCSessionCount);
673 [self notifyDidStartPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700674 return YES;
675}
676
677- (BOOL)endWebRTCSession:(NSError **)outError {
678 if (outError) {
679 *outError = nil;
680 }
681 if (![self checkLock:outError]) {
682 return NO;
683 }
tkchind2511962016-05-06 18:54:15 -0700684 rtc::AtomicOps::Decrement(&_webRTCSessionCount);
685 [self notifyDidStopPlayOrRecord];
686 return YES;
687}
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700688
tkchind2511962016-05-06 18:54:15 -0700689- (BOOL)configureWebRTCSession:(NSError **)outError {
690 if (outError) {
691 *outError = nil;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700692 }
tkchind2511962016-05-06 18:54:15 -0700693 if (![self checkLock:outError]) {
694 return NO;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700695 }
tkchind2511962016-05-06 18:54:15 -0700696 RTCLog(@"Configuring audio session for WebRTC.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700697
tkchind2511962016-05-06 18:54:15 -0700698 // Configure the AVAudioSession and activate it.
699 // Provide an error even if there isn't one so we can log it.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700700 NSError *error = nil;
tkchind2511962016-05-06 18:54:15 -0700701 RTCAudioSessionConfiguration *webRTCConfig =
702 [RTCAudioSessionConfiguration webRTCConfiguration];
703 if (![self setConfiguration:webRTCConfig active:YES error:&error]) {
704 RTCLogError(@"Failed to set WebRTC audio configuration: %@",
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700705 error.localizedDescription);
jttehf84c1d62017-04-21 13:56:39 -0700706 // Do not call setActive:NO if setActive:YES failed.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700707 if (outError) {
708 *outError = error;
709 }
710 return NO;
711 }
712
tkchind2511962016-05-06 18:54:15 -0700713 // Ensure that the device currently supports audio input.
714 // TODO(tkchin): Figure out if this is really necessary.
715 if (!self.inputAvailable) {
716 RTCLogError(@"No audio input path is available!");
717 [self unconfigureWebRTCSession:nil];
718 if (outError) {
719 *outError = [self configurationErrorWithDescription:@"No input path."];
720 }
721 return NO;
722 }
723
henrika2d014be2016-06-16 14:26:55 +0200724 // It can happen (e.g. in combination with BT devices) that the attempt to set
725 // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new
726 // configuration attempt using the sample rate that worked using the active
727 // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in
728 // combination with BT headsets. Using this "trick" seems to avoid a state
729 // where Core Audio asks for a different number of audio frames than what the
730 // session's I/O buffer duration corresponds to.
731 // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been
732 // tested on a limited set of iOS devices and BT devices.
733 double sessionSampleRate = self.sampleRate;
734 double preferredSampleRate = webRTCConfig.sampleRate;
735 if (sessionSampleRate != preferredSampleRate) {
736 RTCLogWarning(
737 @"Current sample rate (%.2f) is not the preferred rate (%.2f)",
738 sessionSampleRate, preferredSampleRate);
739 if (![self setPreferredSampleRate:sessionSampleRate
740 error:&error]) {
741 RTCLogError(@"Failed to set preferred sample rate: %@",
742 error.localizedDescription);
743 if (outError) {
744 *outError = error;
745 }
746 }
747 }
748
tkchind2511962016-05-06 18:54:15 -0700749 return YES;
750}
751
752- (BOOL)unconfigureWebRTCSession:(NSError **)outError {
753 if (outError) {
754 *outError = nil;
755 }
756 if (![self checkLock:outError]) {
757 return NO;
758 }
759 RTCLog(@"Unconfiguring audio session for WebRTC.");
760 [self setActive:NO error:outError];
761
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700762 return YES;
763}
764
765- (NSError *)configurationErrorWithDescription:(NSString *)description {
766 NSDictionary* userInfo = @{
767 NSLocalizedDescriptionKey: description,
768 };
769 return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
770 code:kRTCAudioSessionErrorConfiguration
771 userInfo:userInfo];
772}
773
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800774- (void)updateAudioSessionAfterEvent {
775 BOOL shouldActivate = self.activationCount > 0;
776 AVAudioSessionSetActiveOptions options = shouldActivate ?
777 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
778 NSError *error = nil;
779 if ([self.session setActive:shouldActivate
780 withOptions:options
781 error:&error]) {
782 self.isActive = shouldActivate;
783 } else {
784 RTCLogError(@"Failed to set session active to %d. Error:%@",
785 shouldActivate, error.localizedDescription);
786 }
787}
788
tkchind2511962016-05-06 18:54:15 -0700789- (void)updateCanPlayOrRecord {
790 BOOL canPlayOrRecord = NO;
791 BOOL shouldNotify = NO;
792 @synchronized(self) {
793 canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled;
794 if (_canPlayOrRecord == canPlayOrRecord) {
795 return;
796 }
797 _canPlayOrRecord = canPlayOrRecord;
798 shouldNotify = YES;
799 }
800 if (shouldNotify) {
801 [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord];
802 }
803}
804
jtteh3c9a6c02017-04-18 09:09:35 -0700805- (void)audioSessionDidActivate:(AVAudioSession *)session {
806 if (_session != session) {
807 RTCLogError(@"audioSessionDidActivate called on different AVAudioSession");
808 }
809 [self incrementActivationCount];
810 self.isActive = YES;
811}
812
813- (void)audioSessionDidDeactivate:(AVAudioSession *)session {
814 if (_session != session) {
815 RTCLogError(@"audioSessionDidDeactivate called on different AVAudioSession");
816 }
817 self.isActive = NO;
818 [self decrementActivationCount];
819}
820
jtteh13ae11a2017-05-25 17:52:20 -0700821- (void)observeValueForKeyPath:(NSString *)keyPath
822 ofObject:(id)object
823 change:(NSDictionary *)change
824 context:(void *)context {
Peter Hanspers47217362017-10-05 11:39:15 +0200825 if (context == (__bridge void*)RTCAudioSession.class) {
826 if (object == _session) {
827 NSNumber *newVolume = change[NSKeyValueChangeNewKey];
828 RTCLog(@"OutputVolumeDidChange to %f", newVolume.floatValue);
829 [self notifyDidChangeOutputVolume:newVolume.floatValue];
830 }
jtteh13ae11a2017-05-25 17:52:20 -0700831 } else {
832 [super observeValueForKeyPath:keyPath
833 ofObject:object
834 change:change
835 context:context];
836 }
837}
838
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800839- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700840 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700841 SEL sel = @selector(audioSessionDidBeginInterruption:);
842 if ([delegate respondsToSelector:sel]) {
843 [delegate audioSessionDidBeginInterruption:self];
844 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800845 }
846}
847
848- (void)notifyDidEndInterruptionWithShouldResumeSession:
849 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700850 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700851 SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:);
852 if ([delegate respondsToSelector:sel]) {
853 [delegate audioSessionDidEndInterruption:self
854 shouldResumeSession:shouldResumeSession];
855 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800856 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800857}
858
859- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
860 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700861 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700862 SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:);
863 if ([delegate respondsToSelector:sel]) {
864 [delegate audioSessionDidChangeRoute:self
865 reason:reason
866 previousRoute:previousRoute];
867 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800868 }
869}
870
871- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700872 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800873 SEL sel = @selector(audioSessionMediaServerTerminated:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700874 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800875 [delegate audioSessionMediaServerTerminated:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700876 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800877 }
878}
879
880- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700881 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800882 SEL sel = @selector(audioSessionMediaServerReset:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700883 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800884 [delegate audioSessionMediaServerReset:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700885 }
886 }
887}
888
tkchind2511962016-05-06 18:54:15 -0700889- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700890 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700891 SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700892 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700893 [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700894 }
895 }
896}
897
tkchind2511962016-05-06 18:54:15 -0700898- (void)notifyDidStartPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700899 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700900 SEL sel = @selector(audioSessionDidStartPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700901 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700902 [delegate audioSessionDidStartPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700903 }
904 }
905}
906
tkchind2511962016-05-06 18:54:15 -0700907- (void)notifyDidStopPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700908 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700909 SEL sel = @selector(audioSessionDidStopPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700910 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700911 [delegate audioSessionDidStopPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700912 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800913 }
914}
915
jtteh13ae11a2017-05-25 17:52:20 -0700916- (void)notifyDidChangeOutputVolume:(float)volume {
917 for (auto delegate : self.delegates) {
918 SEL sel = @selector(audioSession:didChangeOutputVolume:);
919 if ([delegate respondsToSelector:sel]) {
920 [delegate audioSession:self didChangeOutputVolume:volume];
921 }
922 }
923}
924
Anders Carlsson121ea322017-06-26 15:34:47 +0200925- (void)notifyDidDetectPlayoutGlitch:(int64_t)totalNumberOfGlitches {
926 for (auto delegate : self.delegates) {
927 SEL sel = @selector(audioSession:didDetectPlayoutGlitch:);
928 if ([delegate respondsToSelector:sel]) {
929 [delegate audioSession:self didDetectPlayoutGlitch:totalNumberOfGlitches];
930 }
931 }
932}
933
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800934@end