blob: 4d9b9774408a52d80da7df2b054651418eec53bc [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
11#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h"
12
tkchin93dd6342016-07-27 10:17:14 -070013#import <UIKit/UIKit.h>
14
Tze Kwang Chin307a0922016-03-21 13:57:40 -070015#include "webrtc/base/atomicops.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080016#include "webrtc/base/checks.h"
tkchin0ce3bf92016-03-12 16:52:04 -080017#include "webrtc/base/criticalsection.h"
tkchine54467f2016-03-15 16:54:03 -070018#include "webrtc/modules/audio_device/ios/audio_device_ios.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080019
tkchin9eeb6242016-04-27 01:54:20 -070020#import "WebRTC/RTCLogging.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080021#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h"
tkchind2511962016-05-06 18:54:15 -070022#import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h"
Zeke Chinb3fb71c2016-02-18 15:44:07 -080023
24NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession";
25NSInteger const kRTCAudioSessionErrorLockRequired = -1;
tkchin9f987d32016-03-12 20:06:28 -080026NSInteger const kRTCAudioSessionErrorConfiguration = -2;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080027
28// This class needs to be thread-safe because it is accessed from many threads.
29// TODO(tkchin): Consider more granular locking. We're not expecting a lot of
30// lock contention so coarse locks should be fine for now.
31@implementation RTCAudioSession {
tkchin0ce3bf92016-03-12 16:52:04 -080032 rtc::CriticalSection _crit;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080033 AVAudioSession *_session;
Tze Kwang Chin307a0922016-03-21 13:57:40 -070034 volatile int _activationCount;
35 volatile int _lockRecursionCount;
36 volatile int _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080037 BOOL _isActive;
tkchind2511962016-05-06 18:54:15 -070038 BOOL _useManualAudio;
39 BOOL _isAudioEnabled;
40 BOOL _canPlayOrRecord;
tkchin93dd6342016-07-27 10:17:14 -070041 BOOL _isInterrupted;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080042}
43
44@synthesize session = _session;
tkchine54467f2016-03-15 16:54:03 -070045@synthesize delegates = _delegates;
Zeke Chinb3fb71c2016-02-18 15:44:07 -080046
47+ (instancetype)sharedInstance {
48 static dispatch_once_t onceToken;
49 static RTCAudioSession *sharedInstance = nil;
50 dispatch_once(&onceToken, ^{
Tze Kwang Chin307a0922016-03-21 13:57:40 -070051 sharedInstance = [[self alloc] init];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080052 });
53 return sharedInstance;
54}
55
56- (instancetype)init {
57 if (self = [super init]) {
58 _session = [AVAudioSession sharedInstance];
tkchin0ce3bf92016-03-12 16:52:04 -080059
Zeke Chinb3fb71c2016-02-18 15:44:07 -080060 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
61 [center addObserver:self
62 selector:@selector(handleInterruptionNotification:)
63 name:AVAudioSessionInterruptionNotification
64 object:nil];
65 [center addObserver:self
66 selector:@selector(handleRouteChangeNotification:)
67 name:AVAudioSessionRouteChangeNotification
68 object:nil];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080069 [center addObserver:self
70 selector:@selector(handleMediaServicesWereLost:)
71 name:AVAudioSessionMediaServicesWereLostNotification
72 object:nil];
73 [center addObserver:self
74 selector:@selector(handleMediaServicesWereReset:)
75 name:AVAudioSessionMediaServicesWereResetNotification
76 object:nil];
henrikaf1363fd2016-09-27 06:06:44 -070077 // Posted on the main thread when the primary audio from other applications
78 // starts and stops. Foreground applications may use this notification as a
79 // hint to enable or disable audio that is secondary.
80 [center addObserver:self
81 selector:@selector(handleSilenceSecondaryAudioHintNotification:)
82 name:AVAudioSessionSilenceSecondaryAudioHintNotification
83 object:nil];
tkchin93dd6342016-07-27 10:17:14 -070084 // Also track foreground event in order to deal with interruption ended situation.
85 [center addObserver:self
86 selector:@selector(handleApplicationDidBecomeActive:)
87 name:UIApplicationDidBecomeActiveNotification
88 object:nil];
Zeke Chinb3fb71c2016-02-18 15:44:07 -080089 }
90 return self;
91}
92
93- (void)dealloc {
94 [[NSNotificationCenter defaultCenter] removeObserver:self];
95}
96
Zeke Chin1300caa2016-03-18 14:39:11 -070097- (NSString *)description {
98 NSString *format =
99 @"RTCAudioSession: {\n"
tkchind2511962016-05-06 18:54:15 -0700100 " category: %@\n"
101 " categoryOptions: %ld\n"
102 " mode: %@\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700103 " isActive: %d\n"
104 " sampleRate: %.2f\n"
105 " IOBufferDuration: %f\n"
106 " outputNumberOfChannels: %ld\n"
107 " inputNumberOfChannels: %ld\n"
108 " outputLatency: %f\n"
109 " inputLatency: %f\n"
henrikac5aea652016-09-21 07:45:55 -0700110 " outputVolume: %f\n"
Zeke Chin1300caa2016-03-18 14:39:11 -0700111 "}";
112 NSString *description = [NSString stringWithFormat:format,
tkchind2511962016-05-06 18:54:15 -0700113 self.category, (long)self.categoryOptions, self.mode,
Zeke Chin1300caa2016-03-18 14:39:11 -0700114 self.isActive, self.sampleRate, self.IOBufferDuration,
115 self.outputNumberOfChannels, self.inputNumberOfChannels,
henrikac5aea652016-09-21 07:45:55 -0700116 self.outputLatency, self.inputLatency, self.outputVolume];
Zeke Chin1300caa2016-03-18 14:39:11 -0700117 return description;
118}
119
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800120- (void)setIsActive:(BOOL)isActive {
121 @synchronized(self) {
122 _isActive = isActive;
123 }
124}
125
126- (BOOL)isActive {
127 @synchronized(self) {
128 return _isActive;
129 }
130}
131
132- (BOOL)isLocked {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700133 return _lockRecursionCount > 0;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800134}
135
tkchind2511962016-05-06 18:54:15 -0700136- (void)setUseManualAudio:(BOOL)useManualAudio {
tkchin9f987d32016-03-12 20:06:28 -0800137 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700138 if (_useManualAudio == useManualAudio) {
tkchin9f987d32016-03-12 20:06:28 -0800139 return;
140 }
tkchind2511962016-05-06 18:54:15 -0700141 _useManualAudio = useManualAudio;
142 }
143 [self updateCanPlayOrRecord];
144}
145
146- (BOOL)useManualAudio {
147 @synchronized(self) {
148 return _useManualAudio;
tkchin9f987d32016-03-12 20:06:28 -0800149 }
150}
151
tkchind2511962016-05-06 18:54:15 -0700152- (void)setIsAudioEnabled:(BOOL)isAudioEnabled {
tkchin9f987d32016-03-12 20:06:28 -0800153 @synchronized(self) {
tkchind2511962016-05-06 18:54:15 -0700154 if (_isAudioEnabled == isAudioEnabled) {
155 return;
156 }
157 _isAudioEnabled = isAudioEnabled;
158 }
159 [self updateCanPlayOrRecord];
160}
161
162- (BOOL)isAudioEnabled {
163 @synchronized(self) {
164 return _isAudioEnabled;
tkchin9f987d32016-03-12 20:06:28 -0800165 }
166}
167
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700168// TODO(tkchin): Check for duplicates.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800169- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
tkchine54467f2016-03-15 16:54:03 -0700170 if (!delegate) {
171 return;
172 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800173 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700174 _delegates.push_back(delegate);
175 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800176 }
177}
178
179- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
tkchine54467f2016-03-15 16:54:03 -0700180 if (!delegate) {
181 return;
182 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800183 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700184 _delegates.erase(std::remove(_delegates.begin(),
185 _delegates.end(),
tkchinefdd9302016-04-11 12:00:59 -0700186 delegate),
187 _delegates.end());
tkchine54467f2016-03-15 16:54:03 -0700188 [self removeZeroedDelegates];
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800189 }
190}
191
192- (void)lockForConfiguration {
tkchin0ce3bf92016-03-12 16:52:04 -0800193 _crit.Enter();
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700194 rtc::AtomicOps::Increment(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800195}
196
197- (void)unlockForConfiguration {
198 // Don't let threads other than the one that called lockForConfiguration
199 // unlock.
tkchin0ce3bf92016-03-12 16:52:04 -0800200 if (_crit.TryEnter()) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700201 rtc::AtomicOps::Decrement(&_lockRecursionCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800202 // One unlock for the tryLock, and another one to actually unlock. If this
tkchin0ce3bf92016-03-12 16:52:04 -0800203 // was called without anyone calling lock, we will hit an assertion.
204 _crit.Leave();
205 _crit.Leave();
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800206 }
207}
208
209#pragma mark - AVAudioSession proxy methods
210
211- (NSString *)category {
212 return self.session.category;
213}
214
215- (AVAudioSessionCategoryOptions)categoryOptions {
216 return self.session.categoryOptions;
217}
218
219- (NSString *)mode {
220 return self.session.mode;
221}
222
223- (BOOL)secondaryAudioShouldBeSilencedHint {
224 return self.session.secondaryAudioShouldBeSilencedHint;
225}
226
227- (AVAudioSessionRouteDescription *)currentRoute {
228 return self.session.currentRoute;
229}
230
231- (NSInteger)maximumInputNumberOfChannels {
232 return self.session.maximumInputNumberOfChannels;
233}
234
235- (NSInteger)maximumOutputNumberOfChannels {
236 return self.session.maximumOutputNumberOfChannels;
237}
238
239- (float)inputGain {
240 return self.session.inputGain;
241}
242
243- (BOOL)inputGainSettable {
244 return self.session.inputGainSettable;
245}
246
247- (BOOL)inputAvailable {
248 return self.session.inputAvailable;
249}
250
251- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources {
252 return self.session.inputDataSources;
253}
254
255- (AVAudioSessionDataSourceDescription *)inputDataSource {
256 return self.session.inputDataSource;
257}
258
259- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources {
260 return self.session.outputDataSources;
261}
262
263- (AVAudioSessionDataSourceDescription *)outputDataSource {
264 return self.session.outputDataSource;
265}
266
267- (double)sampleRate {
268 return self.session.sampleRate;
269}
270
tkchind2511962016-05-06 18:54:15 -0700271- (double)preferredSampleRate {
272 return self.session.preferredSampleRate;
273}
274
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800275- (NSInteger)inputNumberOfChannels {
276 return self.session.inputNumberOfChannels;
277}
278
279- (NSInteger)outputNumberOfChannels {
280 return self.session.outputNumberOfChannels;
281}
282
283- (float)outputVolume {
284 return self.session.outputVolume;
285}
286
287- (NSTimeInterval)inputLatency {
288 return self.session.inputLatency;
289}
290
291- (NSTimeInterval)outputLatency {
292 return self.session.outputLatency;
293}
294
295- (NSTimeInterval)IOBufferDuration {
296 return self.session.IOBufferDuration;
297}
298
tkchind2511962016-05-06 18:54:15 -0700299- (NSTimeInterval)preferredIOBufferDuration {
300 return self.session.preferredIOBufferDuration;
301}
302
tkchine54467f2016-03-15 16:54:03 -0700303// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
304// can just do atomic increments / decrements.
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800305- (BOOL)setActive:(BOOL)active
306 error:(NSError **)outError {
307 if (![self checkLock:outError]) {
308 return NO;
309 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700310 int activationCount = _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800311 if (!active && activationCount == 0) {
312 RTCLogWarning(@"Attempting to deactivate without prior activation.");
313 }
314 BOOL success = YES;
315 BOOL isActive = self.isActive;
316 // Keep a local error so we can log it.
317 NSError *error = nil;
318 BOOL shouldSetActive =
319 (active && !isActive) || (!active && isActive && activationCount == 1);
320 // Attempt to activate if we're not active.
321 // Attempt to deactivate if we're active and it's the last unbalanced call.
322 if (shouldSetActive) {
323 AVAudioSession *session = self.session;
324 // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure
325 // that other audio sessions that were interrupted by our session can return
326 // to their active state. It is recommended for VoIP apps to use this
327 // option.
328 AVAudioSessionSetActiveOptions options =
329 active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
330 success = [session setActive:active
331 withOptions:options
332 error:&error];
333 if (outError) {
334 *outError = error;
335 }
336 }
337 if (success) {
338 if (shouldSetActive) {
339 self.isActive = active;
340 }
341 if (active) {
342 [self incrementActivationCount];
343 }
344 } else {
tkchin9f987d32016-03-12 20:06:28 -0800345 RTCLogError(@"Failed to setActive:%d. Error: %@",
346 active, error.localizedDescription);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800347 }
348 // Decrement activation count on deactivation whether or not it succeeded.
349 if (!active) {
350 [self decrementActivationCount];
351 }
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700352 RTCLog(@"Number of current activations: %d", _activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800353 return success;
354}
355
356- (BOOL)setCategory:(NSString *)category
357 withOptions:(AVAudioSessionCategoryOptions)options
358 error:(NSError **)outError {
359 if (![self checkLock:outError]) {
360 return NO;
361 }
362 return [self.session setCategory:category withOptions:options error:outError];
363}
364
365- (BOOL)setMode:(NSString *)mode error:(NSError **)outError {
366 if (![self checkLock:outError]) {
367 return NO;
368 }
369 return [self.session setMode:mode error:outError];
370}
371
372- (BOOL)setInputGain:(float)gain error:(NSError **)outError {
373 if (![self checkLock:outError]) {
374 return NO;
375 }
376 return [self.session setInputGain:gain error:outError];
377}
378
379- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError {
380 if (![self checkLock:outError]) {
381 return NO;
382 }
383 return [self.session setPreferredSampleRate:sampleRate error:outError];
384}
385
386- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
387 error:(NSError **)outError {
388 if (![self checkLock:outError]) {
389 return NO;
390 }
391 return [self.session setPreferredIOBufferDuration:duration error:outError];
392}
393
394- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
395 error:(NSError **)outError {
396 if (![self checkLock:outError]) {
397 return NO;
398 }
399 return [self.session setPreferredInputNumberOfChannels:count error:outError];
400}
401- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
402 error:(NSError **)outError {
403 if (![self checkLock:outError]) {
404 return NO;
405 }
406 return [self.session setPreferredOutputNumberOfChannels:count error:outError];
407}
408
409- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
410 error:(NSError **)outError {
411 if (![self checkLock:outError]) {
412 return NO;
413 }
414 return [self.session overrideOutputAudioPort:portOverride error:outError];
415}
416
417- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
418 error:(NSError **)outError {
419 if (![self checkLock:outError]) {
420 return NO;
421 }
422 return [self.session setPreferredInput:inPort error:outError];
423}
424
425- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
426 error:(NSError **)outError {
427 if (![self checkLock:outError]) {
428 return NO;
429 }
430 return [self.session setInputDataSource:dataSource error:outError];
431}
432
433- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
434 error:(NSError **)outError {
435 if (![self checkLock:outError]) {
436 return NO;
437 }
438 return [self.session setOutputDataSource:dataSource error:outError];
439}
440
441#pragma mark - Notifications
442
443- (void)handleInterruptionNotification:(NSNotification *)notification {
444 NSNumber* typeNumber =
445 notification.userInfo[AVAudioSessionInterruptionTypeKey];
446 AVAudioSessionInterruptionType type =
447 (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue;
448 switch (type) {
449 case AVAudioSessionInterruptionTypeBegan:
450 RTCLog(@"Audio session interruption began.");
451 self.isActive = NO;
tkchin93dd6342016-07-27 10:17:14 -0700452 self.isInterrupted = YES;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800453 [self notifyDidBeginInterruption];
454 break;
455 case AVAudioSessionInterruptionTypeEnded: {
456 RTCLog(@"Audio session interruption ended.");
tkchin93dd6342016-07-27 10:17:14 -0700457 self.isInterrupted = NO;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800458 [self updateAudioSessionAfterEvent];
459 NSNumber *optionsNumber =
460 notification.userInfo[AVAudioSessionInterruptionOptionKey];
461 AVAudioSessionInterruptionOptions options =
462 optionsNumber.unsignedIntegerValue;
463 BOOL shouldResume =
464 options & AVAudioSessionInterruptionOptionShouldResume;
465 [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume];
466 break;
467 }
468 }
469}
470
471- (void)handleRouteChangeNotification:(NSNotification *)notification {
472 // Get reason for current route change.
473 NSNumber* reasonNumber =
474 notification.userInfo[AVAudioSessionRouteChangeReasonKey];
475 AVAudioSessionRouteChangeReason reason =
476 (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
477 RTCLog(@"Audio route changed:");
478 switch (reason) {
479 case AVAudioSessionRouteChangeReasonUnknown:
480 RTCLog(@"Audio route changed: ReasonUnknown");
481 break;
482 case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
483 RTCLog(@"Audio route changed: NewDeviceAvailable");
484 break;
485 case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
486 RTCLog(@"Audio route changed: OldDeviceUnavailable");
487 break;
488 case AVAudioSessionRouteChangeReasonCategoryChange:
489 RTCLog(@"Audio route changed: CategoryChange to :%@",
490 self.session.category);
491 break;
492 case AVAudioSessionRouteChangeReasonOverride:
493 RTCLog(@"Audio route changed: Override");
494 break;
495 case AVAudioSessionRouteChangeReasonWakeFromSleep:
496 RTCLog(@"Audio route changed: WakeFromSleep");
497 break;
498 case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
499 RTCLog(@"Audio route changed: NoSuitableRouteForCategory");
500 break;
501 case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
502 RTCLog(@"Audio route changed: RouteConfigurationChange");
503 break;
504 }
505 AVAudioSessionRouteDescription* previousRoute =
506 notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
507 // Log previous route configuration.
508 RTCLog(@"Previous route: %@\nCurrent route:%@",
509 previousRoute, self.session.currentRoute);
510 [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute];
511}
512
513- (void)handleMediaServicesWereLost:(NSNotification *)notification {
514 RTCLog(@"Media services were lost.");
515 [self updateAudioSessionAfterEvent];
516 [self notifyMediaServicesWereLost];
517}
518
519- (void)handleMediaServicesWereReset:(NSNotification *)notification {
520 RTCLog(@"Media services were reset.");
521 [self updateAudioSessionAfterEvent];
522 [self notifyMediaServicesWereReset];
523}
524
henrikaf1363fd2016-09-27 06:06:44 -0700525- (void)handleSilenceSecondaryAudioHintNotification:(NSNotification *)notification {
526 // TODO(henrika): just adding logs here for now until we know if we are ever
527 // see this notification and might be affected by it or if further actions
528 // are required.
529 NSNumber *typeNumber =
530 notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey];
531 AVAudioSessionSilenceSecondaryAudioHintType type =
532 (AVAudioSessionSilenceSecondaryAudioHintType)typeNumber.unsignedIntegerValue;
533 switch (type) {
534 case AVAudioSessionSilenceSecondaryAudioHintTypeBegin:
535 RTCLog(@"Another application's primary audio has started.");
536 break;
537 case AVAudioSessionSilenceSecondaryAudioHintTypeEnd:
538 RTCLog(@"Another application's primary audio has stopped.");
539 break;
540 }
541}
542
tkchin93dd6342016-07-27 10:17:14 -0700543- (void)handleApplicationDidBecomeActive:(NSNotification *)notification {
544 if (self.isInterrupted) {
545 RTCLog(@"Application became active after an interruption. Treating as interruption end.");
546 self.isInterrupted = NO;
547 [self updateAudioSessionAfterEvent];
548 [self notifyDidEndInterruptionWithShouldResumeSession:YES];
549 }
550}
551
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800552#pragma mark - Private
553
554+ (NSError *)lockError {
555 NSDictionary *userInfo = @{
556 NSLocalizedDescriptionKey:
557 @"Must call lockForConfiguration before calling this method."
558 };
559 NSError *error =
560 [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
561 code:kRTCAudioSessionErrorLockRequired
562 userInfo:userInfo];
563 return error;
564}
565
tkchine54467f2016-03-15 16:54:03 -0700566- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800567 @synchronized(self) {
tkchine54467f2016-03-15 16:54:03 -0700568 // Note: this returns a copy.
569 return _delegates;
570 }
571}
572
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700573// TODO(tkchin): check for duplicates.
tkchine54467f2016-03-15 16:54:03 -0700574- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
575 @synchronized(self) {
576 _delegates.insert(_delegates.begin(), delegate);
577 }
578}
579
580- (void)removeZeroedDelegates {
581 @synchronized(self) {
tkchinefdd9302016-04-11 12:00:59 -0700582 _delegates.erase(
583 std::remove_if(_delegates.begin(),
584 _delegates.end(),
585 [](id delegate) -> bool { return delegate == nil; }),
586 _delegates.end());
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800587 }
588}
589
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700590- (int)activationCount {
591 return _activationCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800592}
593
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700594- (int)incrementActivationCount {
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800595 RTCLog(@"Incrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700596 return rtc::AtomicOps::Increment(&_activationCount);
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800597}
598
599- (NSInteger)decrementActivationCount {
600 RTCLog(@"Decrementing activation count.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700601 return rtc::AtomicOps::Decrement(&_activationCount);
602}
603
604- (int)webRTCSessionCount {
605 return _webRTCSessionCount;
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800606}
607
tkchind2511962016-05-06 18:54:15 -0700608- (BOOL)canPlayOrRecord {
609 return !self.useManualAudio || self.isAudioEnabled;
610}
611
tkchin93dd6342016-07-27 10:17:14 -0700612- (BOOL)isInterrupted {
613 @synchronized(self) {
614 return _isInterrupted;
615 }
616}
617
618- (void)setIsInterrupted:(BOOL)isInterrupted {
619 @synchronized(self) {
620 if (_isInterrupted == isInterrupted) {
621 return;
622 }
623 _isInterrupted = isInterrupted;
624 }
625}
626
tkchin9f987d32016-03-12 20:06:28 -0800627- (BOOL)checkLock:(NSError **)outError {
628 // Check ivar instead of trying to acquire lock so that we won't accidentally
629 // acquire lock if it hasn't already been called.
630 if (!self.isLocked) {
631 if (outError) {
632 *outError = [RTCAudioSession lockError];
633 }
634 return NO;
635 }
636 return YES;
637}
638
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700639- (BOOL)beginWebRTCSession:(NSError **)outError {
640 if (outError) {
641 *outError = nil;
642 }
643 if (![self checkLock:outError]) {
644 return NO;
645 }
tkchind2511962016-05-06 18:54:15 -0700646 rtc::AtomicOps::Increment(&_webRTCSessionCount);
647 [self notifyDidStartPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700648 return YES;
649}
650
651- (BOOL)endWebRTCSession:(NSError **)outError {
652 if (outError) {
653 *outError = nil;
654 }
655 if (![self checkLock:outError]) {
656 return NO;
657 }
tkchind2511962016-05-06 18:54:15 -0700658 rtc::AtomicOps::Decrement(&_webRTCSessionCount);
659 [self notifyDidStopPlayOrRecord];
660 return YES;
661}
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700662
tkchind2511962016-05-06 18:54:15 -0700663- (BOOL)configureWebRTCSession:(NSError **)outError {
664 if (outError) {
665 *outError = nil;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700666 }
tkchind2511962016-05-06 18:54:15 -0700667 if (![self checkLock:outError]) {
668 return NO;
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700669 }
tkchind2511962016-05-06 18:54:15 -0700670 RTCLog(@"Configuring audio session for WebRTC.");
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700671
tkchind2511962016-05-06 18:54:15 -0700672 // Configure the AVAudioSession and activate it.
673 // Provide an error even if there isn't one so we can log it.
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700674 NSError *error = nil;
tkchind2511962016-05-06 18:54:15 -0700675 RTCAudioSessionConfiguration *webRTCConfig =
676 [RTCAudioSessionConfiguration webRTCConfiguration];
677 if (![self setConfiguration:webRTCConfig active:YES error:&error]) {
678 RTCLogError(@"Failed to set WebRTC audio configuration: %@",
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700679 error.localizedDescription);
tkchind2511962016-05-06 18:54:15 -0700680 [self unconfigureWebRTCSession:nil];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700681 if (outError) {
682 *outError = error;
683 }
684 return NO;
685 }
686
tkchind2511962016-05-06 18:54:15 -0700687 // Ensure that the device currently supports audio input.
688 // TODO(tkchin): Figure out if this is really necessary.
689 if (!self.inputAvailable) {
690 RTCLogError(@"No audio input path is available!");
691 [self unconfigureWebRTCSession:nil];
692 if (outError) {
693 *outError = [self configurationErrorWithDescription:@"No input path."];
694 }
695 return NO;
696 }
697
henrika2d014be2016-06-16 14:26:55 +0200698 // It can happen (e.g. in combination with BT devices) that the attempt to set
699 // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new
700 // configuration attempt using the sample rate that worked using the active
701 // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in
702 // combination with BT headsets. Using this "trick" seems to avoid a state
703 // where Core Audio asks for a different number of audio frames than what the
704 // session's I/O buffer duration corresponds to.
705 // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been
706 // tested on a limited set of iOS devices and BT devices.
707 double sessionSampleRate = self.sampleRate;
708 double preferredSampleRate = webRTCConfig.sampleRate;
709 if (sessionSampleRate != preferredSampleRate) {
710 RTCLogWarning(
711 @"Current sample rate (%.2f) is not the preferred rate (%.2f)",
712 sessionSampleRate, preferredSampleRate);
713 if (![self setPreferredSampleRate:sessionSampleRate
714 error:&error]) {
715 RTCLogError(@"Failed to set preferred sample rate: %@",
716 error.localizedDescription);
717 if (outError) {
718 *outError = error;
719 }
720 }
721 }
722
tkchind2511962016-05-06 18:54:15 -0700723 return YES;
724}
725
726- (BOOL)unconfigureWebRTCSession:(NSError **)outError {
727 if (outError) {
728 *outError = nil;
729 }
730 if (![self checkLock:outError]) {
731 return NO;
732 }
733 RTCLog(@"Unconfiguring audio session for WebRTC.");
734 [self setActive:NO error:outError];
735
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700736 return YES;
737}
738
739- (NSError *)configurationErrorWithDescription:(NSString *)description {
740 NSDictionary* userInfo = @{
741 NSLocalizedDescriptionKey: description,
742 };
743 return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain
744 code:kRTCAudioSessionErrorConfiguration
745 userInfo:userInfo];
746}
747
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800748- (void)updateAudioSessionAfterEvent {
749 BOOL shouldActivate = self.activationCount > 0;
750 AVAudioSessionSetActiveOptions options = shouldActivate ?
751 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation;
752 NSError *error = nil;
753 if ([self.session setActive:shouldActivate
754 withOptions:options
755 error:&error]) {
756 self.isActive = shouldActivate;
757 } else {
758 RTCLogError(@"Failed to set session active to %d. Error:%@",
759 shouldActivate, error.localizedDescription);
760 }
761}
762
tkchind2511962016-05-06 18:54:15 -0700763- (void)updateCanPlayOrRecord {
764 BOOL canPlayOrRecord = NO;
765 BOOL shouldNotify = NO;
766 @synchronized(self) {
767 canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled;
768 if (_canPlayOrRecord == canPlayOrRecord) {
769 return;
770 }
771 _canPlayOrRecord = canPlayOrRecord;
772 shouldNotify = YES;
773 }
774 if (shouldNotify) {
775 [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord];
776 }
777}
778
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800779- (void)notifyDidBeginInterruption {
tkchine54467f2016-03-15 16:54:03 -0700780 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700781 SEL sel = @selector(audioSessionDidBeginInterruption:);
782 if ([delegate respondsToSelector:sel]) {
783 [delegate audioSessionDidBeginInterruption:self];
784 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800785 }
786}
787
788- (void)notifyDidEndInterruptionWithShouldResumeSession:
789 (BOOL)shouldResumeSession {
tkchine54467f2016-03-15 16:54:03 -0700790 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700791 SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:);
792 if ([delegate respondsToSelector:sel]) {
793 [delegate audioSessionDidEndInterruption:self
794 shouldResumeSession:shouldResumeSession];
795 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800796 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800797}
798
799- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
800 previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
tkchine54467f2016-03-15 16:54:03 -0700801 for (auto delegate : self.delegates) {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700802 SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:);
803 if ([delegate respondsToSelector:sel]) {
804 [delegate audioSessionDidChangeRoute:self
805 reason:reason
806 previousRoute:previousRoute];
807 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800808 }
809}
810
811- (void)notifyMediaServicesWereLost {
tkchine54467f2016-03-15 16:54:03 -0700812 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800813 SEL sel = @selector(audioSessionMediaServerTerminated:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700814 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800815 [delegate audioSessionMediaServerTerminated:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700816 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800817 }
818}
819
820- (void)notifyMediaServicesWereReset {
tkchine54467f2016-03-15 16:54:03 -0700821 for (auto delegate : self.delegates) {
kthelgason1634e162017-02-07 02:48:55 -0800822 SEL sel = @selector(audioSessionMediaServerReset:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700823 if ([delegate respondsToSelector:sel]) {
kthelgason1634e162017-02-07 02:48:55 -0800824 [delegate audioSessionMediaServerReset:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700825 }
826 }
827}
828
tkchind2511962016-05-06 18:54:15 -0700829- (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700830 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700831 SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700832 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700833 [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700834 }
835 }
836}
837
tkchind2511962016-05-06 18:54:15 -0700838- (void)notifyDidStartPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700839 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700840 SEL sel = @selector(audioSessionDidStartPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700841 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700842 [delegate audioSessionDidStartPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700843 }
844 }
845}
846
tkchind2511962016-05-06 18:54:15 -0700847- (void)notifyDidStopPlayOrRecord {
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700848 for (auto delegate : self.delegates) {
tkchind2511962016-05-06 18:54:15 -0700849 SEL sel = @selector(audioSessionDidStopPlayOrRecord:);
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700850 if ([delegate respondsToSelector:sel]) {
tkchind2511962016-05-06 18:54:15 -0700851 [delegate audioSessionDidStopPlayOrRecord:self];
Tze Kwang Chin307a0922016-03-21 13:57:40 -0700852 }
Zeke Chinb3fb71c2016-02-18 15:44:07 -0800853 }
854}
855
856@end