Skip to content

iOS: audioDeviceModule becomes permanently nil/unavailable after backgrounding, even though AVAudioSession is never interrupted #1206

Description

@AdamTsaiTCH

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

  1. Join a Room as a participant who is not publishing audio/video (listen-only), so the app never calls setMicrophoneEnabled/setCameraEnabled before backgrounding.
  2. 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).
  3. Bring the app back to the foreground.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions