Add stopMicrophoneOnMute option to release the microphone while muted - #987
adrian-niculescu wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: fe05d1c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
a3b9bd8 to
e60d2f6
Compare
Also fixed local track publication jobs leaking on full reconnect.
e60d2f6 to
fe05d1c
Compare
|
Apologies for the review time spent here, closing: the premise of this PR no longer holds. Since v2.23.1 the bundled WebRTC stops recording when all send streams are muted (webrtc-sdk/webrtc#206), so a plain mute already releases AudioRecord and turns the mic indicator off. Confirmed on an unmodified v2.28.0, details in #375. I'd been using the unpublish/republish workaround since before that release and assumed the mute behavior hadn't changed, with the web SDK's One part is worth keeping: the |
Fixes #375.
Android equivalent of the web SDK's
stopMicTrackOnMute, added asLocalAudioTrackOptions.stopMicrophoneOnMute.Two reasons to want it:
mediaTrack.enabled = false). The ADM keepsAudioRecordopen, so the OS microphone-in-use indicator stays lit for as long as the track is published. An indicator burning next to a muted mic is a privacy concern.Unpublishing and republishing the track does release the mic, but it leaks native memory on the audio path and churns track history on the server.
Leak measurements
Measured on LiveKit Android SDK 2.27.0, on a Pixel 6a (bluejay) running Android 17 beta (build
CP41.260701.005, API 37, 2026-07-05 security patch).On a two-party call with the camera off, toggling the microphone 50 times through unpublish/republish retained about 11.7 MB of native heap, roughly 0.23 MB per cycle, against a baseline noise band of 0.9 MB.
Three details point at retention rather than an artifact. There was no decay: five samples over the following 60 seconds read 104.1, 104.2, 103.6, 104.2, 104.2 MB.
Native Heap Sizegrew from 125.5 to 134.0 MB, so the arena was extended to satisfy live allocations, which fragmentation slack does not do.Graphicsis unchanged, so no part of the video pipeline is involved.The same test driving the camera instead fell back inside its baseline band within 16 seconds, which lines up with #971 fixing the video path by stopping the transceiver and disposing the
VideoSourceon unpublish. The audio path has no counterpart to that fix, and the measured version already contains it.That split looks deliberate rather than accidental. Reviewing the libwebrtc side in webrtc-sdk/webrtc#194, the transceiver teardown crash is described as "not even reproducible without audio", and the equivalent SDK-side fix then landed for video only, described as "restricting to video tracks (where it really matters), due to reproducible crashes in the audio stack". So the audio-side leak is known and was left in place rather than risk the audio path.
The web SDK's approach has no direct port here, since capture is owned by the ADM shared across the
PeerConnectionFactoryrather than by the track. Muting the publication instead stops capture throughPeerConnection.setAudioRecording(false), which flipsrecording_enabled_on the factory-scopedAudioStateand stops the ADM, releasingAudioRecordwith no renegotiation and no change to the publication.AudioState::AddSendingStreamchecks the same flag, so a reconnect or republish won't silently reopen the mic while muted. Capture restarts on unmute.Recording state is reconciled from the current publications on mute changes (including server-initiated ones), publish, unpublish, and
applyOptionschanges while published, with the desired state computed on the RTC thread so rapid toggles converge. Capture is shared by every local audio track, so it is only stopped while all published audio tracks are muted. Apps that don't opt in get no behavior change; a test assertssetAudioRecordingis never called for them.Also fixed:
prepareForFullReconnect()clears the publications, so theunpublishTrack()calls inrepublishTracks()could never find and cancel the publication jobs, leaking each old audio publication's feature collector on every full reconnect and leaving it sending feature updates for a stale track sid. The reconciliation added here rides those same jobs, which is how it surfaced. The jobs are now cancelled when preparing for the reconnect.Tests cover mute/unmute release and restore, zero native calls without opt-in, unpublish while muted,
applyOptionstoggling the flag in both directions while muted, and the all-tracks-muted rule with two simultaneous audio publications.