Skip to content

Resolve the audio device module through the registered WebRTC plugin instance - #1212

Open
hiroshihorie wants to merge 2 commits into
mainfrom
hiroshi/per-engine-webrtc-plugin
Open

hiroshihorie wants to merge 2 commits into
mainfrom
hiroshi/per-engine-webrtc-plugin

Conversation

@hiroshihorie

@hiroshihorie hiroshihorie commented Sep 17, 2026

Copy link
Copy Markdown
Member

Fixes #1206, fixes #1176.

Problem

LiveKitPlugin.swift resolved the audio device module on every call through FlutterWebRTCPlugin.sharedSingleton(). flutter_webrtc assigns that singleton in every plugin init, so a second Flutter engine in the process (background isolates, CallKit, add-to-app) replaced the UI engine's instance with one that never creates a peer connection factory. Every audio device module call then failed with audio device module is unavailable for the rest of the process, which is how setMicrophoneEnabled(true) ended in AudioProcessingException(platformUnavailable) after backgrounding.

Android is not affected: LiveKitPlugin.kt captures the flutter_webrtc instance once at construction.

Fix

  • Capture the flutter_webrtc instance at the first registration, as Android does, and resolve lookups through it while it owns the factory. Fall back to the process singleton otherwise.
  • Correct two comments that claimed accessing peerConnectionFactory creates it.

Works with the currently pinned flutter_webrtc. Upstream half: flutter-webrtc/flutter-webrtc#2186.

Verification

Example app on the iOS 27 simulator with the pinned flutter_webrtc, a second FlutterEngine spawned from the AppDelegate 3 s after launch, and getMicrophoneMuteMode called over the livekit_client channel at 2, 6 and 9 s.

Plugin 2 s 6 s and 9 s (after the second engine)
main OK PlatformException(getMicrophoneMuteMode, audio device module is unavailable)
this PR OK OK

The flutter_webrtc singleton moved to the second engine's instance in both runs; only the lookup changed.

…instance

LiveKitPlugin read the WebRTC plugin's process-wide singleton on every call.
That singleton is assigned in each plugin init, so a second Flutter engine in
the process replaced the UI engine's instance with one that never runs the
initialize channel call and never creates a peer connection factory. From then
on startLocalRecording, setEngineAvailability, mute mode and the track lookups
all failed with "audio device module is unavailable" for the rest of the
process, which is how setMicrophoneEnabled(true) ended in
AudioProcessingException(platformUnavailable) after backgrounding.

Capture the WebRTC plugin instance at the first registration, as the Android
plugin already does, and resolve every lookup through it while it owns the
factory. Fall back to the process singleton otherwise. Also correct two
comments that claimed accessing peerConnectionFactory creates it; only the
initialize channel call does.

Fixes #1206, fixes #1176
@github-actions

This comment has been minimized.

@hiroshihorie
hiroshihorie marked this pull request as ready for review September 17, 2026 03:43

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +76 to +77
if let registered, registered.peerConnectionFactory != nil {
return registered

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Destroyed engine remains WebRTC owner

After the first initialized engine is destroyed, webrtcPlugin keeps returning its retained plugin because its factory remains non-null. Replacement-engine audio and track operations target the detached engine.

Learn more

The captured plugin is held by a process-wide strong reference with no reset path. Destroying its Flutter engine does not clear peerConnectionFactory, so the non-null test continues selecting that detached instance. Later registrations update flutter_webrtc's singleton, but this getter never reaches the singleton while the old factory remains allocated. Calls such as audio processing, visualizer lookup, and audio-device control then use the old instance's tracks and factory.

Example: Engine A registers both plugins, initializes WebRTC, and later shuts down. Engine B then registers and initializes WebRTC. A mute-mode call from Engine B resolves Engine A's retained audio device module instead of Engine B's module.

Recommended fix: Make ownership follow plugin lifecycle rather than permanently retaining the first factory-bearing instance. Store a weak reference and clear or replace it when its engine detaches, or use an upstream API that returns the flutter_webrtc instance registered with the same registrar. Ensure replacement occurs atomically and preserves the multi-engine case where a still-live primary engine owns the active factory.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

1 participant