Skip to content

Resolve the WebRTC plugin registered with the calling engine - #1212

Merged
hiroshihorie merged 4 commits into
mainfrom
hiroshi/per-engine-webrtc-plugin
Oct 27, 2026
Merged

hiroshihorie merged 4 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 resolves the flutter_webrtc plugin through the process-wide FlutterWebRTCPlugin.sharedSingleton() on every call. Each Flutter engine in the process (background isolates, CallKit, add-to-app) registers its own flutter_webrtc instance with its own peer connection factory and audio device module.

Before flutter_webrtc 1.6.3, the singleton moved to every newly registered instance. A second engine that never initialized WebRTC therefore took it over, and every audio device module call failed with audio device module is unavailable. That is the #1206 and #1176 failure. The 1.6.3 bump on main (#1258) fixes it on the flutter_webrtc side (flutter-webrtc/flutter-webrtc#2186, flutter-webrtc/flutter-webrtc#2221). The singleton now stays on the first instance that created a factory.

One case is still wrong. When two engines both initialize WebRTC, the singleton belongs to whichever created its factory first. Mute mode, local recording, audio processing state and options, and local track lookups made from the other engine land on the wrong engine's audio device module and tracks.

Fix

  • Resolve the flutter_webrtc instance registered with the plugin's own engine through FlutterWebRTCPlugin.instance(for:), keyed by the registrar messenger. While that engine has not created a factory, for example a background engine that only toggles engine availability, fall back to the singleton.
  • The static paths keep the singleton, since they run without an engine: setEngineAvailability from an AppDelegate, applying a pending availability at registration, and the shared engine observer.
  • Correct comments that claimed accessing peerConnectionFactory creates it. Only the WebRTC initialize call does.

An earlier revision of this PR captured the first registered instance and held it strongly. On 1.6.3 that would keep a destroyed engine's instance, factory and audio device module alive and keep routing calls to them, so it is gone.

Verification

New e2e scenarios in livekit/e2e-flutter run on macOS. The runner spawns a headless second FlutterEngine that registers the generated plugins.

Scenario main (flutter_webrtc 1.6.3) this PR
Audio device module reachable after a second engine registers, and after it shuts down pass pass
The second engine initializes WebRTC first, then this engine sets the inputMixer mute mode. The second engine's module has to stay on voiceProcessing fail, the second engine's module changes to inputMixer pass

The first scenario fails on the base this PR started from (flutter_webrtc 1.6.2+hotfix.3), which reproduces #1206.

Also checked: flutter analyze, flutter test, and example builds for macOS and the iOS simulator.

…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[bot]

This comment was marked as resolved.

flutter_webrtc 1.6.3 keeps its process-wide instance on a factory owner and
hands it on when an engine detaches, so the first-registration capture is no
longer needed. Holding it strongly also kept a destroyed engine's instance,
factory and audio device module alive and kept routing calls to them.

The singleton is still the first factory owner, so with two engines that both
initialize WebRTC the second engine's mute mode, recording, audio processing
and local track lookups landed on the first engine. Resolve the instance
registered with this plugin's messenger instead, and fall back to the
singleton while that engine has no factory. The static paths keep the
singleton, since they run without an engine.
@hiroshihorie
hiroshihorie requested a review from changt as a code owner October 27, 2026 10:52
@hiroshihorie hiroshihorie changed the title Resolve the audio device module through the registered WebRTC plugin instance Resolve the WebRTC plugin registered with the calling engine Oct 27, 2026

@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 2 new potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 CallKit gate misses the active engine

After another Flutter engine registers, setEngineAvailability targets its singleton instead of the active engine. CallKit gating leaves the original engine running outside its activation window.

(Refers to this code)

Learn more

The native static entry point is used by an AppDelegate before or during CallKit activation. WebRTC's singleton changes when another Flutter engine registers, but the first engine's factory can still own the running audio device module. The method stores the requested availability, then checks only the current singleton; it returns false if that engine has no factory, or updates the wrong module if it does. The pending value cannot correct the first engine without another targeted application.

Example: The UI engine is recording with its factory initialized. A background engine registers without initializing WebRTC, and CallKit calls setEngineAvailability(isInputAvailable: false, isOutputAvailable: false). The call returns false and the UI engine continues recording; the gate was expected to stop it.

Recommended fix: Keep a process-level reference or registry of initialized WebRTC factories for native CallKit calls, and apply the gate to the active module instead of the moving singleton. Preserve deferred application before any factory exists.

Devin Review


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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

flutter_webrtc 1.6.3 no longer moves the singleton when an engine registers. The first registered instance keeps it, and another instance takes it over only by creating a factory while the current one has none. In this example the UI engine keeps the singleton after the background engine registers, so the gate reaches its audio device module and returns true. The reverse order (background engine first, UI engine initializing later) also ends on the UI engine through the takeover. With two engines that both create a factory, the gate targets the first factory owner, which is unchanged from main.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Device updates reach the wrong engine

After a second engine registers, audioDeviceModuleDidUpdateDevices forwards the first engine's updates to the second engine's singleton. The first engine misses audio device changes.

(Refers to this code)

Learn more

The shared observer receives callbacks from an audio device module attached to a WebRTC factory. The process-wide singleton can change when another Flutter engine registers, independently of the module that emitted the callback. Forwarding the callback to that singleton updates a different engine's WebRTC device listeners, leaving the owning engine's listeners without the event.

Example: Engine A connects Bluetooth audio and has an active peer connection. Engine B registers WebRTC, becoming the singleton. When engine A's module reports a device update, the callback invokes engine B's handler, so engine A's device-change listener receives nothing.

Recommended fix: Associate the callback's audioDeviceModule with its owning registered WebRTC plugin and forward to that plugin. Consider how to retain the association when multiple factories use the shared observer.

Devin Review


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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same premise as above. On flutter_webrtc 1.6.3 a second engine registering does not take the singleton, so engine A keeps receiving its own device updates. The remaining case is two engines that both create a factory, where the second engine's updates go to the first factory owner. That is unchanged from main, since this PR leaves the observer on the singleton, and fixing it needs a flutter_webrtc API that maps an audio device module back to its owning instance.

@hiroshihorie
hiroshihorie merged commit 7652723 into main Oct 27, 2026
15 checks passed
@hiroshihorie
hiroshihorie deleted the hiroshi/per-engine-webrtc-plugin branch October 27, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: ADM unavailable when a second Flutter engine replaces flutter_webrtc's sharedSingleton In IOS setMicrophoneEnabled(true) through Error

2 participants