| Index: webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm | 
| diff --git a/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..ea7c546e8e9143decad7e60d08c729358ac155ca | 
| --- /dev/null | 
| +++ b/webrtc/modules/audio_device/ios/objc/RTCAudioSession.mm | 
| @@ -0,0 +1,533 @@ | 
| +/* | 
| + *  Copyright 2016 The WebRTC Project Authors. All rights reserved. | 
| + * | 
| + *  Use of this source code is governed by a BSD-style license | 
| + *  that can be found in the LICENSE file in the root of the source | 
| + *  tree. An additional intellectual property rights grant can be found | 
| + *  in the file PATENTS.  All contributing project authors may | 
| + *  be found in the AUTHORS file in the root of the source tree. | 
| + */ | 
| + | 
| +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h" | 
| + | 
| +#include "webrtc/base/checks.h" | 
| + | 
| +#import "webrtc/base/objc/RTCLogging.h" | 
| +#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession+Private.h" | 
| + | 
| +NSString * const kRTCAudioSessionErrorDomain = @"org.webrtc.RTCAudioSession"; | 
| +NSInteger const kRTCAudioSessionErrorLockRequired = -1; | 
| + | 
| +// This class needs to be thread-safe because it is accessed from many threads. | 
| +// TODO(tkchin): Consider more granular locking. We're not expecting a lot of | 
| +// lock contention so coarse locks should be fine for now. | 
| +@implementation RTCAudioSession { | 
| +  AVAudioSession *_session; | 
| +  NSHashTable *_delegates; | 
| +  NSInteger _activationCount; | 
| +  BOOL _isActive; | 
| +  BOOL _isLocked; | 
| +} | 
| + | 
| +@synthesize session = _session; | 
| +@synthesize lock = _lock; | 
| + | 
| ++ (instancetype)sharedInstance { | 
| +  static dispatch_once_t onceToken; | 
| +  static RTCAudioSession *sharedInstance = nil; | 
| +  dispatch_once(&onceToken, ^{ | 
| +    sharedInstance = [[RTCAudioSession alloc] init]; | 
| +  }); | 
| +  return sharedInstance; | 
| +} | 
| + | 
| +- (instancetype)init { | 
| +  if (self = [super init]) { | 
| +    _session = [AVAudioSession sharedInstance]; | 
| +    _delegates = [NSHashTable weakObjectsHashTable]; | 
| +    _lock = [[NSRecursiveLock alloc] init]; | 
| +    NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; | 
| +    [center addObserver:self | 
| +               selector:@selector(handleInterruptionNotification:) | 
| +                   name:AVAudioSessionInterruptionNotification | 
| +                 object:nil]; | 
| +    [center addObserver:self | 
| +               selector:@selector(handleRouteChangeNotification:) | 
| +                   name:AVAudioSessionRouteChangeNotification | 
| +                 object:nil]; | 
| +    // TODO(tkchin): Maybe listen to SilenceSecondaryAudioHintNotification. | 
| +    [center addObserver:self | 
| +               selector:@selector(handleMediaServicesWereLost:) | 
| +                   name:AVAudioSessionMediaServicesWereLostNotification | 
| +                 object:nil]; | 
| +    [center addObserver:self | 
| +               selector:@selector(handleMediaServicesWereReset:) | 
| +                   name:AVAudioSessionMediaServicesWereResetNotification | 
| +                 object:nil]; | 
| +  } | 
| +  return self; | 
| +} | 
| + | 
| +- (void)dealloc { | 
| +  [[NSNotificationCenter defaultCenter] removeObserver:self]; | 
| +} | 
| + | 
| +- (void)setIsActive:(BOOL)isActive { | 
| +  @synchronized(self) { | 
| +    _isActive = isActive; | 
| +  } | 
| +} | 
| + | 
| +- (BOOL)isActive { | 
| +  @synchronized(self) { | 
| +    return _isActive; | 
| +  } | 
| +} | 
| + | 
| +- (BOOL)isLocked { | 
| +  @synchronized(self) { | 
| +    return _isLocked; | 
| +  } | 
| +} | 
| + | 
| +- (void)addDelegate:(id<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 | 
|  |