Skip to content

feat(audio): add AudioManager microphone mute mode API#1124

Open
hiroshihorie wants to merge 4 commits into
mainfrom
hiroshi/microphone-mute-mode
Open

feat(audio): add AudioManager microphone mute mode API#1124
hiroshihorie wants to merge 4 commits into
mainfrom
hiroshi/microphone-mute-mode

Conversation

@hiroshihorie

@hiroshihorie hiroshihorie commented Jul 5, 2026

Copy link
Copy Markdown
Member

Description

Adds AudioManager.setMicrophoneMuteMode / getMicrophoneMuteMode with a LiveKit-owned MicrophoneMuteMode enum (voiceProcessing, restart, inputMixer), mirroring the Swift SDK's AudioManager.microphoneMuteMode.

Background

On iOS/macOS the AVAudioEngine-based ADM defaults to voiceProcessing muting, which plays the platform's mute/unmute sound effect (see flutter-webrtc/flutter-webrtc#2098). Apps can select inputMixer or restart to mute silently.

await AudioManager.instance.setMicrophoneMuteMode(MicrophoneMuteMode.inputMixer);

The mode applies to LiveKit's own mute path: disabling a published local audio track drives the engine-level microphone mute in webrtc (WebRtcVoiceSendChannel::MuteStream -> adm->SetMicrophoneMute). For the fastest silent mute toggling, combine inputMixer with AudioCaptureOptions(stopAudioCaptureOnMute: false) (documented on the API).

Implementation notes

  • Self-contained: implemented on LiveKit's own plugin and method channel. LiveKitPlugin.swift calls the ADM's muteMode/setMuteMode: directly (public API in the WebRTC-SDK pod already linked), so this does not depend on any flutter_webrtc change or release. The companion flutter-webrtc PR (feat: expose microphone mute mode and ADM-level microphone mute flutter-webrtc/flutter-webrtc#2105) exposes the same control to plain flutter-webrtc users independently.
  • No flutter_webrtc types on the public surface: the enum is LiveKit-owned and named to match the Swift SDK (restart, not restartEngine).
  • No-op on non-Apple platforms, including for unknown, so set(await get()) round-trips safely in cross-platform code.
  • The setter runs off the platform thread natively, since a mode change while muted can rebuild the audio engine. Setter errors propagate to the caller (mirrors the Swift SDK's throwing API), the getter falls back to unknown.
  • Engine-wide state, recommended to set once before connecting.

Testing

  • dart analyze lib test clean, all 315 tests pass.
  • iOS example builds against the released flutter_webrtc 1.5.2 pin, no dependency override needed.

Adds AudioManager.setMicrophoneMuteMode/getMicrophoneMuteMode with a
LiveKit-owned MicrophoneMuteMode enum (voiceProcessing, restart,
inputMixer), mirroring the Swift SDK. Controls how the AVAudioEngine
audio device module mutes mic input on iOS/macOS. No-op elsewhere,
including for unknown, so get/set round-trips are safe cross-platform.
voiceProcessing (the default) plays the platform mute tone, inputMixer
and restart mute silently.

The mode applies to LiveKit's own mute path: disabling a published
local audio track drives the engine-level microphone mute.

Implemented on LiveKit's own plugin and method channel. The audio
device module API is public in the WebRTC-SDK pod, so no flutter_webrtc
change or release is needed.
@hiroshihorie hiroshihorie force-pushed the hiroshi/microphone-mute-mode branch from 6165bde to f4d7b62 Compare July 7, 2026 09:34
@hiroshihorie hiroshihorie marked this pull request as ready for review July 7, 2026 09:40

@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.

Open in Devin Review

Comment on lines +240 to +245
static Future<void> setMicrophoneMuteMode(String mode) async {
await channel.invokeMethod<void>(
'setMicrophoneMuteMode',
<String, dynamic>{'mode': mode},
);
}

@devin-ai-integration devin-ai-integration Bot Jul 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Deliberate error-handling asymmetry between set and get native methods

The Native.setMicrophoneMuteMode at lib/src/support/native.dart:240-245 deliberately does NOT wrap the channel call in a try-catch, allowing PlatformExceptions to propagate to the caller. This is explicitly documented and intentional (a failed set means the caller's assumption about muting behavior is wrong). However, this is a departure from the pattern used by nearly every other method in the Native class (e.g. setAndroidSpeakerphoneOn, configureAudio, etc.), which all swallow errors with logger.warning. The Native.getMicrophoneMuteMode at lib/src/support/native.dart:250-257 follows the conventional swallow-and-return-null pattern. This asymmetry is justified by the doc comment but reviewers should be aware that callers of AudioManager.setMicrophoneMuteMode must handle exceptions, unlike most other AudioManager methods.

Open in 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.

Fair point. 4ec7888 documents the throwing contract on the public AudioManager.setMicrophoneMuteMode, matching the wording used for setEngineAvailability in #1127. The asymmetry itself stays: the setter propagates because a silently failed mode change would leave muting behavior different from what the caller selected, and the getter falls back to unknown since a failed read is harmless.

The setter deliberately propagates native errors, unlike the getter
which falls back to unknown. A silently failed mode change would leave
muting behavior different from what the caller selected. This mirrors
the Swift SDK's throwing API.

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

Open in Devin Review

Comment on lines +476 to +489
DispatchQueue.global(qos: .userInitiated).async {
let admResult = adm.setMuteMode(mode)
DispatchQueue.main.async {
if admResult == 0 {
result(nil)
} else {
result(FlutterError(
code: "setMicrophoneMuteMode",
message: "Audio engine returned error code: \(admResult)",
details: nil
))
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Background thread dispatch for setMuteMode may interact with concurrent calls

In shared_swift/LiveKitPlugin.swift:476-489, setMuteMode is dispatched to DispatchQueue.global(qos: .userInitiated) because it can rebuild the audio engine. If setMicrophoneMuteMode is called rapidly in succession (e.g. user toggling settings), multiple setMuteMode calls could execute concurrently on different global queue threads. The safety of this depends on whether RTCAudioDeviceModule.setMuteMode is internally serialized. This matches the pattern used by handleStartLocalRecording (line 448-461), so it's consistent with existing code, but worth noting if the underlying ADM method isn't thread-safe.

Open in 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

Development

Successfully merging this pull request may close these issues.

1 participant