CallKit speaker button not working

I have a calling app that uses CallKit and it works great except for the speaker button. I found that the CallKit speaker button doesn't work as expected, when I click on it to set the highlight, it always comes back after a while, the code example is as follows:

- (void)configAudiosession {
  AVAudioSession *session = [AVAudioSession sharedInstance];
  NSError *error = nil;
  NSUInteger options = 0;
  options |= AVAudioSessionCategoryOptionAllowBluetooth;
  options |= AVAudioSessionCategoryOptionDuckOthers;
  [session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:options error:&error];
  if (error) NSLog(@"setCategory error %@", error);
  error = nil;
  [session setActive:YES error:&error];
  if (error) NSLog(@"setActive error %@", error);
}

- (void)reportIncomingCall {
  [self configAudiosession];
  NSUUID *uuid = [NSUUID UUID];
  CXCallUpdate *update = [[CXCallUpdate alloc] init];
  update.localizedCallerName = @"caller nickname";
  update.supportsHolding = NO;
  update.supportsDTMF = NO;
  update.hasVideo = NO;
  [self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
    if (error) NSLog(@"reportNewIncomingCallWithUUID error %@", error);
  }];
}

I refer to apple documentation https://developer.apple.com/documentation/avfaudio/avaudiosession/1616614-setmode?language=objc

Instead of setting your category and mode properties independently, set them at the same time using the setCategory:mode:options:error: or setCategory:mode:routeSharingPolicy:options:error: method.

So I use setCategory:mode:options:error: to set the audio session. But this setting will cause the callkit speaker buttons not to work,When I adjust the code as below, the callkit speaker button works fine:

- (void)configAudiosession {
  AVAudioSession *session = [AVAudioSession sharedInstance];
  NSError *error = nil;
  NSUInteger options = 0;
  options |= AVAudioSessionCategoryOptionAllowBluetooth;
  options |= AVAudioSessionCategoryOptionDuckOthers;
  [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:options error:&error];
  if (error) NSLog(@"setCategory error %@", error);
  error = nil;
  [session setMode:AVAudioSessionModeVoiceChat error:&error];
  if (error) NSLog(@"setMode error %@", error);
  error = nil;
  [session setActive:YES error:&error];
  if (error) NSLog(@"setActive error %@", error);
}

Here I use setCategory:withOptions:error: and setMode:error: instead setCategory:mode:options:error:.I know this goes against the advice of the apple docs, But the CallKit speaker button works fine.

But I found another problem.

typedef NS_OPTIONS(NSUInteger, AVAudioSessionCategoryOptions) {
  AVAudioSessionCategoryOptionMixWithOthers      = 0x1,
  AVAudioSessionCategoryOptionDuckOthers        = 0x2,
  AVAudioSessionCategoryOptionAllowBluetooth API_UNAVAILABLE(tvos, watchos, macos) = 0x4,
  AVAudioSessionCategoryOptionDefaultToSpeaker API_UNAVAILABLE(tvos, watchos, macos) = 0x8,
  AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers API_AVAILABLE(ios(9.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos) = 0x11,
  AVAudioSessionCategoryOptionAllowBluetoothA2DP API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0)) API_UNAVAILABLE(macos) = 0x20,
  AVAudioSessionCategoryOptionAllowAirPlay API_AVAILABLE(ios(10.0), tvos(10.0)) API_UNAVAILABLE(watchos, macos) = 0x40,
  AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption API_AVAILABLE(ios(14.5), watchos(7.3)) API_UNAVAILABLE(tvos, macos) = 0x80,
};

The options set in my code should be mixWithOthers and duckOthers and allowBluetooth (according to Apple: Setting duckOthers option will also make your session mixable with others). The correct value should be 7, but the categoryOptions value of this setting is 3. Obviously, allowBluetooth option did not take effect.

This is apparently a bug known to Apple, and I found a similar fix in Google's webRTC code:

- (BOOL)setConfiguration:(RTC_OBJC_TYPE(RTCAudioSessionConfiguration) *)configuration
         active:(BOOL)active
     shouldSetActive:(BOOL)shouldSetActive
          error:(NSError **)outError {
 ......
 if (self.category != configuration.category ||
   self.categoryOptions != configuration.categoryOptions) {
  NSError *categoryError = nil;
  if (![self setCategory:configuration.category
        withOptions:configuration.categoryOptions
           error:&categoryError]) {
   RTCLogError(@"Failed to set category: %@",
         categoryError.localizedDescription);
   error = categoryError;
  } else {
   RTCLog(@"Set category to: %@", configuration.category);
  }
 }

 if (self.mode != configuration.mode) {
  NSError *modeError = nil;
  if (![self setMode:configuration.mode error:&modeError]) {
   RTCLogError(@"Failed to set mode: %@",
         modeError.localizedDescription);
   error = modeError;
  } else {
   RTCLog(@"Set mode to: %@", configuration.mode);
  }
 }

 // Sometimes category options don't stick after setting mode.
 if (self.categoryOptions != configuration.categoryOptions) {
  NSError *categoryError = nil;
  if (![self setCategory:configuration.category
        withOptions:configuration.categoryOptions
           error:&categoryError]) {
   RTCLogError(@"Failed to set category options: %@",
         categoryError.localizedDescription);
   error = categoryError;
  } else {
   RTCLog(@"Set category options to: %ld",
       (long)configuration.categoryOptions);
  }
 }
 ......
}

I saw that after setting the mode, the options were confused, and webRTC avoided this bug by setting it again. So I did the same:

- (void)configAudiosession {
  AVAudioSession *session = [AVAudioSession sharedInstance];
  NSError *error = nil;
  AVAudioSessionCategory category = AVAudioSessionCategoryPlayAndRecord;
  NSUInteger options = 0;
  options |= AVAudioSessionCategoryOptionAllowBluetooth;
  options |= AVAudioSessionCategoryOptionDuckOthers;
  options |= AVAudioSessionCategoryOptionMixWithOthers;
  [session setCategory:category withOptions:options error:&error];
  if (error) NSLog(@"setCategory error %@", error);
  error = nil;
  [session setMode:AVAudioSessionModeVoiceChat error:&error];
  if (error) NSLog(@"setMode error %@", error);
  if (![session.category isEqualToString:category] || options != session.categoryOptions) {
    NSError *error = nil;
    [session setCategory:category withOptions:options error:&error];
    if (error) NSLog(@"again setCategory error %@", error);
  }
  error = nil;
  [session setActive:YES error:&error];
  if (error) NSLog(@"setActive error %@", error);
}

CallKit speaker buttons no longer work. I think it's apple bug, but some apps like what's app, teams they work fine. Can someone help me?

Hello Im not sure if this will help you or not, but I was having a similar problem after updating to iOS16 and fixed it by changing the options for my category. As mentioned in your first block of code, I set the category, mode, options at the same time. Category is PlayAndRecord, mode is voiceChat and options are allowBluetooth and mixWithOthers. Speaker button didnt work until I added allowBluetoothA2DP to the options. Not sure why that worked ( maybe Apple bug ), but havent had problems since

CallKit speaker button not working
 
 
Q