AVCaptureSession.startRunning() triggers AVCaptureSessionRuntimeErrorNotification with AVError.unknown (-11800), underlying OSStatus 2003329396 → fourCC 'what', every cold launch, but only when an audio AVCaptureDeviceInput is attached. Removing only the audio input makes the error disappear. Same code in a fresh project records audio fine — bug only appears in this app's binary.
AVAudioApplication.shared.recordPermission == .granted. Info.plist has NSMicrophoneUsageDescription. No interruption notifications fire.
Test device: iPhone 16 Pro, iOS 26.4.2. iOS deployment target 17.1.
Minimal reproducer
import AVFoundation
let session = AVCaptureSession()
session.beginConfiguration()
let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)!
session.addInput(try AVCaptureDeviceInput(device: camera))
// Removing ONLY this line makes the error disappear:
let mic = AVCaptureDevice.default(for: .audio)!
session.addInput(try AVCaptureDeviceInput(device: mic))
session.addOutput(AVCaptureMovieFileOutput())
session.addOutput(AVCapturePhotoOutput())
session.commitConfiguration()
NotificationCenter.default.addObserver(
forName: .AVCaptureSessionRuntimeError, object: session, queue: nil
) { print($0.userInfo ?? [:]) }
session.startRunning() // -11800 / 'what' fires within ~2 sec
Observed state at error time
AVError.unknown (-11800)
underlyingError = NSError(NSOSStatusErrorDomain, 2003329396)
userInfo[AVErrorFourCharCode] = 'what'
captureSession.isRunning = false ← never came up
captureSession.isInterrupted = false
captureSession.preset = .high
captureSession.inputs = [Back Triple Camera, iPhone Microphone]
AVAudioSession.sharedInstance():
category = .playAndRecord
mode = .videoRecording
sampleRate = 48000.0
isInputAvailable = true
isOtherAudioPlaying = false
availableInputs = [MicrophoneBuiltIn] (no BT/Continuity/AirPods)
currentRoute.inputs = [] ← EMPTY
currentRoute.outputs = [Speaker|Speaker]
2003329396 = 0x77686174 = 'what'. From a few SO threads this maps to AURemoteIO::StartIO returning a HAL-bring-up failure.
The smoking gun: currentRoute.inputs is empty even though availableInputs contains the built-in mic, isInputAvailable is true, the category is .playAndRecord, and isOtherAudioPlaying is false. The HAL never routes the mic into the session, then 'what' follows. Nothing observable from AVAudioSession indicates a competing client.
Environment / SDKs linked
Firebase (SPM: Crashlytics, Performance, Messaging, Analytics, AppCheck, RemoteConfig, DynamicLinks), FBSDK, Kingfisher, MetalPetal. Multiple Google ad mediation pods present, but their audio session takeover is already disabled (audioVideoManager.isAudioSessionApplicationManaged = true, IMSdk.shouldAutoManageAVAudioSession(false)).
What I've ruled out (all still produce 'what')
Audio session config: .playAndRecord/.videoRecording, .playAndRecord/.default, .record/.measurement, .record/.default. With/without .defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP, .mixWithOthers. setActive(true) before vs. after attaching audio input. setPreferredInput(builtInMic) (verified accepted). 200ms Thread.sleep between setActive(true) and startRunning(). Setting usesApplicationAudioSession = false swaps the fourCC to '!rec' but produces the same outcome.
Topology: sessionPreset = .high / .hd1920x1080 / .hd1280x720 / .medium. Camera = .builtInTripleCamera / .builtInDualWideCamera / .builtInWideAngleCamera. AVCam-style always-attached graph. Setting sessionPreset before vs. after adding inputs.
Threading: All session mutations on a single dedicated DispatchQueue (vs. Swift actor). 1× and 2× full stopRunning()+startRunning() recovery cycles ("do it twice" pattern) — both re-fail with 'what'.
SDK takeover prevention: GoogleMobileAdsMediation pods (Vungle, Mintegral, Pangle, Unity, InMobi), Google-Mobile-Ads-SDK, MediaPipeTasksVision removed via full pod uninstall + clean build — 'what' persists.
Notifications during the failure window:
- 3 ×
AVAudioSession.routeChangeNotificationreasoncategoryChangebefore the error fires, even though category stays.playAndRecord/.videoRecording. DisablingautomaticallyConfiguresApplicationAudioSessiondrops this to 1, but the runtime error still fires. - No
AVAudioSession.interruptionNotification. - No
AVCaptureSessionWasInterruptedNotification.
Symbol audit
otool -L and nm of the bundle confirm none of the linked frameworks reference AVAudioRecorder, AudioComponentInstanceNew, AURemoteIO, or AudioUnitInitialize in their symbol tables. Only the app's own files reference any audio API. Yet adding AVCaptureDeviceInput(.audio) reproduces 100% in this binary and 0% in a fresh project.
My questions
- Who is most likely holding the audio HAL in a process where no linked framework references the AudioUnit / HAL APIs directly? Are there framework load-time audio initializations that don't show up in symbol tables (e.g., dynamic
dlopen,CFBundleLoadExecutable) that could grab the HAL? - Is there an
os_logsubsystem / category that surfaces the underlyingAURemoteIO::StartIOfailure reason at runtime?com.apple.coreaudioshows'what'but not the originating cause. currentRoute.inputsis empty at error time even thoughavailableInputs = [MicrophoneBuiltIn],isInputAvailable = true, and the category is.playAndRecord. What does an empty input route under those conditions imply, and what other system-level holders could be preventing the HAL from routing the mic in?- Has anyone seen
'what'resolve with a device reboot, an iOS update, or by removing a specific framework?
Happy to share a sysdiagnose. Thanks!
Resolved — posting the fix for anyone who hits this later.
Root cause
An EXAppExtensionAttributes block in our main app's Info.plist was registering the host app itself as an embedded com.apple.appintents-extension carrying our intents:
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>
<string>com.apple.appintents-extension</string>
<key>IntentsSupported</key>
<array>
<string>FeatureAppIntent</string>
...
</array>
</dict>
After removing that block:
AVCaptureSession.startRunning()with the audioAVCaptureDeviceInputattached no longer fires-11800 / 'what'.currentRoute.inputsis no longer empty —MicrophoneBuiltInshows up as expected.AVAudioRecorder.record()returnstrue(we'd also tried that path; same family of bug).- The three spurious
routeChangeNotification(.categoryChange)events also stop.
Why it matters
Declaring AppIntents this way registers the main app bundle itself as hosting an appintents-extension extension point. Even though no intent was being invoked, that registration appears to engage the audio HAL pipeline at app launch in a way that conflicts with AVCaptureSession audio input attachment.
None of the workarounds we tried could pre-empt it from app code, because it happens before main() runs:
AVAudioSessionconfig combinations(.playAndRecord/.videoRecording,.record/.measurement,every option combo)- Codec-recovery patterns (1-shot, 2-shot stop + start)
usesApplicationAudioSession = falsetoggle- Pod-level SDK bisect (ad mediations, MediaPipe, etc.)
The fix
Declare AppIntents in a real, separate App Intents Extension target — its own Info.plist, its own EXAppExtensionAttributes — instead of embedding EXAppExtensionAttributes directly in the host app’s Info.plist.
Moving the intents into a sibling extension target (same project, separate target) resolves the issue with no loss of intent functionality.
Hope this saves someone else the bisect.