Index: webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm |
diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ea7c546e8e9143decad7e60d08c729358ac155ca |
--- /dev/null |
+++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm |
@@ -0,0 +1,533 @@ |
+/* |
+ * Copyright 2016 The WebRTC Project Authors. All rights reserved. |
+ * |
+ * Use of this source code is governed by a BSD-style license |
+ * that can be found in the LICENSE file in the root of the source |
+ * tree. An additional intellectual property rights grant can be found |
+ * in the file PATENTS. All contributing project authors may |
+ * be found in the AUTHORS file in the root of the source tree. |
+ */ |
+ |
+#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" |
+ |
+#include "webrtc/base/checks.h" |
+ |
+#import "webrtc/base/objc/RTCLogging.h" |
+#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h" |
+ |
+NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession"; |
+NSInteger const kRTCAudioSessionErrorLockRequired = -1; |
+ |
+// This class needs to be thread-safe because it is accessed from many threads. |
+// TODO(tkchin): Consider more granular locking. We're not expecting a lot of |
+// lock contention so coarse locks should be fine for now. |
+@implementation RTCAudioSession { |
+ AVAudioSession *_session; |
+ NSHashTable *_delegates; |
+ NSInteger _activationCount; |
+ BOOL _isActive; |
+ BOOL _isLocked; |
+} |
+ |
+@synthesize session = _session; |
+@synthesize lock = _lock; |
+ |
++ (instancetype)sharedInstance { |
+ static dispatch_once_t onceToken; |
+ static RTCAudioSession *sharedInstance = nil; |
+ dispatch_once(&onceToken, ^{ |
+ sharedInstance = [[RTCAudioSession alloc] init]; |
+ }); |
+ return sharedInstance; |
+} |
+ |
+- (instancetype)init { |
+ if (self = [super init]) { |
+ _session = [AVAudioSession sharedInstance]; |
+ _delegates = [NSHashTable weakObjectsHashTable]; |
+ _lock = [[NSRecursiveLock alloc] init]; |
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; |
+ [center addObserver:self |
+ selector:@selector(handleInterruptionNotification:) |
+ name:AVAudioSessionInterruptionNotification |
+ object:nil]; |
+ [center addObserver:self |
+ selector:@selector(handleRouteChangeNotification:) |
+ name:AVAudioSessionRouteChangeNotification |
+ object:nil]; |
+ // TODO(tkchin): Maybe listen to SilenceSecondaryAudioHintNotification. |
+ [center addObserver:self |
+ selector:@selector(handleMediaServicesWereLost:) |
+ name:AVAudioSessionMediaServicesWereLostNotification |
+ object:nil]; |
+ [center addObserver:self |
+ selector:@selector(handleMediaServicesWereReset:) |
+ name:AVAudioSessionMediaServicesWereResetNotification |
+ object:nil]; |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [[NSNotificationCenter defaultCenter] removeObserver:self]; |
+} |
+ |
+- (void)setIsActive:(BOOL)isActive { |
+ @synchronized(self) { |
+ _isActive = isActive; |
+ } |
+} |
+ |
+- (BOOL)isActive { |
+ @synchronized(self) { |
+ return _isActive; |
+ } |
+} |
+ |
+- (BOOL)isLocked { |
+ @synchronized(self) { |
+ return _isLocked; |
+ } |
+} |
+ |
+- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate { |
+ @synchronized(self) { |
+ [_delegates addObject:delegate]; |
+ } |
+} |
+ |
+- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate { |
+ @synchronized(self) { |
+ [_delegates removeObject:delegate]; |
+ } |
+} |
+ |
+- (void)lockForConfiguration { |
+ [_lock lock]; |
+ @synchronized(self) { |
+ _isLocked = YES; |
+ } |
+} |
+ |
+- (void)unlockForConfiguration { |
+ // Don't let threads other than the one that called lockForConfiguration |
+ // unlock. |
+ if ([_lock tryLock]) { |
+ @synchronized(self) { |
+ _isLocked = NO; |
+ } |
+ // One unlock for the tryLock, and another one to actually unlock. If this |
+ // was called without anyone calling lock, the underlying NSRecursiveLock |
+ // should spit out an error. |
+ [_lock unlock]; |
+ [_lock unlock]; |
+ } |
+} |
+ |
+#pragma mark - AVAudioSession proxy methods |
+ |
+- (NSString *)category { |
+ return self.session.category; |
+} |
+ |
+- (AVAudioSessionCategoryOptions)categoryOptions { |
+ return self.session.categoryOptions; |
+} |
+ |
+- (NSString *)mode { |
+ return self.session.mode; |
+} |
+ |
+- (BOOL)secondaryAudioShouldBeSilencedHint { |
+ return self.session.secondaryAudioShouldBeSilencedHint; |
+} |
+ |
+- (AVAudioSessionRouteDescription *)currentRoute { |
+ return self.session.currentRoute; |
+} |
+ |
+- (NSInteger)maximumInputNumberOfChannels { |
+ return self.session.maximumInputNumberOfChannels; |
+} |
+ |
+- (NSInteger)maximumOutputNumberOfChannels { |
+ return self.session.maximumOutputNumberOfChannels; |
+} |
+ |
+- (float)inputGain { |
+ return self.session.inputGain; |
+} |
+ |
+- (BOOL)inputGainSettable { |
+ return self.session.inputGainSettable; |
+} |
+ |
+- (BOOL)inputAvailable { |
+ return self.session.inputAvailable; |
+} |
+ |
+- (NSArray<AVAudioSessionDataSourceDescription *> *)inputDataSources { |
+ return self.session.inputDataSources; |
+} |
+ |
+- (AVAudioSessionDataSourceDescription *)inputDataSource { |
+ return self.session.inputDataSource; |
+} |
+ |
+- (NSArray<AVAudioSessionDataSourceDescription *> *)outputDataSources { |
+ return self.session.outputDataSources; |
+} |
+ |
+- (AVAudioSessionDataSourceDescription *)outputDataSource { |
+ return self.session.outputDataSource; |
+} |
+ |
+- (double)sampleRate { |
+ return self.session.sampleRate; |
+} |
+ |
+- (NSInteger)inputNumberOfChannels { |
+ return self.session.inputNumberOfChannels; |
+} |
+ |
+- (NSInteger)outputNumberOfChannels { |
+ return self.session.outputNumberOfChannels; |
+} |
+ |
+- (float)outputVolume { |
+ return self.session.outputVolume; |
+} |
+ |
+- (NSTimeInterval)inputLatency { |
+ return self.session.inputLatency; |
+} |
+ |
+- (NSTimeInterval)outputLatency { |
+ return self.session.outputLatency; |
+} |
+ |
+- (NSTimeInterval)IOBufferDuration { |
+ return self.session.IOBufferDuration; |
+} |
+ |
+- (BOOL)setActive:(BOOL)active |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ NSInteger activationCount = self.activationCount; |
+ if (!active && activationCount == 0) { |
+ RTCLogWarning(@"Attempting to deactivate without prior activation."); |
+ } |
+ BOOL success = YES; |
+ BOOL isActive = self.isActive; |
+ // Keep a local error so we can log it. |
+ NSError *error = nil; |
+ BOOL shouldSetActive = |
+ (active && !isActive) || (!active && isActive && activationCount == 1); |
+ // Attempt to activate if we're not active. |
+ // Attempt to deactivate if we're active and it's the last unbalanced call. |
+ if (shouldSetActive) { |
+ AVAudioSession *session = self.session; |
+ // AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation is used to ensure |
+ // that other audio sessions that were interrupted by our session can return |
+ // to their active state. It is recommended for VoIP apps to use this |
+ // option. |
+ AVAudioSessionSetActiveOptions options = |
+ active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation; |
+ success = [session setActive:active |
+ withOptions:options |
+ error:&error]; |
+ if (outError) { |
+ *outError = error; |
+ } |
+ } |
+ if (success) { |
+ if (shouldSetActive) { |
+ self.isActive = active; |
+ } |
+ if (active) { |
+ [self incrementActivationCount]; |
+ } |
+ } else { |
+ RTCLogError(@"Failed to setActive:%d. Error: %@", active, error); |
+ } |
+ // Decrement activation count on deactivation whether or not it succeeded. |
+ if (!active) { |
+ [self decrementActivationCount]; |
+ } |
+ RTCLog(@"Number of current activations: %ld", (long)self.activationCount); |
+ return success; |
+} |
+ |
+- (BOOL)setCategory:(NSString *)category |
+ withOptions:(AVAudioSessionCategoryOptions)options |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setCategory:category withOptions:options error:outError]; |
+} |
+ |
+- (BOOL)setMode:(NSString *)mode error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setMode:mode error:outError]; |
+} |
+ |
+- (BOOL)setInputGain:(float)gain error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setInputGain:gain error:outError]; |
+} |
+ |
+- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setPreferredSampleRate:sampleRate error:outError]; |
+} |
+ |
+- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setPreferredIOBufferDuration:duration error:outError]; |
+} |
+ |
+- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setPreferredInputNumberOfChannels:count error:outError]; |
+} |
+- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setPreferredOutputNumberOfChannels:count error:outError]; |
+} |
+ |
+- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session overrideOutputAudioPort:portOverride error:outError]; |
+} |
+ |
+- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setPreferredInput:inPort error:outError]; |
+} |
+ |
+- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setInputDataSource:dataSource error:outError]; |
+} |
+ |
+- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource |
+ error:(NSError **)outError { |
+ if (![self checkLock:outError]) { |
+ return NO; |
+ } |
+ return [self.session setOutputDataSource:dataSource error:outError]; |
+} |
+ |
+#pragma mark - Notifications |
+ |
+- (void)handleInterruptionNotification:(NSNotification *)notification { |
+ NSNumber* typeNumber = |
+ notification.userInfo[AVAudioSessionInterruptionTypeKey]; |
+ AVAudioSessionInterruptionType type = |
+ (AVAudioSessionInterruptionType)typeNumber.unsignedIntegerValue; |
+ switch (type) { |
+ case AVAudioSessionInterruptionTypeBegan: |
+ RTCLog(@"Audio session interruption began."); |
+ self.isActive = NO; |
+ [self notifyDidBeginInterruption]; |
+ break; |
+ case AVAudioSessionInterruptionTypeEnded: { |
+ RTCLog(@"Audio session interruption ended."); |
+ [self updateAudioSessionAfterEvent]; |
+ NSNumber *optionsNumber = |
+ notification.userInfo[AVAudioSessionInterruptionOptionKey]; |
+ AVAudioSessionInterruptionOptions options = |
+ optionsNumber.unsignedIntegerValue; |
+ BOOL shouldResume = |
+ options & AVAudioSessionInterruptionOptionShouldResume; |
+ [self notifyDidEndInterruptionWithShouldResumeSession:shouldResume]; |
+ break; |
+ } |
+ } |
+} |
+ |
+- (void)handleRouteChangeNotification:(NSNotification *)notification { |
+ // Get reason for current route change. |
+ NSNumber* reasonNumber = |
+ notification.userInfo[AVAudioSessionRouteChangeReasonKey]; |
+ AVAudioSessionRouteChangeReason reason = |
+ (AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue; |
+ RTCLog(@"Audio route changed:"); |
+ switch (reason) { |
+ case AVAudioSessionRouteChangeReasonUnknown: |
+ RTCLog(@"Audio route changed: ReasonUnknown"); |
+ break; |
+ case AVAudioSessionRouteChangeReasonNewDeviceAvailable: |
+ RTCLog(@"Audio route changed: NewDeviceAvailable"); |
+ break; |
+ case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: |
+ RTCLog(@"Audio route changed: OldDeviceUnavailable"); |
+ break; |
+ case AVAudioSessionRouteChangeReasonCategoryChange: |
+ RTCLog(@"Audio route changed: CategoryChange to :%@", |
+ self.session.category); |
+ break; |
+ case AVAudioSessionRouteChangeReasonOverride: |
+ RTCLog(@"Audio route changed: Override"); |
+ break; |
+ case AVAudioSessionRouteChangeReasonWakeFromSleep: |
+ RTCLog(@"Audio route changed: WakeFromSleep"); |
+ break; |
+ case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: |
+ RTCLog(@"Audio route changed: NoSuitableRouteForCategory"); |
+ break; |
+ case AVAudioSessionRouteChangeReasonRouteConfigurationChange: |
+ RTCLog(@"Audio route changed: RouteConfigurationChange"); |
+ break; |
+ } |
+ AVAudioSessionRouteDescription* previousRoute = |
+ notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey]; |
+ // Log previous route configuration. |
+ RTCLog(@"Previous route: %@\nCurrent route:%@", |
+ previousRoute, self.session.currentRoute); |
+ [self notifyDidChangeRouteWithReason:reason previousRoute:previousRoute]; |
+} |
+ |
+- (void)handleMediaServicesWereLost:(NSNotification *)notification { |
+ RTCLog(@"Media services were lost."); |
+ [self updateAudioSessionAfterEvent]; |
+ [self notifyMediaServicesWereLost]; |
+} |
+ |
+- (void)handleMediaServicesWereReset:(NSNotification *)notification { |
+ RTCLog(@"Media services were reset."); |
+ [self updateAudioSessionAfterEvent]; |
+ [self notifyMediaServicesWereReset]; |
+} |
+ |
+#pragma mark - Private |
+ |
++ (NSError *)lockError { |
+ NSDictionary *userInfo = @{ |
+ NSLocalizedDescriptionKey: |
+ @"Must call lockForConfiguration before calling this method." |
+ }; |
+ NSError *error = |
+ [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain |
+ code:kRTCAudioSessionErrorLockRequired |
+ userInfo:userInfo]; |
+ return error; |
+} |
+ |
+- (BOOL)checkLock:(NSError **)outError { |
+ // Check ivar instead of trying to acquire lock so that we won't accidentally |
+ // acquire lock if it hasn't already been called. |
+ if (!self.isLocked) { |
+ if (outError) { |
+ *outError = [RTCAudioSession lockError]; |
+ } |
+ return NO; |
+ } |
+ return YES; |
+} |
+ |
+- (NSSet *)delegates { |
+ @synchronized(self) { |
+ return _delegates.setRepresentation; |
+ } |
+} |
+ |
+- (NSInteger)activationCount { |
+ @synchronized(self) { |
+ return _activationCount; |
+ } |
+} |
+ |
+- (NSInteger)incrementActivationCount { |
+ RTCLog(@"Incrementing activation count."); |
+ @synchronized(self) { |
+ return ++_activationCount; |
+ } |
+} |
+ |
+- (NSInteger)decrementActivationCount { |
+ RTCLog(@"Decrementing activation count."); |
+ @synchronized(self) { |
+ return --_activationCount; |
+ } |
+} |
+ |
+- (void)updateAudioSessionAfterEvent { |
+ BOOL shouldActivate = self.activationCount > 0; |
+ AVAudioSessionSetActiveOptions options = shouldActivate ? |
+ 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation; |
+ NSError *error = nil; |
+ if ([self.session setActive:shouldActivate |
+ withOptions:options |
+ error:&error]) { |
+ self.isActive = shouldActivate; |
+ } else { |
+ RTCLogError(@"Failed to set session active to %d. Error:%@", |
+ shouldActivate, error.localizedDescription); |
+ } |
+} |
+ |
+- (void)notifyDidBeginInterruption { |
+ for (id<RTCAudioSessionDelegate> delegate in self.delegates) { |
+ [delegate audioSessionDidBeginInterruption:self]; |
+ } |
+} |
+ |
+- (void)notifyDidEndInterruptionWithShouldResumeSession: |
+ (BOOL)shouldResumeSession { |
+ for (id<RTCAudioSessionDelegate> delegate in self.delegates) { |
+ [delegate audioSessionDidEndInterruption:self |
+ shouldResumeSession:shouldResumeSession]; |
+ } |
+ |
+} |
+ |
+- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason |
+ previousRoute:(AVAudioSessionRouteDescription *)previousRoute { |
+ for (id<RTCAudioSessionDelegate> delegate in self.delegates) { |
+ [delegate audioSessionDidChangeRoute:self |
+ reason:reason |
+ previousRoute:previousRoute]; |
+ } |
+} |
+ |
+- (void)notifyMediaServicesWereLost { |
+ for (id<RTCAudioSessionDelegate> delegate in self.delegates) { |
+ [delegate audioSessionMediaServicesWereLost:self]; |
+ } |
+} |
+ |
+- (void)notifyMediaServicesWereReset { |
+ for (id<RTCAudioSessionDelegate> delegate in self.delegates) { |
+ [delegate audioSessionMediaServicesWereReset:self]; |
+ } |
+} |
+ |
+@end |