Add RTCAudioSession proxy class.

BUG=
R=haysc@webrtc.org, henrika@webrtc.org

Review URL: https://codereview.webrtc.org/1709853002 .

Cr-Commit-Position: refs/heads/master@{#11676}
This commit is contained in:
Zeke Chin 2016-02-18 15:44:07 -08:00
parent 9ac4df1ba6
commit b3fb71c101
6 changed files with 828 additions and 145 deletions

View File

@ -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",
]
}

View File

@ -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',

View File

@ -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 apps 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 sessions 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 apps 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 sessions 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";

View File

@ -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

View File

@ -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 <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
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 <NSObject>
/** 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<AVAudioSessionDataSourceDescription *> * inputDataSources;
@property(readonly, nullable)
AVAudioSessionDataSourceDescription *inputDataSource;
@property(readonly, nullable)
NSArray<AVAudioSessionDataSourceDescription *> * 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<RTCAudioSessionDelegate>)delegate;
/** Removes an added delegate. */
- (void)removeDelegate:(id<RTCAudioSessionDelegate>)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

View File

@ -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