| /* | |
| * 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 "RTCAudioSession+Private.h" | |
| #import <UIKit/UIKit.h> | |
| #include <atomic> | |
| #include <vector> | |
| #include "absl/base/attributes.h" | |
| #include "rtc_base/checks.h" | |
| #include "rtc_base/synchronization/mutex.h" | |
| #import "RTCAudioSessionConfiguration.h" | |
| #import "base/RTCLogging.h" | |
| #if !defined(ABSL_HAVE_THREAD_LOCAL) | |
| #error ABSL_HAVE_THREAD_LOCAL should be defined for MacOS / iOS Targets. | |
| #endif | |
| NSString *const kRTCAudioSessionErrorDomain = | |
| @"org.webrtc.RTC_OBJC_TYPE(RTCAudioSession)"; | |
| NSInteger const kRTCAudioSessionErrorLockRequired = -1; | |
| NSInteger const kRTCAudioSessionErrorConfiguration = -2; | |
| NSString *const kRTCAudioSessionOutputVolumeSelector = @"outputVolume"; | |
| namespace { | |
| // Since webrtc::Mutex is not a reentrant lock and cannot check if the mutex is | |
| // locked, we need a separate variable to check that the mutex is locked in the | |
| // RTCAudioSession. | |
| ABSL_CONST_INIT thread_local bool mutex_locked = false; | |
| } // namespace | |
| @interface RTC_OBJC_TYPE (RTCAudioSession) | |
| () @property(nonatomic, readonly) | |
| std::vector<__weak id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)> > delegates; | |
| @end | |
| // 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 RTC_OBJC_TYPE (RTCAudioSession) { | |
| webrtc::Mutex _mutex; | |
| AVAudioSession *_session; | |
| std::atomic<int> _activationCount; | |
| std::atomic<int> _webRTCSessionCount; | |
| BOOL _isActive; | |
| BOOL _useManualAudio; | |
| BOOL _isAudioEnabled; | |
| BOOL _canPlayOrRecord; | |
| BOOL _isInterrupted; | |
| } | |
| @synthesize session = _session; | |
| @synthesize delegates = _delegates; | |
| @synthesize ignoresPreferredAttributeConfigurationErrors = | |
| _ignoresPreferredAttributeConfigurationErrors; | |
| + (instancetype)sharedInstance { | |
| static dispatch_once_t onceToken; | |
| static RTC_OBJC_TYPE(RTCAudioSession) *sharedInstance = nil; | |
| dispatch_once(&onceToken, ^{ | |
| sharedInstance = [[self alloc] init]; | |
| }); | |
| return sharedInstance; | |
| } | |
| - (instancetype)init { | |
| return [self initWithAudioSession:[AVAudioSession sharedInstance]]; | |
| } | |
| /** This initializer provides a way for unit tests to inject a fake/mock audio | |
| * session. */ | |
| - (instancetype)initWithAudioSession:(id)audioSession { | |
| self = [super init]; | |
| if (self) { | |
| _session = audioSession; | |
| NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; | |
| [center addObserver:self | |
| selector:@selector(handleInterruptionNotification:) | |
| name:AVAudioSessionInterruptionNotification | |
| object:nil]; | |
| [center addObserver:self | |
| selector:@selector(handleRouteChangeNotification:) | |
| name:AVAudioSessionRouteChangeNotification | |
| object:nil]; | |
| [center addObserver:self | |
| selector:@selector(handleMediaServicesWereLost:) | |
| name:AVAudioSessionMediaServicesWereLostNotification | |
| object:nil]; | |
| [center addObserver:self | |
| selector:@selector(handleMediaServicesWereReset:) | |
| name:AVAudioSessionMediaServicesWereResetNotification | |
| object:nil]; | |
| // Posted on the main thread when the primary audio from other applications | |
| // starts and stops. Foreground applications may use this notification as a | |
| // hint to enable or disable audio that is secondary. | |
| [center addObserver:self | |
| selector:@selector(handleSilenceSecondaryAudioHintNotification:) | |
| name:AVAudioSessionSilenceSecondaryAudioHintNotification | |
| object:nil]; | |
| // Also track foreground event in order to deal with interruption ended | |
| // situation. | |
| [center addObserver:self | |
| selector:@selector(handleApplicationDidBecomeActive:) | |
| name:UIApplicationDidBecomeActiveNotification | |
| object:nil]; | |
| [_session | |
| addObserver:self | |
| forKeyPath:kRTCAudioSessionOutputVolumeSelector | |
| options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | |
| context:(__bridge void *)RTC_OBJC_TYPE(RTCAudioSession).class]; | |
| RTCLog(@"RTC_OBJC_TYPE(RTCAudioSession) (%p): init.", self); | |
| } | |
| return self; | |
| } | |
| - (void)dealloc { | |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| [_session | |
| removeObserver:self | |
| forKeyPath:kRTCAudioSessionOutputVolumeSelector | |
| context:(__bridge void *)RTC_OBJC_TYPE(RTCAudioSession).class]; | |
| RTCLog(@"RTC_OBJC_TYPE(RTCAudioSession) (%p): dealloc.", self); | |
| } | |
| - (NSString *)description { | |
| NSString *format = @"RTC_OBJC_TYPE(RTCAudioSession): {\n" | |
| " category: %@\n" | |
| " categoryOptions: %ld\n" | |
| " mode: %@\n" | |
| " isActive: %d\n" | |
| " sampleRate: %.2f\n" | |
| " IOBufferDuration: %f\n" | |
| " outputNumberOfChannels: %ld\n" | |
| " inputNumberOfChannels: %ld\n" | |
| " outputLatency: %f\n" | |
| " inputLatency: %f\n" | |
| " outputVolume: %f\n" | |
| "}"; | |
| NSString *description = | |
| [NSString stringWithFormat:format, | |
| self.category, | |
| (long)self.categoryOptions, | |
| self.mode, | |
| self.isActive, | |
| self.sampleRate, | |
| self.IOBufferDuration, | |
| self.outputNumberOfChannels, | |
| self.inputNumberOfChannels, | |
| self.outputLatency, | |
| self.inputLatency, | |
| self.outputVolume]; | |
| return description; | |
| } | |
| - (void)setIsActive:(BOOL)isActive { | |
| @synchronized(self) { | |
| _isActive = isActive; | |
| } | |
| } | |
| - (BOOL)isActive { | |
| @synchronized(self) { | |
| return _isActive; | |
| } | |
| } | |
| - (void)setUseManualAudio:(BOOL)useManualAudio { | |
| @synchronized(self) { | |
| if (_useManualAudio == useManualAudio) { | |
| return; | |
| } | |
| _useManualAudio = useManualAudio; | |
| } | |
| [self updateCanPlayOrRecord]; | |
| } | |
| - (BOOL)useManualAudio { | |
| @synchronized(self) { | |
| return _useManualAudio; | |
| } | |
| } | |
| - (void)setIsAudioEnabled:(BOOL)isAudioEnabled { | |
| @synchronized(self) { | |
| if (_isAudioEnabled == isAudioEnabled) { | |
| return; | |
| } | |
| _isAudioEnabled = isAudioEnabled; | |
| } | |
| [self updateCanPlayOrRecord]; | |
| } | |
| - (BOOL)isAudioEnabled { | |
| @synchronized(self) { | |
| return _isAudioEnabled; | |
| } | |
| } | |
| - (void)setIgnoresPreferredAttributeConfigurationErrors: | |
| (BOOL)ignoresPreferredAttributeConfigurationErrors { | |
| @synchronized(self) { | |
| if (_ignoresPreferredAttributeConfigurationErrors == | |
| ignoresPreferredAttributeConfigurationErrors) { | |
| return; | |
| } | |
| _ignoresPreferredAttributeConfigurationErrors = | |
| ignoresPreferredAttributeConfigurationErrors; | |
| } | |
| } | |
| - (BOOL)ignoresPreferredAttributeConfigurationErrors { | |
| @synchronized(self) { | |
| return _ignoresPreferredAttributeConfigurationErrors; | |
| } | |
| } | |
| // TODO(tkchin): Check for duplicates. | |
| - (void)addDelegate:(id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)>)delegate { | |
| RTCLog(@"Adding delegate: (%p)", delegate); | |
| if (!delegate) { | |
| return; | |
| } | |
| @synchronized(self) { | |
| _delegates.push_back(delegate); | |
| [self removeZeroedDelegates]; | |
| } | |
| } | |
| - (void)removeDelegate:(id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)>)delegate { | |
| RTCLog(@"Removing delegate: (%p)", delegate); | |
| if (!delegate) { | |
| return; | |
| } | |
| @synchronized(self) { | |
| std::erase(_delegates, delegate); | |
| [self removeZeroedDelegates]; | |
| } | |
| } | |
| #pragma clang diagnostic push | |
| #pragma clang diagnostic ignored "-Wthread-safety-analysis" | |
| - (void)lockForConfiguration { | |
| RTC_CHECK(!mutex_locked); | |
| _mutex.Lock(); | |
| mutex_locked = true; | |
| } | |
| - (void)unlockForConfiguration { | |
| mutex_locked = false; | |
| _mutex.Unlock(); | |
| } | |
| #pragma clang diagnostic pop | |
| #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; | |
| } | |
| - (double)preferredSampleRate { | |
| return self.session.preferredSampleRate; | |
| } | |
| - (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; | |
| } | |
| - (NSTimeInterval)preferredIOBufferDuration { | |
| return self.session.preferredIOBufferDuration; | |
| } | |
| - (BOOL)setActive:(BOOL)active error:(NSError **)outError { | |
| if (![self checkLock:outError]) { | |
| return NO; | |
| } | |
| int activationCount = _activationCount.load(); | |
| if (!active && activationCount == 0) { | |
| RTCLogWarning(@"Attempting to deactivate without prior activation."); | |
| } | |
| [self notifyWillSetActive:active]; | |
| 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 (active) { | |
| if (shouldSetActive) { | |
| self.isActive = active; | |
| if (self.isInterrupted) { | |
| self.isInterrupted = NO; | |
| [self notifyDidEndInterruptionWithShouldResumeSession:YES]; | |
| } | |
| } | |
| [self incrementActivationCount]; | |
| [self notifyDidSetActive:active]; | |
| } | |
| } else { | |
| RTCLogError(@"Failed to setActive:%d. Error: %@", | |
| active, | |
| error.localizedDescription); | |
| [self notifyFailedToSetActive:active error:error]; | |
| } | |
| // Set isActive and decrement activation count on deactivation | |
| // whether or not it succeeded. | |
| if (!active) { | |
| if (shouldSetActive) { | |
| self.isActive = active; | |
| [self notifyDidSetActive:active]; | |
| } | |
| [self decrementActivationCount]; | |
| } | |
| RTCLog(@"Number of current activations: %d", _activationCount.load()); | |
| return success; | |
| } | |
| - (BOOL)setCategory:(AVAudioSessionCategory)category | |
| mode:(AVAudioSessionMode)mode | |
| options:(AVAudioSessionCategoryOptions)options | |
| error:(NSError **)outError { | |
| if (![self checkLock:outError]) { | |
| return NO; | |
| } | |
| return [self.session setCategory:category | |
| mode:mode | |
| options:options | |
| error:outError]; | |
| } | |
| - (BOOL)setCategory:(AVAudioSessionCategory)category | |
| withOptions:(AVAudioSessionCategoryOptions)options | |
| error:(NSError **)outError { | |
| if (![self checkLock:outError]) { | |
| return NO; | |
| } | |
| return [self.session setCategory:category withOptions:options error:outError]; | |
| } | |
| - (BOOL)setMode:(AVAudioSessionMode)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.isInterrupted = YES; | |
| [self notifyDidBeginInterruption]; | |
| break; | |
| case AVAudioSessionInterruptionTypeEnded: { | |
| RTCLog(@"Audio session interruption ended."); | |
| self.isInterrupted = NO; | |
| [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]; | |
| } | |
| - (void)handleSilenceSecondaryAudioHintNotification: | |
| (NSNotification *)notification { | |
| // TODO(henrika): just adding logs here for now until we know if we are ever | |
| // see this notification and might be affected by it or if further actions | |
| // are required. | |
| NSNumber *typeNumber = | |
| notification.userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey]; | |
| AVAudioSessionSilenceSecondaryAudioHintType type = | |
| (AVAudioSessionSilenceSecondaryAudioHintType) | |
| typeNumber.unsignedIntegerValue; | |
| switch (type) { | |
| case AVAudioSessionSilenceSecondaryAudioHintTypeBegin: | |
| RTCLog(@"Another application's primary audio has started."); | |
| break; | |
| case AVAudioSessionSilenceSecondaryAudioHintTypeEnd: | |
| RTCLog(@"Another application's primary audio has stopped."); | |
| break; | |
| } | |
| } | |
| - (void)handleApplicationDidBecomeActive:(NSNotification *)notification { | |
| BOOL isInterrupted = self.isInterrupted; | |
| RTCLog(@"Application became active after an interruption. Treating as " | |
| @"interruption " | |
| "end. isInterrupted changed from %d to 0.", | |
| isInterrupted); | |
| if (isInterrupted) { | |
| self.isInterrupted = NO; | |
| [self updateAudioSessionAfterEvent]; | |
| } | |
| // Always treat application becoming active as an interruption end event. | |
| [self notifyDidEndInterruptionWithShouldResumeSession:YES]; | |
| } | |
| #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<RTC_OBJC_TYPE(RTCAudioSessionDelegate)> >)delegates { | |
| @synchronized(self) { | |
| // Note: this returns a copy. | |
| return _delegates; | |
| } | |
| } | |
| // TODO(tkchin): check for duplicates. | |
| - (void)pushDelegate:(id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)>)delegate { | |
| @synchronized(self) { | |
| _delegates.insert(_delegates.begin(), delegate); | |
| } | |
| } | |
| - (void)removeZeroedDelegates { | |
| @synchronized(self) { | |
| std::erase_if(_delegates, | |
| [](id delegate) -> bool { return delegate == nil; }); | |
| } | |
| } | |
| - (int)activationCount { | |
| return _activationCount.load(); | |
| } | |
| - (int)incrementActivationCount { | |
| RTCLog(@"Incrementing activation count."); | |
| return _activationCount.fetch_add(1) + 1; | |
| } | |
| - (NSInteger)decrementActivationCount { | |
| RTCLog(@"Decrementing activation count."); | |
| return _activationCount.fetch_sub(1) - 1; | |
| } | |
| - (int)webRTCSessionCount { | |
| return _webRTCSessionCount.load(); | |
| } | |
| - (BOOL)canPlayOrRecord { | |
| return !self.useManualAudio || self.isAudioEnabled; | |
| } | |
| - (BOOL)isInterrupted { | |
| @synchronized(self) { | |
| return _isInterrupted; | |
| } | |
| } | |
| - (void)setIsInterrupted:(BOOL)isInterrupted { | |
| @synchronized(self) { | |
| if (_isInterrupted == isInterrupted) { | |
| return; | |
| } | |
| _isInterrupted = isInterrupted; | |
| } | |
| } | |
| - (BOOL)checkLock:(NSError **)outError { | |
| if (!mutex_locked) { | |
| if (outError) { | |
| *outError = [RTC_OBJC_TYPE(RTCAudioSession) lockError]; | |
| } | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| - (BOOL)beginWebRTCSession:(NSError **)outError { | |
| if (outError) { | |
| *outError = nil; | |
| } | |
| _webRTCSessionCount.fetch_add(1); | |
| [self notifyDidStartPlayOrRecord]; | |
| return YES; | |
| } | |
| - (BOOL)endWebRTCSession:(NSError **)outError { | |
| if (outError) { | |
| *outError = nil; | |
| } | |
| _webRTCSessionCount.fetch_sub(1); | |
| [self notifyDidStopPlayOrRecord]; | |
| return YES; | |
| } | |
| - (BOOL)configureWebRTCSession:(NSError **)outError { | |
| if (outError) { | |
| *outError = nil; | |
| } | |
| RTCLog(@"Configuring audio session for WebRTC."); | |
| // Configure the AVAudioSession and activate it. | |
| // Provide an error even if there isn't one so we can log it. | |
| NSError *error = nil; | |
| RTC_OBJC_TYPE(RTCAudioSessionConfiguration) *webRTCConfig = | |
| [RTC_OBJC_TYPE(RTCAudioSessionConfiguration) webRTCConfiguration]; | |
| if (![self setConfiguration:webRTCConfig active:YES error:&error]) { | |
| RTCLogError(@"Failed to set WebRTC audio configuration: %@", | |
| error.localizedDescription); | |
| // Do not call setActive:NO if setActive:YES failed. | |
| if (outError) { | |
| *outError = error; | |
| } | |
| return NO; | |
| } | |
| // Ensure that the device currently supports audio input. | |
| // TODO(tkchin): Figure out if this is really necessary. | |
| if (!self.inputAvailable) { | |
| RTCLogError(@"No audio input path is available!"); | |
| [self unconfigureWebRTCSession:nil]; | |
| if (outError) { | |
| *outError = [self configurationErrorWithDescription:@"No input path."]; | |
| } | |
| return NO; | |
| } | |
| // It can happen (e.g. in combination with BT devices) that the attempt to set | |
| // the preferred sample rate for WebRTC (48kHz) fails. If so, make a new | |
| // configuration attempt using the sample rate that worked using the active | |
| // audio session. A typical case is that only 8 or 16kHz can be set, e.g. in | |
| // combination with BT headsets. Using this "trick" seems to avoid a state | |
| // where Core Audio asks for a different number of audio frames than what the | |
| // session's I/O buffer duration corresponds to. | |
| // TODO(henrika): this fix resolves bugs.webrtc.org/6004 but it has only been | |
| // tested on a limited set of iOS devices and BT devices. | |
| double sessionSampleRate = self.sampleRate; | |
| double preferredSampleRate = webRTCConfig.sampleRate; | |
| if (sessionSampleRate != preferredSampleRate) { | |
| RTCLogWarning( | |
| @"Current sample rate (%.2f) is not the preferred rate (%.2f)", | |
| sessionSampleRate, | |
| preferredSampleRate); | |
| if (![self setPreferredSampleRate:sessionSampleRate error:&error]) { | |
| RTCLogError(@"Failed to set preferred sample rate: %@", | |
| error.localizedDescription); | |
| if (outError) { | |
| *outError = error; | |
| } | |
| } | |
| } | |
| return YES; | |
| } | |
| - (BOOL)unconfigureWebRTCSession:(NSError **)outError { | |
| if (outError) { | |
| *outError = nil; | |
| } | |
| RTCLog(@"Unconfiguring audio session for WebRTC."); | |
| [self setActive:NO error:outError]; | |
| return YES; | |
| } | |
| - (NSError *)configurationErrorWithDescription:(NSString *)description { | |
| NSDictionary *userInfo = @{ | |
| NSLocalizedDescriptionKey : description, | |
| }; | |
| return [[NSError alloc] initWithDomain:kRTCAudioSessionErrorDomain | |
| code:kRTCAudioSessionErrorConfiguration | |
| userInfo:userInfo]; | |
| } | |
| - (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)updateCanPlayOrRecord { | |
| BOOL canPlayOrRecord = NO; | |
| BOOL shouldNotify = NO; | |
| @synchronized(self) { | |
| canPlayOrRecord = !self.useManualAudio || self.isAudioEnabled; | |
| if (_canPlayOrRecord == canPlayOrRecord) { | |
| return; | |
| } | |
| _canPlayOrRecord = canPlayOrRecord; | |
| shouldNotify = YES; | |
| } | |
| if (shouldNotify) { | |
| [self notifyDidChangeCanPlayOrRecord:canPlayOrRecord]; | |
| } | |
| } | |
| - (void)audioSessionDidActivate:(AVAudioSession *)session { | |
| if (_session != session) { | |
| RTCLogError(@"audioSessionDidActivate called on different AVAudioSession"); | |
| } | |
| RTCLog(@"Audio session was externally activated."); | |
| [self incrementActivationCount]; | |
| self.isActive = YES; | |
| // When a CallKit call begins, it's possible that we receive an interruption | |
| // begin without a corresponding end. Since we know that we have an activated | |
| // audio session at this point, just clear any saved interruption flag since | |
| // the app may never be foregrounded during the duration of the call. | |
| if (self.isInterrupted) { | |
| RTCLog(@"Clearing interrupted state due to external activation."); | |
| self.isInterrupted = NO; | |
| } | |
| // Treat external audio session activation as an end interruption event. | |
| [self notifyDidEndInterruptionWithShouldResumeSession:YES]; | |
| } | |
| - (void)audioSessionDidDeactivate:(AVAudioSession *)session { | |
| if (_session != session) { | |
| RTCLogError( | |
| @"audioSessionDidDeactivate called on different AVAudioSession"); | |
| } | |
| RTCLog(@"Audio session was externally deactivated."); | |
| self.isActive = NO; | |
| [self decrementActivationCount]; | |
| } | |
| - (void)observeValueForKeyPath:(NSString *)keyPath | |
| ofObject:(id)object | |
| change:(NSDictionary *)change | |
| context:(void *)context { | |
| if (context == (__bridge void *)RTC_OBJC_TYPE(RTCAudioSession).class) { | |
| if (object == _session) { | |
| NSNumber *newVolume = change[NSKeyValueChangeNewKey]; | |
| RTCLog(@"OutputVolumeDidChange to %f", newVolume.floatValue); | |
| [self notifyDidChangeOutputVolume:newVolume.floatValue]; | |
| } | |
| } else { | |
| [super observeValueForKeyPath:keyPath | |
| ofObject:object | |
| change:change | |
| context:context]; | |
| } | |
| } | |
| - (void)notifyAudioUnitStartFailedWithError:(OSStatus)error { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:audioUnitStartFailedWithError:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self | |
| audioUnitStartFailedWithError: | |
| [NSError errorWithDomain:kRTCAudioSessionErrorDomain | |
| code:error | |
| userInfo:nil]]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidBeginInterruption { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionDidBeginInterruption:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionDidBeginInterruption:self]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidEndInterruptionWithShouldResumeSession: | |
| (BOOL)shouldResumeSession { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionDidEndInterruption:shouldResumeSession:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionDidEndInterruption:self | |
| shouldResumeSession:shouldResumeSession]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidChangeRouteWithReason:(AVAudioSessionRouteChangeReason)reason | |
| previousRoute: | |
| (AVAudioSessionRouteDescription *)previousRoute { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionDidChangeRoute:reason:previousRoute:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionDidChangeRoute:self | |
| reason:reason | |
| previousRoute:previousRoute]; | |
| } | |
| } | |
| } | |
| - (void)notifyMediaServicesWereLost { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionMediaServerTerminated:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionMediaServerTerminated:self]; | |
| } | |
| } | |
| } | |
| - (void)notifyMediaServicesWereReset { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionMediaServerReset:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionMediaServerReset:self]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidChangeCanPlayOrRecord:(BOOL)canPlayOrRecord { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:didChangeCanPlayOrRecord:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self didChangeCanPlayOrRecord:canPlayOrRecord]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidStartPlayOrRecord { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionDidStartPlayOrRecord:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionDidStartPlayOrRecord:self]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidStopPlayOrRecord { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSessionDidStopPlayOrRecord:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSessionDidStopPlayOrRecord:self]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidChangeOutputVolume:(float)volume { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:didChangeOutputVolume:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self didChangeOutputVolume:volume]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidDetectPlayoutGlitch:(int64_t)totalNumberOfGlitches { | |
| for (auto delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:didDetectPlayoutGlitch:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self didDetectPlayoutGlitch:totalNumberOfGlitches]; | |
| } | |
| } | |
| } | |
| - (void)notifyWillSetActive:(BOOL)active { | |
| for (id delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:willSetActive:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self willSetActive:active]; | |
| } | |
| } | |
| } | |
| - (void)notifyDidSetActive:(BOOL)active { | |
| for (id delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:didSetActive:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self didSetActive:active]; | |
| } | |
| } | |
| } | |
| - (void)notifyFailedToSetActive:(BOOL)active error:(NSError *)error { | |
| for (id delegate : self.delegates) { | |
| SEL sel = @selector(audioSession:failedToSetActive:error:); | |
| if ([delegate respondsToSelector:sel]) { | |
| [delegate audioSession:self failedToSetActive:active error:error]; | |
| } | |
| } | |
| } | |
| @end |