AVAudioSession, setPrefferedInput and switching between multiple Bluetooth Devices

I'm working on a VoIP app which needs to allow the user to switch between the in built ear speaker, speaker, wired headset and bluetooth head sets.

Switching between the built in ear speaker, speaker and wired headset works perfectly fine (through a combination of

overrideOutputAudioPort
and
setPreferredInput
)


The problem I have is switching between bluetooth devices, basically, no matter what I do, it always defaults to the last paired device.


The following is based on paring the devices in the following order

  • mini503
  • BeatsStudio Wireless

In this setup, the BeatsStudio Wireless always wins


The session is setup using...


let session = AVAudioSession.sharedInstance()
try session.setActive(false)
var baseOptions: AVAudioSessionCategoryOptions = [.mixWithOthers]
for option in with {
  baseOptions.insert(option)
}
try session.setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .allowBluetooth, defaultToSpeaker])
try session.overrideOutputAudioPort(.speaker)
try session.setMode(AVAudioSessionModeVoiceChat)
try session.setActive(false)


I then load the available inputs using:

if let inputs = session.availableInputs {
  for input in inputs {
    print(">> Input: \(input)")
    // Stored in array for later use
  }
}


This generates the output:

>> Input: <AVAudioSessionPortDescription: 0x174003de0, type = MicrophoneBuiltIn; name = iPhone Microphone; UID = Built-In Microphone; selectedDataSource = Front>
>> Input: <AVAudioSessionPortDescription: 0x170004380, type = BluetoothHFP; name = mini503; UID = 00:12:6F:15:41:33-tsco; selectedDataSource = (null)>
>> Input: <AVAudioSessionPortDescription: 0x170004410, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>


I then use session.setPrefferedInput to switch the input, when using "BeatsStudio Wireless", it will generate the following:

Current route is currently 
 >> Optional(<AVAudioSessionPortDescription: 0x174002aa0, type = MicrophoneBuiltIn; name = iPhone Microphone; UID = Built-In Microphone; selectedDataSource = Front>)
 >> Optional(<AVAudioSessionPortDescription: 0x174002a30, type = Speaker; name = Speaker; UID = Speaker; selectedDataSource = (null)>)
Apply input: <AVAudioSessionPortDescription: 0x170004410, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>
Current route is now 
 >> Optional(<AVAudioSessionPortDescription: 0x1740029f0, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>)
 >> Optional(<AVAudioSessionPortDescription: 0x174002aa0, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>)


Which shows the route before the

setPrefferredInput
call and the route after it, this also generates
AVAudioSessionRouteChange notification
(as expected)


When I try changing to the mini503 it outputs:

Current route is currently 
 >> Optional(<AVAudioSessionPortDescription: 0x1740029f0, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>)
 >> Optional(<AVAudioSessionPortDescription: 0x174002aa0, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>)
Apply input: <AVAudioSessionPortDescription: 0x170004380, type = BluetoothHFP; name = mini503; UID = 00:12:6F:15:41:33-tsco; selectedDataSource = (null)>
Current route is now 
 >> Optional(<AVAudioSessionPortDescription: 0x1740029f0, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>)
 >> Optional(<AVAudioSessionPortDescription: 0x174002aa0, type = BluetoothHFP; name = BeatsStudio Wireless; UID = 04:88:E2:2B:E2:A0-tsco; selectedDataSource = (null)>)


Which clearly shows that the route has not changed.


I've tried cycling the

active
state of the
session
(turning to
false
before I call
setPrefferredInput
and calling
true
after it) without any effect


If I change the order in which I connect the devices, the last connected device always wins.


I know it should be possible, because the phone app does this, but I can't seem to figure out how. Is there a option or category I should be using? Is there another method I should be trying?

Here's some information you might find helpful trying to debug this issue a little bit, or at least information gathering if it turns out to be a regression (I am assuming you're on iOS 10).


Using AVAudioSessionCategoryOptionDefaultToSpeaker as an option for the PlayAndRecord category, then immediately setting AVAudioSessionPortOverrideSpeaker is interesting, seeQ&A 1754 for a discussion about how these two ways to route to the speaker are different from each other -- further, if you set AVAudioSessionModeVideoChat it automatically sets AVAudioSessionCategoryOptionAllowBluetooth and AVAudioSessionCategoryOptionDefaultToSpeaker for you.


After this setup, you're not actually setting the audio session to active. To set the input, the app's session needs to be in control of routing. Listing 1 in Q&A1799 has some input selection demo code.


Also, if an application is using setPreferredInput to select a Bluetooth HFP input, the output should automatically be changed to the Bluetooth HFP output corresponding with that input. Moreover, selecting a Bluetooth HFP output using the MPVolumeView's route picker should automatically change the input to the Bluetooth HFP input corresponding with that output. In other words, both the input and output should always end up on the same Bluetooth HFP device chosen for either input/output even though only the input or output was set individually. This is the intended behavior, but if it's not happening we definitely want to know about it.


You should see if modifying your setup code and activating the session changes any behavior, and as a test even add an MPVolumeView to see if that allows you to pick the output/input you are intending to select by setting the preferred input/output.


Finally and not specifically related to audio session, but since you mentioned you're working on a VoIP app you may want to check out the Enhancing VoIP Apps with CallKit WWDC session.

Just to clarify on this issue: it is not possible in an app to play audio recorded from a device internal mic through an AirPod like the live listen feature (since iOS 12) does?

Are you able to resolve this issue? I am also facing the same issue. Please let me know, how to solve this issue.
AVAudioSession, setPrefferedInput and switching between multiple Bluetooth Devices
 
 
Q