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.
MPVolumeView
,AVRoutePickerView
instead ofAVAudioSession.preferredInput
– Pit