Repository navigation
Resolve the WebRTC plugin registered with the calling engine - #1212
Conversation
…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
This comment has been minimized.
This comment has been minimized.
…brtc-plugin-rework
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.
There was a problem hiding this comment.
Devin Review found 2 new potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
Fixes #1206, fixes #1176.
Problem
LiveKitPlugin.swiftresolves the flutter_webrtc plugin through the process-wideFlutterWebRTCPlugin.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
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.setEngineAvailabilityfrom an AppDelegate, applying a pending availability at registration, and the shared engine observer.peerConnectionFactorycreates it. Only the WebRTCinitializecall 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
FlutterEnginethat registers the generated plugins.inputMixermute mode. The second engine's module has to stay onvoiceProcessinginputMixerThe 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.