| /* |
| * 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) { |
| _delegates.erase(std::remove(_delegates.begin(), |
| _delegates.end(), |
| delegate), |
| _delegates.end()); |
| [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) { |
| _delegates.erase( |
| std::remove_if(_delegates.begin(), |
| _delegates.end(), |
| [](id delegate) -> bool { return delegate == nil; }), |
| _delegates.end()); |
| } |
| } |
| |
| - (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 |