AVAudioSession services reset when capturing input from bluetooth device that disconnects
Asked Answered
F

0

8

TL;DR - AVAudioSession will fire AVAudioSessionMediaServicesWereLostNotification when a bluetooth port is specified with AVAudioSession.setPreferredInput and that device disconnects while actively reading input with either AVCaptureSession or AVAudioEngine. Example project here.

https://openradar.appspot.com/FB8921592

The Problem

Through what I consider a "happy path", or a path that should at least be supported by Apple, I can consistently get AVAudioSession to fire its AVAudioSessionMediaServicesWereLostNotification and AVAudioSessionMediaServicesWereResetNotification notifications.

In between these two notifications, your app can't do any audio or video I/O or processing (encoding or decoding). It's a total media services shutdown for 1 to 2 seconds.

The Trigger

This only happens when using AVAudioSession.setPreferredInput, where the port you set is a bluetooth device. While actively recording from that port, you turn the bluetooth device off.

When this happens, no other notifications from AVAudioSession, AVAudioEngine, or AVCaptureSession are fired before services are lost, so there is no way to preemptively handle the situation. The user can turn off their headset at any time, causing total services failure.

Some Code

First I set up the audio session

try! AVAudioSession.sharedInstance().setCategory(.record, mode: .videoRecording, options: .allowBluetooth)
try! AVAudioSession.sharedInstance().setActive(true)

Then specify the correct port

let bluetoothPort = AVAudioSession.sharedInstance().availableInputs?
    .first(where: { $0.portType == .bluetoothHFP })
guard let validPort = newPort else {
    print("Couldn't find a valid bluetooth port")
    return
}
print("Swapping \(AVAudioSession.sharedInstance().currentRoute.inputs.first!.portName) for \(validPort.portName)")
try! AVAudioSession.sharedInstance().setPreferredInput(newPort)

Finally set up the AVCaptureSession

let cap = AVCaptureSession()
cap.automaticallyConfiguresApplicationAudioSession = false
let device = AVCaptureDevice.default(for: .audio)!
let input = try! AVCaptureDeviceInput(device: device)
let output = AVCaptureAudioDataOutput()
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "test-queue"))
cap.beginConfiguration()
cap.addInput(input)
cap.addOutput(output)
cap.commitConfiguration()
cap.startRunning()

Or AVAudioEngine

let engine = AVAudioEngine()
let inputNode = engine.inputNode
let bus = 0
inputNode.installTap(onBus: bus, bufferSize: 1024, format: inputNode.inputFormat(forBus: 0)) { (buffer, time) in
    // anything really
}
engine.prepare()
try! engine.start()

I do have a repo that will demonstrate the problem using either AVAudioEngine or AVCaptureSession, you can find it here - https://github.com/bclymer/ios-preferredinput-crash. There are more explicit repro steps in the README.

The following devices of mine are impacted (it's all of them)

╔═════════════╦═════════════╗
║ Device Name ║ iOS Version ║
╠═════════════╬═════════════╣
║ iPhone 7    ║ iOS 14.2    ║
║ iPhone Xr   ║ iOS 14.0.1  ║
║ iPad Air 2  ║ iOS 13.7    ║
╚═════════════╩═════════════╝

What I'm Looking For

Is there a way to not have services reset while reading I/O from an external device that disconnects suddenly?

A solution that I'm aware of but is not acceptable is relying on Apple's "last in" approach to audio routing. When attaching a bluetooth device, it becomes the default route automatically if AVAudioSession.preferredInput == nil. However, I need to be able to explicitly choose between any number of external devices. Using implicit routing won't work for me.

Fruin answered 28/11, 2020 at 2:34 Comment(7)
Which iOS versions have you seen this behaviour on?Criollo
@RhythmicFistman I see this on an iPhone 7 with iOS 14.2, an iPhone Xr with iOS 14.0.1, and an iPad Air 2 with iOS 13.7.Fruin
use MPVolumeView, AVRoutePickerView instead of AVAudioSession.preferredInputPit
@Pit those are both for selecting the output route, not input.Fruin
@Fruin did you manage to find a solution for this issue, I'm struggling too with it.Caryloncaryn
@Caryloncaryn sadly no. We worked around it in our UX by only allowing a single external device to be connected at a time, or if there are multiple you are only able to select the most recently plugged in device (because that is what will be selected when preferred input is nil). No response from Apple on my radar either. But since this has been around since iOS 10, I don't have much hope.Fruin
A related issue: Create a recording session with the preferred route set to nil. Connect a Bluetooth headset to the device, then call the device (e.g., FaceTime). Terminate the FaceTime call. The app will be notified of a route change to the built-in microphone. I tried to set the preferred input to the Bluetooth device, but then I run into the issue that you note here.Stationer

© 2022 - 2024 — McMap. All rights reserved.