Repository navigation
fix: keep the track processor across mute and restartTrack - #1249
Merged
Merged
Conversation
restartTrack called stop(), which destroys the processor and clears _processor, before it read _processor to attach it to the new track, so the processor was always dropped. Mute went through stop() too. With the default stopAudioCaptureOnMute and stopCameraCaptureOnMute (both true), muting stops the media track and unmuting restarts it, so a microphone or camera processor was lost after the first mute and unmute, and on every camera switch. mute and restartTrack now stop the media track and keep the processor, and restartTrack moves it to the new track as before. stop() still releases it, also when a mute already stopped the media track.
hiroshihorie
marked this pull request as ready for review
October 26, 2026 04:52
hiroshihorie
requested review from
changt,
cloudwebrtc and
xianshijing-lk
as code owners
October 26, 2026 04:52
cloudwebrtc
approved these changes
Oct 26, 2026
hiroshihorie
added a commit
that referenced
this pull request
Oct 26, 2026
) ## Why flutter_webrtc 1.6.3 carries two things livekit_client needs: - **Desktop video processors (CLTCU-14).** Windows and Linux plugins can attach native video processors to local tracks (flutter-webrtc/flutter-webrtc#2233), on par with Android, iOS and macOS. 1.6.3 also fixes a Linux deadlock in `desktopCapturer.getSources()` that hung every Linux screen share on flutter-webrtc `main` (flutter-webrtc/flutter-webrtc#2244). - **eMudhra CA compromise.** WebRTC-SDK 150.7871.04 and libwebrtc m150.7871.07 no longer bundle `AAA Certificate Services`, which cross-signs the compromised eMudhra intermediates, in the TURN/TLS roots. ## What changed Two commits: 1. `flutter_webrtc: 1.6.3`, and WebRTC-SDK `150.7871.04` in both podspecs and `android/build.gradle`. The podspecs pin WebRTC-SDK exactly like flutter_webrtc's own, so they have to move together. `pubspec.lock` only changes the `flutter_webrtc` entry. 2. `livekit_ffi: ^0.13.2`. Its libwebrtc also drops the AAA root. The SDK only calls `buildVersion()` today, so this changes no behavior. Each commit has its own changeset. ## Verification - `flutter analyze`, `flutter test` (466 passed) and `check_version` pass locally. - Linux arm64 e2e in Docker through a local livekit-server, using flutter_webrtc with these fixes plus #1249. A screen share with a native video processor attached through a `TrackProcessor` reaches the subscriber processed, on both simulcast layers and across `restartTrack()`. That run used the flutter-webrtc commits before the release tag, not the published 1.6.3. Not verified yet: - iOS, Android and macOS example builds with WebRTC-SDK 150.7871.04. CI covers the builds. - Windows at runtime. - That the shipped binaries no longer contain `AAA Certificate Services`, which is checked with `grep` on the resolved WebRTC binaries. ## Related - #1249 keeps a `TrackProcessor` across `restartTrack()` and default mute and unmute. Without it, a desktop processor is lost on every camera switch.
restartTrack() destroyed the processor and then called init() on the same instance for the new media track. That emitted a processor removed event followed by an added one, and lost what onPublish() had set up, since onPublish() is not called again. TrackProcessor already has restart() for this. restartTrack() now calls it with the new media track and keeps the processor attached. A processor whose restart() throws stays attached and the new track is still used.
restartTrack() replaced the sender track with the new raw track before the processor was attached to it. setProcessedTrack() only swaps the local track, so a processor with a processed track, such as Krisp on web, was no longer applied to what the sender carried after an unmute or a camera switch. The processor is now restarted on the new track first, and the sender and the simulcast senders get mediaStreamTrack, which is the processed track when the processor has one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A
TrackProcessorset on a local track is silently dropped in two cases:restartTrack(): it callsstop(), which destroys the processor and clears_processor, before it reads_processorto attach it to the new track. So the processor is always lost.setCameraPosition()andswitchCamera()go throughrestartTrack(), so a camera switch drops a video processor.stopAudioCaptureOnMuteandstopCameraCaptureOnMuteboth default totrue. Muting then callsstop(), and unmuting callsrestartTrack(), so a microphone or camera processor is gone after the first mute and unmute. Apps that set these options tofalseonly toggleenabledand are not affected.Fix
Three commits:
restartTrack().mute()andrestartTrack()now stop and dispose the media track without releasing the processor.stop()still releases it, also when a stop-on-mute already stopped the media track. Before, that case skippedstopProcessor().stop()keeps callingsuper.stop()itself becauseTrack.stopis@mustCallSuper, so the shared teardown takes its result as a parameter.restartTrack()used to calldestroy()and theninit()on the same processor. That emitted a processor removed event followed by an added one, and lost whatonPublish()had set up, sinceonPublish()is not called again. It now callsTrackProcessor.restart()with the new media track, as the JS SDK does. Krisp implementsrestart()on web, Android and iOS. Ifrestart()throws, the processor stays attached and the new track is still used.restartTrack(). The sender got the new raw track before the processor was attached to it, andsetProcessedTrack()only swaps the local track. A processor with a processed track, such as Krisp on web, was then no longer applied to what was sent after an unmute or a camera switch. The processor is now restarted first, and the sender and the simulcast senders getmediaStreamTrack, which is the processed track when there is one.On native platforms this re-binding is needed because flutter_webrtc creates a new
MediaStreamTrackfor every unmute with stop-on-mute and for every camera switch. Swift and Android keep the processor on the capturer, source or audio processing module, so their tracks are never replaced.Testing
test/track/local_track_processor_test.dart:restartTrack(): the processor stays, is restarted on the new track, and is not destroyedprocessed-mic-2after unmute andprocessed-mic-3afterrestartTrack(). Against the previous revision it gotmic-2andmic-3.restart()throws: the sender still gets the new trackstop()after a mute releases the processorflutter test: 471 passed.flutter analyze,dart formatandimport_sorterare clean.restartTrack()and unpublish. It is initialised once, restarted on each new media track, destroyed only on unpublish,onPublishruns once, and the subscriber keeps receiving packets. Both the microphone and the camera variants pass.mainfails the microphone variant with "processor dropped by mute". The camera variant was not run againstmain.TrackProcessor(flutter_webrtc's desktopAddVideoProcessor, feat(desktop): let native plugins attach video processors to local tracks flutter-webrtc/flutter-webrtc#2233). AfterrestartTrack()the processor moved to the newMediaStreamTrackand the subscriber kept decoding processed frames. Passed 3 runs in a row. Its test processor implementsrestart()as detach and attach, so it makes the same native calls with the later commits, but it was not re-run.Found while working on CLTCU-14 (desktop video processors for Proton).