diff --git a/webrtc/modules/audio_device/BUILD.gn b/webrtc/modules/audio_device/BUILD.gn index 5897176845..307a615a4f 100644 --- a/webrtc/modules/audio_device/BUILD.gn +++ b/webrtc/modules/audio_device/BUILD.gn @@ -17,6 +17,14 @@ config("audio_device_config") { } source_set("audio_device") { + deps = [ + "../..:webrtc_common", + "../../base:rtc_base_approved", + "../../common_audio", + "../../system_wrappers", + "../utility", + ] + sources = [ "audio_device_buffer.cc", "audio_device_buffer.h", @@ -122,10 +130,14 @@ source_set("audio_device") { ] } if (is_ios) { + deps += [ "../../base:rtc_base_objc" ] sources += [ "ios/audio_device_ios.h", "ios/audio_device_ios.mm", "ios/audio_device_not_implemented_ios.mm", + "ios/objc/RTCAudioSession+Private.h", + "ios/objc/RTCAudioSession.h", + "ios/objc/RTCAudioSession.mm", ] cflags += [ "-fobjc-arc" ] # CLANG_ENABLE_OBJC_ARC = YES. libs = [ @@ -175,12 +187,4 @@ source_set("audio_device") { # See http://code.google.com/p/webrtc/issues/detail?id=163 for details. configs -= [ "//build/config/clang:find_bad_constructs" ] } - - deps = [ - "../..:webrtc_common", - "../../base:rtc_base_approved", - "../../common_audio", - "../../system_wrappers", - "../utility", - ] } diff --git a/webrtc/modules/audio_device/audio_device.gypi b/webrtc/modules/audio_device/audio_device.gypi index 2db9f1e17b..60281edaea 100644 --- a/webrtc/modules/audio_device/audio_device.gypi +++ b/webrtc/modules/audio_device/audio_device.gypi @@ -166,10 +166,16 @@ }, }], ['OS=="ios"', { + 'dependencies': [ + '<(webrtc_root)/base/base.gyp:rtc_base_objc', + ], 'sources': [ 'ios/audio_device_ios.h', 'ios/audio_device_ios.mm', 'ios/audio_device_not_implemented_ios.mm', + 'ios/objc/RTCAudioSession+Private.h', + 'ios/objc/RTCAudioSession.h', + 'ios/objc/RTCAudioSession.mm', ], 'xcode_settings': { 'CLANG_ENABLE_OBJC_ARC': 'YES', diff --git a/webrtc/modules/audio_device/ios/audio_device_ios.mm b/webrtc/modules/audio_device/ios/audio_device_ios.mm index f6dee5b3cf..a0ea2a0f45 100644 --- a/webrtc/modules/audio_device/ios/audio_device_ios.mm +++ b/webrtc/modules/audio_device/ios/audio_device_ios.mm @@ -25,18 +25,10 @@ #include "webrtc/modules/audio_device/fine_audio_buffer.h" #include "webrtc/modules/utility/include/helpers_ios.h" +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" + namespace webrtc { -// Protects |g_audio_session_users|. -static rtc::GlobalLockPod g_lock; - -// Counts number of users (=instances of this object) who needs an active -// audio session. This variable is used to ensure that we only activate an audio -// session for the first user and deactivate it for the last. -// Member is static to ensure that the value is counted for all instances -// and not per instance. -static int g_audio_session_users GUARDED_BY(g_lock) = 0; - #define LOGI() LOG(LS_INFO) << "AudioDeviceIOS::" #define LOG_AND_RETURN_IF_ERROR(error, message) \ @@ -97,10 +89,10 @@ using ios::CheckAndLogError; // Verifies that the current audio session supports input audio and that the // required category and mode are enabled. -static bool VerifyAudioSession(AVAudioSession* session) { +static bool VerifyAudioSession(RTCAudioSession* session) { LOG(LS_INFO) << "VerifyAudioSession"; // Ensure that the device currently supports audio input. - if (!session.isInputAvailable) { + if (!session.inputAvailable) { LOG(LS_ERROR) << "No audio input path is available!"; return false; } @@ -121,93 +113,89 @@ static bool VerifyAudioSession(AVAudioSession* session) { // Activates an audio session suitable for full duplex VoIP sessions when // |activate| is true. Also sets the preferred sample rate and IO buffer // duration. Deactivates an active audio session if |activate| is set to false. -static bool ActivateAudioSession(AVAudioSession* session, bool activate) - EXCLUSIVE_LOCKS_REQUIRED(g_lock) { +static bool ActivateAudioSession(RTCAudioSession* session, bool activate) { LOG(LS_INFO) << "ActivateAudioSession(" << activate << ")"; - @autoreleasepool { - NSError* error = nil; - BOOL success = NO; - if (!activate) { - // Deactivate the audio session using an extra option and then return. - // 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. - success = [session - setActive:NO - withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation - error:&error]; - return CheckAndLogError(success, error); - } + NSError* error = nil; + BOOL success = NO; - // Go ahead and active our own audio session since |activate| is true. - // Use a category which supports simultaneous recording and playback. - // By default, using this category implies that our app’s audio is - // nonmixable, hence activating the session will interrupt any other - // audio sessions which are also nonmixable. - if (session.category != AVAudioSessionCategoryPlayAndRecord) { - error = nil; - success = [session setCategory:AVAudioSessionCategoryPlayAndRecord - withOptions:AVAudioSessionCategoryOptionAllowBluetooth - error:&error]; - RTC_DCHECK(CheckAndLogError(success, error)); - } - - // Specify mode for two-way voice communication (e.g. VoIP). - if (session.mode != AVAudioSessionModeVoiceChat) { - error = nil; - success = [session setMode:AVAudioSessionModeVoiceChat error:&error]; - RTC_DCHECK(CheckAndLogError(success, error)); - } - - // Set the session's sample rate or the hardware sample rate. - // It is essential that we use the same sample rate as stream format - // to ensure that the I/O unit does not have to do sample rate conversion. - error = nil; - success = - [session setPreferredSampleRate:kPreferredSampleRate error:&error]; - RTC_DCHECK(CheckAndLogError(success, error)); - - // Set the preferred audio I/O buffer duration, in seconds. - error = nil; - success = [session setPreferredIOBufferDuration:kPreferredIOBufferDuration - error:&error]; - RTC_DCHECK(CheckAndLogError(success, error)); - - // Activate the audio session. Activation can fail if another active audio - // session (e.g. phone call) has higher priority than ours. - error = nil; - success = [session setActive:YES error:&error]; - if (!CheckAndLogError(success, error)) { - return false; - } - - // Ensure that the active audio session has the correct category and mode. - if (!VerifyAudioSession(session)) { - LOG(LS_ERROR) << "Failed to verify audio session category and mode"; - return false; - } - - // Try to set the preferred number of hardware audio channels. These calls - // must be done after setting the audio session’s category and mode and - // activating the session. - // We try to use mono in both directions to save resources and format - // conversions in the audio unit. Some devices does only support stereo; - // e.g. wired headset on iPhone 6. - // TODO(henrika): add support for stereo if needed. - error = nil; - success = - [session setPreferredInputNumberOfChannels:kPreferredNumberOfChannels - error:&error]; - RTC_DCHECK(CheckAndLogError(success, error)); - error = nil; - success = - [session setPreferredOutputNumberOfChannels:kPreferredNumberOfChannels - error:&error]; - RTC_DCHECK(CheckAndLogError(success, error)); - return true; + [session lockForConfiguration]; + if (!activate) { + success = [session setActive:NO + error:&error]; + [session unlockForConfiguration]; + return CheckAndLogError(success, error); } + + // Go ahead and active our own audio session since |activate| is true. + // Use a category which supports simultaneous recording and playback. + // By default, using this category implies that our app’s audio is + // nonmixable, hence activating the session will interrupt any other + // audio sessions which are also nonmixable. + if (session.category != AVAudioSessionCategoryPlayAndRecord) { + error = nil; + success = [session setCategory:AVAudioSessionCategoryPlayAndRecord + withOptions:AVAudioSessionCategoryOptionAllowBluetooth + error:&error]; + RTC_DCHECK(CheckAndLogError(success, error)); + } + + // Specify mode for two-way voice communication (e.g. VoIP). + if (session.mode != AVAudioSessionModeVoiceChat) { + error = nil; + success = [session setMode:AVAudioSessionModeVoiceChat error:&error]; + RTC_DCHECK(CheckAndLogError(success, error)); + } + + // Set the session's sample rate or the hardware sample rate. + // It is essential that we use the same sample rate as stream format + // to ensure that the I/O unit does not have to do sample rate conversion. + error = nil; + success = + [session setPreferredSampleRate:kPreferredSampleRate error:&error]; + RTC_DCHECK(CheckAndLogError(success, error)); + + // Set the preferred audio I/O buffer duration, in seconds. + error = nil; + success = [session setPreferredIOBufferDuration:kPreferredIOBufferDuration + error:&error]; + RTC_DCHECK(CheckAndLogError(success, error)); + + // Activate the audio session. Activation can fail if another active audio + // session (e.g. phone call) has higher priority than ours. + error = nil; + success = [session setActive:YES error:&error]; + if (!CheckAndLogError(success, error)) { + [session unlockForConfiguration]; + return false; + } + + // Ensure that the active audio session has the correct category and mode. + if (!VerifyAudioSession(session)) { + LOG(LS_ERROR) << "Failed to verify audio session category and mode"; + [session unlockForConfiguration]; + return false; + } + + // Try to set the preferred number of hardware audio channels. These calls + // must be done after setting the audio session’s category and mode and + // activating the session. + // We try to use mono in both directions to save resources and format + // conversions in the audio unit. Some devices does only support stereo; + // e.g. wired headset on iPhone 6. + // TODO(henrika): add support for stereo if needed. + error = nil; + success = + [session setPreferredInputNumberOfChannels:kPreferredNumberOfChannels + error:&error]; + RTC_DCHECK(CheckAndLogError(success, error)); + error = nil; + success = + [session setPreferredOutputNumberOfChannels:kPreferredNumberOfChannels + error:&error]; + RTC_DCHECK(CheckAndLogError(success, error)); + [session unlockForConfiguration]; + return true; } // An application can create more than one ADM and start audio streaming @@ -215,24 +203,8 @@ static bool ActivateAudioSession(AVAudioSession* session, bool activate) // session once (for the first one) and deactivate it once (for the last). static bool ActivateAudioSession() { LOGI() << "ActivateAudioSession"; - rtc::GlobalLockScope ls(&g_lock); - if (g_audio_session_users == 0) { - // The system provides an audio session object upon launch of an - // application. However, we must initialize the session in order to - // handle interruptions. Implicit initialization occurs when obtaining - // a reference to the AVAudioSession object. - AVAudioSession* session = [AVAudioSession sharedInstance]; - // Try to activate the audio session and ask for a set of preferred audio - // parameters. - if (!ActivateAudioSession(session, true)) { - LOG(LS_ERROR) << "Failed to activate the audio session"; - return false; - } - LOG(LS_INFO) << "The audio session is now activated"; - } - ++g_audio_session_users; - LOG(LS_INFO) << "Number of audio session users: " << g_audio_session_users; - return true; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + return ActivateAudioSession(session, true); } // If more than one object is using the audio session, ensure that only the @@ -240,18 +212,8 @@ static bool ActivateAudioSession() { // only as needed and deactivate it when you are not using audio". static bool DeactivateAudioSession() { LOGI() << "DeactivateAudioSession"; - rtc::GlobalLockScope ls(&g_lock); - if (g_audio_session_users == 1) { - AVAudioSession* session = [AVAudioSession sharedInstance]; - if (!ActivateAudioSession(session, false)) { - LOG(LS_ERROR) << "Failed to deactivate the audio session"; - return false; - } - LOG(LS_INFO) << "Our audio session is now deactivated"; - } - --g_audio_session_users; - LOG(LS_INFO) << "Number of audio session users: " << g_audio_session_users; - return true; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + return ActivateAudioSession(session, false); } #if !defined(NDEBUG) @@ -344,13 +306,6 @@ int32_t AudioDeviceIOS::Terminate() { StopPlayout(); StopRecording(); initialized_ = false; - { - rtc::GlobalLockScope ls(&g_lock); - if (g_audio_session_users != 0) { - LOG(LS_WARNING) << "Object is destructed with an active audio session"; - } - RTC_DCHECK_GE(g_audio_session_users, 0); - } return 0; } @@ -456,7 +411,8 @@ int32_t AudioDeviceIOS::StopRecording() { int32_t AudioDeviceIOS::SetLoudspeakerStatus(bool enable) { LOGI() << "SetLoudspeakerStatus(" << enable << ")"; - AVAudioSession* session = [AVAudioSession sharedInstance]; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + [session lockForConfiguration]; NSString* category = session.category; AVAudioSessionCategoryOptions options = session.categoryOptions; // Respect old category options if category is @@ -476,12 +432,13 @@ int32_t AudioDeviceIOS::SetLoudspeakerStatus(bool enable) { withOptions:options error:&error]; ios::CheckAndLogError(success, error); + [session unlockForConfiguration]; return (error == nil) ? 0 : -1; } int32_t AudioDeviceIOS::GetLoudspeakerStatus(bool& enabled) const { LOGI() << "GetLoudspeakerStatus"; - AVAudioSession* session = [AVAudioSession sharedInstance]; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; AVAudioSessionCategoryOptions options = session.categoryOptions; enabled = options & AVAudioSessionCategoryOptionDefaultToSpeaker; return 0; @@ -618,7 +575,7 @@ void AudioDeviceIOS::RegisterNotificationObservers() { // Only restart audio for a valid route change and if the // session sample rate has changed. - AVAudioSession* session = [AVAudioSession sharedInstance]; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; const double session_sample_rate = session.sampleRate; LOG(LS_INFO) << "session sample rate: " << session_sample_rate; if (playout_parameters_.sample_rate() != session_sample_rate) { @@ -672,7 +629,7 @@ void AudioDeviceIOS::UnregisterNotificationObservers() { void AudioDeviceIOS::SetupAudioBuffersForActiveAudioSession() { LOGI() << "SetupAudioBuffersForActiveAudioSession"; // Verify the current values once the audio session has been activated. - AVAudioSession* session = [AVAudioSession sharedInstance]; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; LOG(LS_INFO) << " sample rate: " << session.sampleRate; LOG(LS_INFO) << " IO buffer duration: " << session.IOBufferDuration; LOG(LS_INFO) << " output channels: " << session.outputNumberOfChannels; @@ -954,7 +911,7 @@ bool AudioDeviceIOS::InitPlayOrRecord() { } // Ensure that the active audio session has the correct category and mode. - AVAudioSession* session = [AVAudioSession sharedInstance]; + RTCAudioSession* session = [RTCAudioSession sharedInstance]; if (!VerifyAudioSession(session)) { DeactivateAudioSession(); LOG(LS_ERROR) << "Failed to verify audio session category and mode"; diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h new file mode 100644 index 0000000000..523a4c1094 --- /dev/null +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h @@ -0,0 +1,30 @@ +/* + * 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" + +NS_ASSUME_NONNULL_BEGIN + +@interface RTCAudioSession () + +/** The lock that guards access to AVAudioSession methods. */ +@property(nonatomic, strong) NSRecursiveLock *lock; + +/** The delegates. */ +@property(nonatomic, readonly) NSSet *delegates; + +/** Number of times setActive:YES has succeeded without a balanced call to + * setActive:NO. + */ +@property(nonatomic, readonly) NSInteger activationCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h new file mode 100644 index 0000000000..e797a05b4b --- /dev/null +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.h @@ -0,0 +1,153 @@ +/* + * 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString * const kRTCAudioSessionErrorDomain; +extern NSInteger const kRTCAudioSessionErrorLockRequired; + +@class RTCAudioSession; + +// Surfaces AVAudioSession events. WebRTC will listen directly for notifications +// from AVAudioSession and handle them before calling these delegate methods, +// at which point applications can perform additional processing if required. +@protocol RTCAudioSessionDelegate + +/** Called when AVAudioSession starts an interruption event. */ +- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session; + +/** Called when AVAudioSession ends an interruption event. */ +- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session + shouldResumeSession:(BOOL)shouldResumeSession; + +/** Called when AVAudioSession changes the route. */ +- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session + reason:(AVAudioSessionRouteChangeReason)reason + previousRoute:(AVAudioSessionRouteDescription *)previousRoute; + +/** Called when AVAudioSession media server terminates. */ +- (void)audioSessionMediaServicesWereLost:(RTCAudioSession *)session; + +/** Called when AVAudioSession media server restarts. */ +- (void)audioSessionMediaServicesWereReset:(RTCAudioSession *)session; + +// TODO(tkchin): Maybe handle SilenceSecondaryAudioHintNotification. + +@end + +/** Proxy class for AVAudioSession that adds a locking mechanism similar to + * AVCaptureDevice. This is used to that interleaving configurations between + * WebRTC and the application layer are avoided. Only setter methods are + * currently proxied. Getters can be accessed directly off AVAudioSession. + * + * RTCAudioSession also coordinates activation so that the audio session is + * activated only once. See |setActive:error:|. + */ +@interface RTCAudioSession : NSObject + +/** Convenience property to access the AVAudioSession singleton. Callers should + * not call setters on AVAudioSession directly, but other method invocations + * are fine. + */ +@property(nonatomic, readonly) AVAudioSession *session; + +/** Our best guess at whether the session is active based on results of calls to + * AVAudioSession. + */ +@property(nonatomic, readonly) BOOL isActive; +/** Whether RTCAudioSession is currently locked for configuration. */ +@property(nonatomic, readonly) BOOL isLocked; + +// Proxy properties. +@property(readonly) NSString *category; +@property(readonly) AVAudioSessionCategoryOptions categoryOptions; +@property(readonly) NSString *mode; +@property(readonly) BOOL secondaryAudioShouldBeSilencedHint; +@property(readonly) AVAudioSessionRouteDescription *currentRoute; +@property(readonly) NSInteger maximumInputNumberOfChannels; +@property(readonly) NSInteger maximumOutputNumberOfChannels; +@property(readonly) float inputGain; +@property(readonly) BOOL inputGainSettable; +@property(readonly) BOOL inputAvailable; +@property(readonly, nullable) + NSArray * inputDataSources; +@property(readonly, nullable) + AVAudioSessionDataSourceDescription *inputDataSource; +@property(readonly, nullable) + NSArray * outputDataSources; +@property(readonly, nullable) + AVAudioSessionDataSourceDescription *outputDataSource; +@property(readonly) double sampleRate; +@property(readonly) NSInteger inputNumberOfChannels; +@property(readonly) NSInteger outputNumberOfChannels; +@property(readonly) float outputVolume; +@property(readonly) NSTimeInterval inputLatency; +@property(readonly) NSTimeInterval outputLatency; +@property(readonly) NSTimeInterval IOBufferDuration; + +/** Default constructor. Do not call init. */ ++ (instancetype)sharedInstance; + +/** Adds a delegate, which is held weakly. Even though it's held weakly, callers + * should still call |removeDelegate| when it's no longer required to ensure + * proper dealloc. This is due to internal use of an NSHashTable. + */ +- (void)addDelegate:(id)delegate; +/** Removes an added delegate. */ +- (void)removeDelegate:(id)delegate; + +/** Request exclusive access to the audio session for configuration. This call + * will block if the lock is held by another object. + */ +- (void)lockForConfiguration; +/** Relinquishes exclusive access to the audio session. */ +- (void)unlockForConfiguration; + +/** If |active|, activates the audio session if it isn't already active. + * Successful calls must be balanced with a setActive:NO when activation is no + * longer required. If not |active|, deactivates the audio session if one is + * active and this is the last balanced call. When deactivating, the + * AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation option is passed to + * AVAudioSession. + */ +- (BOOL)setActive:(BOOL)active + error:(NSError **)outError; + +// The following methods are proxies for the associated methods on +// AVAudioSession. |lockForConfiguration| must be called before using them +// otherwise they will fail with kRTCAudioSessionErrorLockRequired. + +- (BOOL)setCategory:(NSString *)category + withOptions:(AVAudioSessionCategoryOptions)options + error:(NSError **)outError; +- (BOOL)setMode:(NSString *)mode error:(NSError **)outError; +- (BOOL)setInputGain:(float)gain error:(NSError **)outError; +- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError; +- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration + error:(NSError **)outError; +- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count + error:(NSError **)outError; +- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count + error:(NSError **)outError; +- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride + error:(NSError **)outError; +- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort + error:(NSError **)outError; +- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource + error:(NSError **)outError; +- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource + error:(NSError **)outError; + +@end + +NS_ASSUME_NONNULL_END 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 0000000000..ea7c546e8e --- /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)delegate { + @synchronized(self) { + [_delegates addObject:delegate]; + } +} + +- (void)removeDelegate:(id)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 *)inputDataSources { + return self.session.inputDataSources; +} + +- (AVAudioSessionDataSourceDescription *)inputDataSource { + return self.session.inputDataSource; +} + +- (NSArray *)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 delegate in self.delegates) { + [delegate audioSessionDidBeginInterruption:self]; + } +} + +- (void)notifyDidEndInterruptionWithShouldResumeSession: + (BOOL)shouldResumeSession { + for (id delegate in self.delegates) { + [delegate audioSessionDidEndInterruption:self + shouldResumeSession:shouldResumeSession]; + } + +} + +- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason + previousRoute:(AVAudioSessionRouteDescription *)previousRoute { + for (id delegate in self.delegates) { + [delegate audioSessionDidChangeRoute:self + reason:reason + previousRoute:previousRoute]; + } +} + +- (void)notifyMediaServicesWereLost { + for (id delegate in self.delegates) { + [delegate audioSessionMediaServicesWereLost:self]; + } +} + +- (void)notifyMediaServicesWereReset { + for (id delegate in self.delegates) { + [delegate audioSessionMediaServicesWereReset:self]; + } +} + +@end