blob: c2985764d4fada32693d668701fcb4ad0bed8f37 [file] [log] [blame]
/*
* 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"
#include "webrtc/base/criticalsection.h"
#include "webrtc/modules/audio_device/ios/audio_device_ios.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;
NSInteger const kRTCAudioSessionErrorConfiguration = -2;
// 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 {
rtc::CriticalSection _crit;
AVAudioSession *_session;
NSInteger _activationCount;
NSInteger _lockRecursionCount;
BOOL _isActive;
BOOL _shouldDelayAudioConfiguration;
}
@synthesize session = _session;
@synthesize delegates = _delegates;
+ (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];
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 _lockRecursionCount > 0;
}
}
- (void)setShouldDelayAudioConfiguration:(BOOL)shouldDelayAudioConfiguration {
@synchronized(self) {
if (_shouldDelayAudioConfiguration == shouldDelayAudioConfiguration) {
return;
}
_shouldDelayAudioConfiguration = shouldDelayAudioConfiguration;
}
}
- (BOOL)shouldDelayAudioConfiguration {
@synchronized(self) {
return _shouldDelayAudioConfiguration;
}
}
- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate {
if (!delegate) {
return;
}
@synchronized(self) {
_delegates.push_back(delegate);
[self removeZeroedDelegates];
}
}
- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate {
if (!delegate) {
return;
}
@synchronized(self) {
_delegates.erase(std::remove(_delegates.begin(),
_delegates.end(),
delegate));
[self removeZeroedDelegates];
}
}
- (void)lockForConfiguration {
_crit.Enter();
@synchronized(self) {
++_lockRecursionCount;
}
}
- (void)unlockForConfiguration {
// Don't let threads other than the one that called lockForConfiguration
// unlock.
if (_crit.TryEnter()) {
@synchronized(self) {
--_lockRecursionCount;
}
// One unlock for the tryLock, and another one to actually unlock. If this
// was called without anyone calling lock, we will hit an assertion.
_crit.Leave();
_crit.Leave();
}
}
#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;
}
// TODO(tkchin): Simplify the amount of locking happening here. Likely that we
// can just do atomic increments / decrements.
- (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.localizedDescription);
}
// 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;
}
- (std::vector<__weak id<RTCAudioSessionDelegate> >)delegates {
@synchronized(self) {
// Note: this returns a copy.
return _delegates;
}
}
- (void)pushDelegate:(id<RTCAudioSessionDelegate>)delegate {
@synchronized(self) {
_delegates.insert(_delegates.begin(), delegate);
}
}
- (void)removeZeroedDelegates {
@synchronized(self) {
for (auto it = _delegates.begin(); it != _delegates.end(); ++it) {
if (!*it) {
_delegates.erase(it);
}
}
}
}
- (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;
}
}
- (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;
}
- (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 (auto delegate : self.delegates) {
[delegate audioSessionDidBeginInterruption:self];
}
}
- (void)notifyDidEndInterruptionWithShouldResumeSession:
(BOOL)shouldResumeSession {
for (auto delegate : self.delegates) {
[delegate audioSessionDidEndInterruption:self
shouldResumeSession:shouldResumeSession];
}
}
- (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason
previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
for (auto delegate : self.delegates) {
[delegate audioSessionDidChangeRoute:self
reason:reason
previousRoute:previousRoute];
}
}
- (void)notifyMediaServicesWereLost {
for (auto delegate : self.delegates) {
[delegate audioSessionMediaServicesWereLost:self];
}
}
- (void)notifyMediaServicesWereReset {
for (auto delegate : self.delegates) {
[delegate audioSessionMediaServicesWereReset:self];
}
}
@end