On iOS, after a Room stays connected through an app background → foreground cycle, the WebRTC audio device module (ADM) becomes unavailable and never recovers within the process lifetime — even though a native-level trace shows AVAudioSession itself is completely undisturbed by the transition (no interruption, no route change, no media services reset). This affects both:
AudioManager.instance.setEngineAvailability(...), which throws PlatformException(setEngineAvailability, audio device module is unavailable, null, null).
localParticipant.setMicrophoneEnabled(true), which throws AudioProcessingException(platformUnavailable): audio device module is unavailable and does not recover on retry (tested up to 4 retries / ~2s).
Environment
livekit_client: 2.12.0
- Flutter: 3.38.10
- Platform: iOS (physical device), deployment target 15.0
WebRTC-SDK pod: 144.7559.09 (as pinned by livekit_client/flutter_webrtc)
Info.plist already declares UIBackgroundModes: [audio]
Repro steps
- Join a
Room as a participant who is not publishing audio/video (listen-only), so the app never calls setMicrophoneEnabled/setCameraEnabled before backgrounding.
- Background the app (Home button / swipe up) and leave it backgrounded for roughly 10–30+ seconds. Do not disconnect the
Room — it stays connected the whole time (AudioSessionManagementMode.automatic, no manual session calls before this point).
- Bring the app back to the foreground.
- From the app's resume/
AppLifecycleListener.onRestart handler, call AudioManager.instance.setEngineAvailability(AudioEngineAvailability.none), or call localParticipant.setMicrophoneEnabled(true) to open the mic for the first time.
Actual behavior
Both calls fail immediately with the native side reporting the audio device module is unavailable:
PlatformException(setEngineAvailability, audio device module is unavailable, null, null)
AudioProcessingException(platformUnavailable): audio device module is unavailable
- Retrying
setMicrophoneEnabled (500ms backoff, 4 attempts) never succeeds.
- Calling
setEngineAvailability(AudioEngineAvailability.none) first (in an attempt to "kick" the engine back awake, mirroring the CallKit gate pattern) also fails outright, because — per LiveKitPlugin.swift's handleSetEngineAvailability — that call itself requires a live audioDeviceModule to operate on:
guard let adm = FlutterWebRTCPlugin.sharedSingleton()?.peerConnectionFactory?.audioDeviceModule else {
result(FlutterError(code: "setEngineAvailability", message: "audio device module is unavailable", details: nil))
return
}
So it can't be used to recover a already-nil ADM — only to gate one that's already alive.
- We also tried
AudioManager.instance.setAudioSessionOptions(const AudioSessionOptions.communication()) (switching to AudioSessionManagementMode.manual and immediately re-applying/activating the Apple audio session configuration) right before resuming, on the theory that proactively reconfiguring AVAudioSession before WebRTC touches it again might help. It completed without throwing, but had no effect — the ADM was still unavailable afterward.
- This is a permanent condition for the rest of the process lifetime, not a transient timing issue: we've seen it persist across multiple minutes and multiple foreground interactions in the same session, with no self-recovery observed. (We have not yet fully confirmed whether it ever self-heals given a much longer wait — but several minutes was not enough.)
Expected behavior
Either:
- The ADM should survive an app background/foreground cycle when the
Room stays connected (no explicit disconnect()), especially since UIBackgroundModes: audio is declared and AVAudioSession itself shows no signs of interruption; or
- There should be a supported, documented way to force-recreate/recover the ADM from the Dart API surface after this happens, distinct from
setEngineAvailability (which requires a live ADM as a precondition and can't be used to fix a dead one).
Diagnostic evidence: AVAudioSession is not the problem
To rule out an OS-level audio session interruption as the cause, we added native observers (in our own AppDelegate, not modifying any plugin) for:
UIApplication.willResignActiveNotification / didEnterBackgroundNotification / willEnterForegroundNotification / didBecomeActiveNotification
AVAudioSession.interruptionNotification
AVAudioSession.routeChangeNotification
AVAudioSession.mediaServicesWereResetNotification
At each app-lifecycle event we log AVAudioSession.sharedInstance()'s category, mode, isOtherAudioPlaying, secondaryAudioShouldBeSilencedHint, recordPermission, and currentRoute. Across a full background → foreground cycle that reproduced the ADM failure, the output was:
[AudioDebug] willResignActive | category=AVAudioSessionCategoryPlayback mode=AVAudioSessionModeSpokenAudio isOtherAudioPlaying=false secondaryAudioShouldBeSilenced=false recordPermission=granted route=<... outputs=(Speaker) inputs=() >
[AudioDebug] didEnterBackground | category=AVAudioSessionCategoryPlayback mode=AVAudioSessionModeSpokenAudio isOtherAudioPlaying=false secondaryAudioShouldBeSilenced=false recordPermission=granted route=<... outputs=(Speaker) inputs=() >
[AudioDebug] willEnterForeground | category=AVAudioSessionCategoryPlayback mode=AVAudioSessionModeSpokenAudio isOtherAudioPlaying=false secondaryAudioShouldBeSilenced=false recordPermission=granted route=<... outputs=(Speaker) inputs=() >
[AudioDebug] didBecomeActive | category=AVAudioSessionCategoryPlayback mode=AVAudioSessionModeSpokenAudio isOtherAudioPlaying=false secondaryAudioShouldBeSilenced=false recordPermission=granted route=<... outputs=(Speaker) inputs=() >
category/mode/isOtherAudioPlaying/route are byte-for-byte identical across all four events. No interruption, routeChange, or mediaServicesWereReset notification fired at any point. This strongly suggests the ADM teardown is not driven by any observable AVAudioSession-level event, but happens internally inside the WebRTC-SDK binary's own engine/session management, which we don't have source access to (it ships as a precompiled WebRTC-SDK pod, version 144.7559.09).
We also confirmed LKAudioEngineObserver (LiveKitPlugin.swift) — the Swift-side glue that owns iOS audio session activation timing — is purely reactive to RTCAudioDeviceModuleDelegate callbacks coming from the WebRTC-SDK binary; it does not itself observe UIApplication lifecycle notifications. So whatever decides to tear the engine/ADM down on backgrounding is a decision made inside the closed-source binary, not in anything on the Dart or Swift-glue side we can inspect or patch.
What we've already ruled out
Question for maintainers
- Is this a known limitation of how the
WebRTC-SDK fork (144.7559.09) manages its audio engine/ADM across iOS app backgrounding, even when UIBackgroundModes: audio is declared and the app never disconnects the Room?
- Is there a supported way (public API or documented pattern) to force the ADM to be recreated/recovered after this happens, short of a full app restart?
- Would adding instrumentation/logging around ADM creation/teardown inside the
WebRTC-SDK binary (or exposing an event for it) be something the team would consider, given AVAudioSession-level signals don't correlate with the failure at all?
Happy to provide a minimal reproduction project, a full device log, or test a patched build if that would help narrow this down further.
On iOS, after a
Roomstays connected through an app background → foreground cycle, the WebRTC audio device module (ADM) becomes unavailable and never recovers within the process lifetime — even though a native-level trace showsAVAudioSessionitself is completely undisturbed by the transition (no interruption, no route change, no media services reset). This affects both:AudioManager.instance.setEngineAvailability(...), which throwsPlatformException(setEngineAvailability, audio device module is unavailable, null, null).localParticipant.setMicrophoneEnabled(true), which throwsAudioProcessingException(platformUnavailable): audio device module is unavailableand does not recover on retry (tested up to 4 retries / ~2s).Environment
livekit_client: 2.12.0WebRTC-SDKpod: 144.7559.09 (as pinned bylivekit_client/flutter_webrtc)Info.plistalready declaresUIBackgroundModes: [audio]Repro steps
Roomas a participant who is not publishing audio/video (listen-only), so the app never callssetMicrophoneEnabled/setCameraEnabledbefore backgrounding.Room— it stays connected the whole time (AudioSessionManagementMode.automatic, no manual session calls before this point).AppLifecycleListener.onRestarthandler, callAudioManager.instance.setEngineAvailability(AudioEngineAvailability.none), or calllocalParticipant.setMicrophoneEnabled(true)to open the mic for the first time.Actual behavior
Both calls fail immediately with the native side reporting the audio device module is unavailable:
setMicrophoneEnabled(500ms backoff, 4 attempts) never succeeds.setEngineAvailability(AudioEngineAvailability.none)first (in an attempt to "kick" the engine back awake, mirroring the CallKit gate pattern) also fails outright, because — perLiveKitPlugin.swift'shandleSetEngineAvailability— that call itself requires a liveaudioDeviceModuleto operate on:So it can't be used to recover a already-nil ADM — only to gate one that's already alive.
AudioManager.instance.setAudioSessionOptions(const AudioSessionOptions.communication())(switching toAudioSessionManagementMode.manualand immediately re-applying/activating the Apple audio session configuration) right before resuming, on the theory that proactively reconfiguringAVAudioSessionbefore WebRTC touches it again might help. It completed without throwing, but had no effect — the ADM was still unavailable afterward.Expected behavior
Either:
Roomstays connected (no explicitdisconnect()), especially sinceUIBackgroundModes: audiois declared andAVAudioSessionitself shows no signs of interruption; orsetEngineAvailability(which requires a live ADM as a precondition and can't be used to fix a dead one).Diagnostic evidence:
AVAudioSessionis not the problemTo rule out an OS-level audio session interruption as the cause, we added native observers (in our own
AppDelegate, not modifying any plugin) for:UIApplication.willResignActiveNotification/didEnterBackgroundNotification/willEnterForegroundNotification/didBecomeActiveNotificationAVAudioSession.interruptionNotificationAVAudioSession.routeChangeNotificationAVAudioSession.mediaServicesWereResetNotificationAt each app-lifecycle event we log
AVAudioSession.sharedInstance()'scategory,mode,isOtherAudioPlaying,secondaryAudioShouldBeSilencedHint,recordPermission, andcurrentRoute. Across a full background → foreground cycle that reproduced the ADM failure, the output was:category/mode/isOtherAudioPlaying/routeare byte-for-byte identical across all four events. Nointerruption,routeChange, ormediaServicesWereResetnotification fired at any point. This strongly suggests the ADM teardown is not driven by any observableAVAudioSession-level event, but happens internally inside the WebRTC-SDK binary's own engine/session management, which we don't have source access to (it ships as a precompiledWebRTC-SDKpod, version 144.7559.09).We also confirmed
LKAudioEngineObserver(LiveKitPlugin.swift) — the Swift-side glue that owns iOS audio session activation timing — is purely reactive toRTCAudioDeviceModuleDelegatecallbacks coming from the WebRTC-SDK binary; it does not itself observeUIApplicationlifecycle notifications. So whatever decides to tear the engine/ADM down on backgrounding is a decision made inside the closed-source binary, not in anything on the Dart or Swift-glue side we can inspect or patch.What we've already ruled out
livekit_clientand upgraded specifically hoping this would fix it (required a coordinatedfreezed/json_serializable/json_annotationbump in our app to unblock the version). It did not change the behavior.-9001for pre-connect audio / pre-join mic preview / mic-only scenarios, where the session policy hadn't been pushed to native yet because it's normally only pushed fromRoom.connect(). Our repro is post-connect, mid-session, well after the room is connected and the policy has been pushed — different code path.-9001root cause as Resolve a default audio session from engine state when no policy was pushed #1182, folded into it.ensureMicrophoneAccess): addresses microphone permission requests silently stalling when triggered while the app isn't active. Not our case — our test device already has microphone permission.authorized(confirmed viaAVAudioSession.recordPermissionin the log above), so this permission-check path is a no-op for us.livekit_client-2.12.0package source that all three of the above are already present in what we're running.Question for maintainers
WebRTC-SDKfork (144.7559.09) manages its audio engine/ADM across iOS app backgrounding, even whenUIBackgroundModes: audiois declared and the app never disconnects theRoom?WebRTC-SDKbinary (or exposing an event for it) be something the team would consider, givenAVAudioSession-level signals don't correlate with the failure at all?Happy to provide a minimal reproduction project, a full device log, or test a patched build if that would help narrow this down further.