From 2012b386399b636f22e9f87ec838a370eb48b960 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:34:49 +0900 Subject: [PATCH 1/7] chore(deps): bump WebRTC-SDK to 144.7559.09 --- android/build.gradle | 2 +- ios/livekit_client.podspec | 2 +- macos/livekit_client.podspec | 2 +- pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 17fa1f448..82e5dd8d7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -59,7 +59,7 @@ android { dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.mockito:mockito-core:5.0.0") - implementation 'io.github.webrtc-sdk:android:144.7559.01' + implementation 'io.github.webrtc-sdk:android:144.7559.09' implementation 'io.livekit:noise:2.0.0' } diff --git a/ios/livekit_client.podspec b/ios/livekit_client.podspec index e9b3032eb..004152ca5 100644 --- a/ios/livekit_client.podspec +++ b/ios/livekit_client.podspec @@ -16,6 +16,6 @@ Pod::Spec.new do |s| s.static_framework = true s.dependency 'Flutter' - s.dependency 'WebRTC-SDK', '144.7559.01' + s.dependency 'WebRTC-SDK', '144.7559.09' s.dependency 'flutter_webrtc' end diff --git a/macos/livekit_client.podspec b/macos/livekit_client.podspec index 16c318b85..d96841f45 100644 --- a/macos/livekit_client.podspec +++ b/macos/livekit_client.podspec @@ -16,6 +16,6 @@ Pod::Spec.new do |s| s.static_framework = true s.dependency 'FlutterMacOS' - s.dependency 'WebRTC-SDK', '144.7559.01' + s.dependency 'WebRTC-SDK', '144.7559.09' s.dependency 'flutter_webrtc' end diff --git a/pubspec.yaml b/pubspec.yaml index 2c8783473..2c768472e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: json_annotation: ^4.9.0 # Fix version to avoid version conflicts between WebRTC-SDK pods, which both this package and flutter_webrtc depend on. - flutter_webrtc: 1.4.0 + flutter_webrtc: 1.5.0 dart_webrtc: ^1.8.0 dev_dependencies: From fe697cd0720fd1c413d2a6090a909d3fc1618ffa Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:01:37 +0900 Subject: [PATCH 2/7] feat(audio): add runtime AudioProcessingOptions for LocalAudioTrack Introduce AudioProcessingMode and AudioProcessingOptions in track options, plus LocalAudioTrack.setAudioProcessingOptions() to apply per-component echo cancellation / noise suppression / auto gain control / high-pass filter modes at runtime. AudioCaptureOptions now carries these processing fields and serializes the per-component modes into its capture constraints. Updates are currently delegated to flutter_webrtc; a follow-up moves the implementation into this SDK's own native plugin. --- lib/src/track/local/audio.dart | 45 ++++++++++- lib/src/track/options.dart | 138 +++++++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 9 deletions(-) diff --git a/lib/src/track/local/audio.dart b/lib/src/track/local/audio.dart index e3675a340..ff542f861 100644 --- a/lib/src/track/local/audio.dart +++ b/lib/src/track/local/audio.dart @@ -19,19 +19,20 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; import '../../events.dart'; +import '../../internal/events.dart'; import '../../logger.dart'; import '../../options.dart'; import '../../stats/audio_source_stats.dart'; import '../../stats/stats.dart'; import '../../types/other.dart'; import '../audio_management.dart'; -import '../options.dart'; +import '../options.dart' as track_options; import 'local.dart'; class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMixin { // Options used for this track @override - covariant AudioCaptureOptions currentOptions; + covariant track_options.AudioCaptureOptions currentOptions; AudioPublishOptions? lastPublishOptions; @@ -45,6 +46,32 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi } } + Future setAudioProcessingOptions(track_options.AudioProcessingOptions options) async { + final nextOptions = currentOptions.copyWith(processing: options); + final success = await rtc.AudioProcessingMediaStreamTrackExtension(mediaStreamTrack).setAudioProcessingOptions( + rtc.AudioProcessingOptions( + echoCancellation: options.echoCancellation, + noiseSuppression: options.noiseSuppression, + autoGainControl: options.autoGainControl, + highPassFilter: options.highPassFilter, + echoCancellationMode: _rtcAudioProcessingMode(options.echoCancellationMode), + noiseSuppressionMode: _rtcAudioProcessingMode(options.noiseSuppressionMode), + autoGainControlMode: _rtcAudioProcessingMode(options.autoGainControlMode), + highPassFilterMode: _rtcAudioProcessingMode(options.highPassFilterMode), + ), + ); + + if (success) { + currentOptions = nextOptions; + events.emit(LocalTrackOptionsUpdatedEvent( + track: this, + options: currentOptions, + )); + } + + return success; + } + num? _currentBitrate; num? get currentBitrate => _currentBitrate; @@ -126,9 +153,9 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi /// Creates a new audio track from the default audio input device. static Future create([ - AudioCaptureOptions? options, + track_options.AudioCaptureOptions? options, ]) async { - options ??= const AudioCaptureOptions(); + options ??= const track_options.AudioCaptureOptions(); final stream = await LocalTrack.createStream(options); final track = LocalAudioTrack( @@ -145,3 +172,13 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi return track; } } + +rtc.AudioProcessingMode _rtcAudioProcessingMode( + track_options.AudioProcessingMode mode, +) { + return switch (mode) { + track_options.AudioProcessingMode.platform => rtc.AudioProcessingMode.platform, + track_options.AudioProcessingMode.software => rtc.AudioProcessingMode.software, + track_options.AudioProcessingMode.automatic => rtc.AudioProcessingMode.automatic, + }; +} diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index d5bfc5bbf..24eaafbe8 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -242,8 +242,79 @@ abstract class VideoCaptureOptions extends LocalTrackOptions { Map toMediaConstraintsMap() => params.toMediaConstraintsMap(); } +/// Selects whether a voice-processing component uses platform or software processing. +enum AudioProcessingMode { + automatic('auto'), + platform('platform'), + software('software'); + + const AudioProcessingMode(this.constraintValue); + + final String constraintValue; +} + +/// Runtime voice-processing options for a [LocalAudioTrack]. +/// +/// These values update the native local audio source without restarting +/// capture. When the track is being sent, the native WebRTC sender reapplies +/// the updated processing config. The effective audio processing module config +/// is shared by the native voice engine/channel, so conflicting updates from +/// multiple local tracks are not isolated per track. +class AudioProcessingOptions { + const AudioProcessingOptions({ + required this.echoCancellation, + required this.noiseSuppression, + required this.autoGainControl, + required this.highPassFilter, + this.echoCancellationMode = AudioProcessingMode.automatic, + this.noiseSuppressionMode = AudioProcessingMode.automatic, + this.autoGainControlMode = AudioProcessingMode.automatic, + this.highPassFilterMode = AudioProcessingMode.automatic, + }); + + const AudioProcessingOptions.communication() + : echoCancellation = true, + noiseSuppression = true, + autoGainControl = true, + highPassFilter = true, + echoCancellationMode = AudioProcessingMode.automatic, + noiseSuppressionMode = AudioProcessingMode.automatic, + autoGainControlMode = AudioProcessingMode.automatic, + highPassFilterMode = AudioProcessingMode.automatic; + + const AudioProcessingOptions.raw() + : echoCancellation = false, + noiseSuppression = false, + autoGainControl = false, + highPassFilter = false, + echoCancellationMode = AudioProcessingMode.automatic, + noiseSuppressionMode = AudioProcessingMode.automatic, + autoGainControlMode = AudioProcessingMode.automatic, + highPassFilterMode = AudioProcessingMode.automatic; + + final bool echoCancellation; + final bool noiseSuppression; + final bool autoGainControl; + final bool highPassFilter; + final AudioProcessingMode echoCancellationMode; + final AudioProcessingMode noiseSuppressionMode; + final AudioProcessingMode autoGainControlMode; + final AudioProcessingMode highPassFilterMode; + + Map toMap() => { + 'echoCancellation': echoCancellation, + 'noiseSuppression': noiseSuppression, + 'autoGainControl': autoGainControl, + 'highPassFilter': highPassFilter, + 'echoCancellationMode': echoCancellationMode.constraintValue, + 'noiseSuppressionMode': noiseSuppressionMode.constraintValue, + 'autoGainControlMode': autoGainControlMode.constraintValue, + 'highPassFilterMode': highPassFilterMode.constraintValue, + }; +} + /// Options used when creating a [LocalAudioTrack]. -class AudioCaptureOptions extends LocalTrackOptions { +class AudioCaptureOptions extends LocalTrackOptions implements AudioProcessingOptions { /// The deviceId of the capture device to use. /// Available deviceIds can be obtained through `flutter_webrtc`: /// ``` @@ -256,22 +327,38 @@ class AudioCaptureOptions extends LocalTrackOptions { /// Attempt to use noiseSuppression option (if supported by the platform) /// See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/noiseSuppression /// Defaults to true. + @override final bool noiseSuppression; /// Attempt to use echoCancellation option (if supported by the platform) /// See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/echoCancellation /// Defaults to true. + @override final bool echoCancellation; /// Attempt to use autoGainControl option (if supported by the platform) /// See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/autoGainControl /// Defaults to true. + @override final bool autoGainControl; /// Attempt to use highPassFilter options (if supported by the platform) /// Defaults to false. + @override final bool highPassFilter; + @override + final AudioProcessingMode echoCancellationMode; + + @override + final AudioProcessingMode noiseSuppressionMode; + + @override + final AudioProcessingMode autoGainControlMode; + + @override + final AudioProcessingMode highPassFilterMode; + /// Attempt to use typingNoiseDetection option (if supported by the platform) /// Defaults to true. final bool typingNoiseDetection; @@ -292,12 +379,30 @@ class AudioCaptureOptions extends LocalTrackOptions { this.echoCancellation = true, this.autoGainControl = true, this.highPassFilter = false, + this.echoCancellationMode = AudioProcessingMode.automatic, + this.noiseSuppressionMode = AudioProcessingMode.automatic, + this.autoGainControlMode = AudioProcessingMode.automatic, + this.highPassFilterMode = AudioProcessingMode.automatic, this.voiceIsolation = true, this.typingNoiseDetection = true, this.stopAudioCaptureOnMute = true, this.processor, }); + AudioProcessingOptions get processing => AudioProcessingOptions( + echoCancellation: echoCancellation, + noiseSuppression: noiseSuppression, + autoGainControl: autoGainControl, + highPassFilter: highPassFilter, + echoCancellationMode: echoCancellationMode, + noiseSuppressionMode: noiseSuppressionMode, + autoGainControlMode: autoGainControlMode, + highPassFilterMode: highPassFilterMode, + ); + + @override + Map toMap() => processing.toMap(); + @override Map toMediaConstraintsMap() { final constraints = {}; @@ -316,6 +421,10 @@ class AudioCaptureOptions extends LocalTrackOptions { {'autoGainControl': false}, {'voiceIsolation': false}, {'googDAEchoCancellation': false}, + if (!kIsWeb) {'echoCancellationMode': echoCancellationMode.constraintValue}, + if (!kIsWeb) {'noiseSuppressionMode': noiseSuppressionMode.constraintValue}, + if (!kIsWeb) {'autoGainControlMode': autoGainControlMode.constraintValue}, + if (!kIsWeb) {'highPassFilterMode': highPassFilterMode.constraintValue}, ]; } else { /// in we platform it's not possible to provide optional and mandatory parameters. @@ -334,6 +443,10 @@ class AudioCaptureOptions extends LocalTrackOptions { {'googAutoGainControl': autoGainControl}, {'googHighpassFilter': highPassFilter}, {'googTypingNoiseDetection': typingNoiseDetection}, + if (!kIsWeb) {'echoCancellationMode': echoCancellationMode.constraintValue}, + if (!kIsWeb) {'noiseSuppressionMode': noiseSuppressionMode.constraintValue}, + if (!kIsWeb) {'autoGainControlMode': autoGainControlMode.constraintValue}, + if (!kIsWeb) {'highPassFilterMode': highPassFilterMode.constraintValue}, ]; } } @@ -358,15 +471,30 @@ class AudioCaptureOptions extends LocalTrackOptions { bool? echoCancellation, bool? autoGainControl, bool? highPassFilter, + AudioProcessingMode? echoCancellationMode, + AudioProcessingMode? noiseSuppressionMode, + AudioProcessingMode? autoGainControlMode, + AudioProcessingMode? highPassFilterMode, + AudioProcessingOptions? processing, + bool? voiceIsolation, bool? typingNoiseDetection, + bool? stopAudioCaptureOnMute, + TrackProcessor? processor, }) { return AudioCaptureOptions( deviceId: deviceId ?? this.deviceId, - noiseSuppression: noiseSuppression ?? this.noiseSuppression, - echoCancellation: echoCancellation ?? this.echoCancellation, - autoGainControl: autoGainControl ?? this.autoGainControl, - highPassFilter: highPassFilter ?? this.highPassFilter, + noiseSuppression: processing?.noiseSuppression ?? noiseSuppression ?? this.noiseSuppression, + echoCancellation: processing?.echoCancellation ?? echoCancellation ?? this.echoCancellation, + autoGainControl: processing?.autoGainControl ?? autoGainControl ?? this.autoGainControl, + highPassFilter: processing?.highPassFilter ?? highPassFilter ?? this.highPassFilter, + echoCancellationMode: processing?.echoCancellationMode ?? echoCancellationMode ?? this.echoCancellationMode, + noiseSuppressionMode: processing?.noiseSuppressionMode ?? noiseSuppressionMode ?? this.noiseSuppressionMode, + autoGainControlMode: processing?.autoGainControlMode ?? autoGainControlMode ?? this.autoGainControlMode, + highPassFilterMode: processing?.highPassFilterMode ?? highPassFilterMode ?? this.highPassFilterMode, + voiceIsolation: voiceIsolation ?? this.voiceIsolation, typingNoiseDetection: typingNoiseDetection ?? this.typingNoiseDetection, + stopAudioCaptureOnMute: stopAudioCaptureOnMute ?? this.stopAudioCaptureOnMute, + processor: processor ?? this.processor, ); } } From 710abfac44847fb3ee3f2f05fcd567ae24b6029b Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:14:24 +0900 Subject: [PATCH 3/7] feat(audio): route audio processing through the LiveKit native plugin Move setAudioProcessingOptions off flutter_webrtc's Dart API and onto this SDK's own `livekit_client` method channel. The iOS/macOS and Android plugins resolve the local track from flutter_webrtc's shared registry and call the WebRTC AudioEngine setAudioProcessingOptions API directly, returning {result, code, message}. This removes the dependency on fork-only flutter_webrtc Dart symbols (rtc.AudioProcessingOptions / rtc.AudioProcessingMode), so the SDK once again analyzes cleanly against the published flutter_webrtc package. --- .../kotlin/io/livekit/plugin/LiveKitPlugin.kt | 65 +++++++++++++++++++ lib/src/support/native.dart | 28 ++++++++ lib/src/track/local/audio.dart | 25 ++----- shared_swift/LiveKitPlugin.swift | 63 ++++++++++++++++++ 4 files changed, 160 insertions(+), 21 deletions(-) diff --git a/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt b/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt index 88c73c658..d3ccd5391 100644 --- a/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt +++ b/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt @@ -29,6 +29,10 @@ import com.cloudwebrtc.webrtc.FlutterWebRTCPlugin import com.cloudwebrtc.webrtc.audio.LocalAudioTrack import io.flutter.plugin.common.BinaryMessenger import org.webrtc.AudioTrack +import org.webrtc.audio.AudioProcessingComponentOptions +import org.webrtc.audio.AudioProcessingMode +import org.webrtc.audio.AudioProcessingOptions +import org.webrtc.audio.AudioProcessingOptionsResult /** LiveKitPlugin */ class LiveKitPlugin : FlutterPlugin, MethodCallHandler { @@ -210,6 +214,63 @@ class LiveKitPlugin : FlutterPlugin, MethodCallHandler { result.success(true) } + private fun handleSetAudioProcessingOptions(call: MethodCall, result: Result) { + val trackId = call.argument("trackId") + if (trackId == null) { + result.error("INVALID_ARGUMENT", "trackId is required", null) + return + } + + val mediaTrack = (flutterWebRTCPlugin.getLocalTrack(trackId) as? LocalAudioTrack)?.track + if (mediaTrack !is AudioTrack) { + result.error("INVALID_ARGUMENT", "track is not a local audio track", null) + return + } + + val options = AudioProcessingOptions( + AudioProcessingComponentOptions( + call.argument("echoCancellation") ?: true, + audioProcessingMode(call.argument("echoCancellationMode")), + ), + AudioProcessingComponentOptions( + call.argument("noiseSuppression") ?: true, + audioProcessingMode(call.argument("noiseSuppressionMode")), + ), + AudioProcessingComponentOptions( + call.argument("autoGainControl") ?: true, + audioProcessingMode(call.argument("autoGainControlMode")), + ), + AudioProcessingComponentOptions( + call.argument("highPassFilter") ?: false, + audioProcessingMode(call.argument("highPassFilterMode")), + ), + ) + + val processingResult = mediaTrack.setAudioProcessingOptions(options) + result.success( + mapOf( + "result" to processingResult.isSuccess, + "code" to audioProcessingResultCodeString(processingResult.code), + "message" to processingResult.message, + ), + ) + } + + private fun audioProcessingMode(value: String?): AudioProcessingMode = when (value) { + "platform" -> AudioProcessingMode.PLATFORM + "software" -> AudioProcessingMode.SOFTWARE + else -> AudioProcessingMode.AUTOMATIC + } + + private fun audioProcessingResultCodeString(code: AudioProcessingOptionsResult.Code): String = when (code) { + AudioProcessingOptionsResult.Code.APPLIED -> "applied" + AudioProcessingOptionsResult.Code.STORED -> "stored" + AudioProcessingOptionsResult.Code.REJECTED_REMOTE_TRACK -> "rejectedRemoteTrack" + AudioProcessingOptionsResult.Code.REJECTED_INVALID_COMBINATION -> "rejectedInvalidCombination" + AudioProcessingOptionsResult.Code.REJECTED_PLATFORM_UNAVAILABLE -> "rejectedPlatformUnavailable" + AudioProcessingOptionsResult.Code.APPLY_FAILED -> "applyFailed" + } + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "startVisualizer" -> { @@ -228,6 +289,10 @@ class LiveKitPlugin : FlutterPlugin, MethodCallHandler { handleStopAudioRenderer(call, result) } + "setAudioProcessingOptions" -> { + handleSetAudioProcessingOptions(call, result) + } + else -> { result.notImplemented() } diff --git a/lib/src/support/native.dart b/lib/src/support/native.dart index 2f3b3e24d..035977c36 100644 --- a/lib/src/support/native.dart +++ b/lib/src/support/native.dart @@ -50,6 +50,34 @@ class Native { } } + /// Applies runtime audio processing options to a local audio track. + /// + /// Resolved natively against the underlying WebRTC audio track owned by + /// flutter_webrtc; [options] is the serialized [AudioProcessingOptions] map. + /// Returns whether the native layer applied/stored the request. + @internal + static Future setAudioProcessingOptions( + String trackId, + Map options, + ) async { + try { + final response = await channel.invokeMethod( + 'setAudioProcessingOptions', + { + 'trackId': trackId, + ...options, + }, + ); + if (response is Map) { + return response['result'] == true; + } + return response == true; + } catch (error) { + logger.warning('setAudioProcessingOptions did throw $error'); + return false; + } + } + @internal static Future startVisualizer( String trackId, { diff --git a/lib/src/track/local/audio.dart b/lib/src/track/local/audio.dart index ff542f861..9edea8eb1 100644 --- a/lib/src/track/local/audio.dart +++ b/lib/src/track/local/audio.dart @@ -24,6 +24,7 @@ import '../../logger.dart'; import '../../options.dart'; import '../../stats/audio_source_stats.dart'; import '../../stats/stats.dart'; +import '../../support/native.dart'; import '../../types/other.dart'; import '../audio_management.dart'; import '../options.dart' as track_options; @@ -48,17 +49,9 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi Future setAudioProcessingOptions(track_options.AudioProcessingOptions options) async { final nextOptions = currentOptions.copyWith(processing: options); - final success = await rtc.AudioProcessingMediaStreamTrackExtension(mediaStreamTrack).setAudioProcessingOptions( - rtc.AudioProcessingOptions( - echoCancellation: options.echoCancellation, - noiseSuppression: options.noiseSuppression, - autoGainControl: options.autoGainControl, - highPassFilter: options.highPassFilter, - echoCancellationMode: _rtcAudioProcessingMode(options.echoCancellationMode), - noiseSuppressionMode: _rtcAudioProcessingMode(options.noiseSuppressionMode), - autoGainControlMode: _rtcAudioProcessingMode(options.autoGainControlMode), - highPassFilterMode: _rtcAudioProcessingMode(options.highPassFilterMode), - ), + final success = await Native.setAudioProcessingOptions( + mediaStreamTrack.id!, + options.toMap(), ); if (success) { @@ -172,13 +165,3 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi return track; } } - -rtc.AudioProcessingMode _rtcAudioProcessingMode( - track_options.AudioProcessingMode mode, -) { - return switch (mode) { - track_options.AudioProcessingMode.platform => rtc.AudioProcessingMode.platform, - track_options.AudioProcessingMode.software => rtc.AudioProcessingMode.software, - track_options.AudioProcessingMode.automatic => rtc.AudioProcessingMode.automatic, - }; -} diff --git a/shared_swift/LiveKitPlugin.swift b/shared_swift/LiveKitPlugin.swift index a2d8d8639..325087a09 100644 --- a/shared_swift/LiveKitPlugin.swift +++ b/shared_swift/LiveKitPlugin.swift @@ -388,6 +388,67 @@ public class LiveKitPlugin: NSObject, FlutterPlugin { return versions.map { String($0) }.joined(separator: ".") } + public func handleSetAudioProcessingOptions(args: [String: Any?], result: @escaping FlutterResult) { + guard let trackId = args["trackId"] as? String else { + result(FlutterError(code: "setAudioProcessingOptions", message: "trackId is required", details: nil)) + return + } + + let webrtc = FlutterWebRTCPlugin.sharedSingleton() + guard let localTrack = webrtc?.localTracks?[trackId] as? LocalAudioTrack, + let audioTrack = localTrack.track() as? RTCAudioTrack + else { + result(FlutterError(code: "setAudioProcessingOptions", message: "track is not a local audio track", details: nil)) + return + } + + let options = RTCAudioProcessingOptions( + echoCancellationOptions: RTCAudioProcessingComponentOptions( + enabled: (args["echoCancellation"] as? Bool) ?? true, + mode: LiveKitPlugin.audioProcessingMode(from: args["echoCancellationMode"] as? String) + ), + noiseSuppressionOptions: RTCAudioProcessingComponentOptions( + enabled: (args["noiseSuppression"] as? Bool) ?? true, + mode: LiveKitPlugin.audioProcessingMode(from: args["noiseSuppressionMode"] as? String) + ), + autoGainControlOptions: RTCAudioProcessingComponentOptions( + enabled: (args["autoGainControl"] as? Bool) ?? true, + mode: LiveKitPlugin.audioProcessingMode(from: args["autoGainControlMode"] as? String) + ), + highPassFilterOptions: RTCAudioProcessingComponentOptions( + enabled: (args["highPassFilter"] as? Bool) ?? false, + mode: LiveKitPlugin.audioProcessingMode(from: args["highPassFilterMode"] as? String) + ) + ) + + let processingResult = audioTrack.setAudioProcessingOptions(options) + result([ + "result": processingResult.isSuccess, + "code": LiveKitPlugin.audioProcessingResultCodeString(processingResult.code), + "message": processingResult.message, + ]) + } + + static func audioProcessingMode(from string: String?) -> RTCAudioProcessingMode { + switch string { + case "platform": return .platform + case "software": return .software + default: return .automatic + } + } + + static func audioProcessingResultCodeString(_ code: RTCAudioProcessingOptionsResultCode) -> String { + switch code { + case .applied: return "applied" + case .stored: return "stored" + case .rejectedRemoteTrack: return "rejectedRemoteTrack" + case .rejectedInvalidCombination: return "rejectedInvalidCombination" + case .rejectedPlatformUnavailable: return "rejectedPlatformUnavailable" + case .applyFailed: return "applyFailed" + @unknown default: return "applyFailed" + } + } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let args = call.arguments as? [String: Any?] else { print("[LiveKit] arguments must be a dictionary") @@ -406,6 +467,8 @@ public class LiveKitPlugin: NSObject, FlutterPlugin { handleStartAudioRenderer(args: args, result: result) case "stopAudioRenderer": handleStopAudioRenderer(args: args, result: result) + case "setAudioProcessingOptions": + handleSetAudioProcessingOptions(args: args, result: result) case "osVersionString": result(LiveKitPlugin.osVersionString()) #if os(iOS) From cc2ff62aa0fd8abb912fb3b899d344fdef1d545e Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:34:49 +0900 Subject: [PATCH 4/7] feat(audio): apply audio processing options with a typed result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LocalAudioTrack.setAudioProcessingOptions returns an AudioProcessingApplyResult (code + message + isSuccess) for operational outcomes — applied/stored, and the device-capability rejections platform-unavailable / apply-failed (inspect isSuccess). It throws AudioProcessingException only for malformed requests (invalid mode combination, or a non-local track). The native plugin handlers return {result, code, message}; the Dart side maps the code into the result or the exception accordingly. --- lib/src/support/native.dart | 30 +++++++++---------- lib/src/track/local/audio.dart | 24 +++++++++++---- lib/src/track/options.dart | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 22 deletions(-) diff --git a/lib/src/support/native.dart b/lib/src/support/native.dart index 035977c36..d6fd11c25 100644 --- a/lib/src/support/native.dart +++ b/lib/src/support/native.dart @@ -54,28 +54,24 @@ class Native { /// /// Resolved natively against the underlying WebRTC audio track owned by /// flutter_webrtc; [options] is the serialized [AudioProcessingOptions] map. - /// Returns whether the native layer applied/stored the request. + /// Returns the native result map (`result`/`code`/`message`) so the caller + /// can surface typed rejections. Channel errors propagate to the caller. @internal - static Future setAudioProcessingOptions( + static Future> setAudioProcessingOptions( String trackId, Map options, ) async { - try { - final response = await channel.invokeMethod( - 'setAudioProcessingOptions', - { - 'trackId': trackId, - ...options, - }, - ); - if (response is Map) { - return response['result'] == true; - } - return response == true; - } catch (error) { - logger.warning('setAudioProcessingOptions did throw $error'); - return false; + final response = await channel.invokeMethod( + 'setAudioProcessingOptions', + { + 'trackId': trackId, + ...options, + }, + ); + if (response is Map) { + return response.map((key, value) => MapEntry(key.toString(), value)); } + return {}; } @internal diff --git a/lib/src/track/local/audio.dart b/lib/src/track/local/audio.dart index 9edea8eb1..67eab47a8 100644 --- a/lib/src/track/local/audio.dart +++ b/lib/src/track/local/audio.dart @@ -47,22 +47,36 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi } } - Future setAudioProcessingOptions(track_options.AudioProcessingOptions options) async { + Future setAudioProcessingOptions( + track_options.AudioProcessingOptions options) async { final nextOptions = currentOptions.copyWith(processing: options); - final success = await Native.setAudioProcessingOptions( + final response = await Native.setAudioProcessingOptions( mediaStreamTrack.id!, options.toMap(), ); - if (success) { + final code = track_options.AudioProcessingOptionsResultCode.fromValue(response['code'] as String?); + final message = (response['message'] as String?) ?? ''; + + // Malformed requests (incompatible modes, or a non-local track) are caller + // bugs — surface them loudly rather than as a silently-unsuccessful result. + if (code == track_options.AudioProcessingOptionsResultCode.rejectedInvalidCombination || + code == track_options.AudioProcessingOptionsResultCode.rejectedRemoteTrack) { + throw track_options.AudioProcessingException( + code, + message.isNotEmpty ? message : 'Unable to apply audio processing options', + ); + } + + final result = track_options.AudioProcessingApplyResult(code, message); + if (result.isSuccess) { currentOptions = nextOptions; events.emit(LocalTrackOptionsUpdatedEvent( track: this, options: currentOptions, )); } - - return success; + return result; } num? _currentBitrate; diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index 24eaafbe8..c31e99513 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -516,3 +516,56 @@ class AudioOutputOptions { ); } } + +/// Result code from applying [AudioProcessingOptions], mirroring the native +/// `AudioProcessingOptionsResult`. `applied`/`stored` are success; the rest are +/// rejections. +enum AudioProcessingOptionsResultCode { + applied('applied'), + stored('stored'), + rejectedRemoteTrack('rejectedRemoteTrack'), + rejectedInvalidCombination('rejectedInvalidCombination'), + rejectedPlatformUnavailable('rejectedPlatformUnavailable'), + applyFailed('applyFailed'); + + const AudioProcessingOptionsResultCode(this.value); + + final String value; + + static AudioProcessingOptionsResultCode fromValue(String? value) => + AudioProcessingOptionsResultCode.values.firstWhere( + (e) => e.value == value, + orElse: () => AudioProcessingOptionsResultCode.applyFailed, + ); + + bool get isSuccess => + this == AudioProcessingOptionsResultCode.applied || this == AudioProcessingOptionsResultCode.stored; +} + +/// Thrown when the native layer rejects requested [AudioProcessingOptions] +/// (e.g. an invalid platform/software combination, or platform processing that +/// is unavailable on the device). +class AudioProcessingException implements Exception { + AudioProcessingException(this.code, this.message); + + final AudioProcessingOptionsResultCode code; + final String message; + + @override + String toString() => 'AudioProcessingException(${code.value}): $message'; +} + +/// Outcome of applying [AudioProcessingOptions]. +/// +/// Returned for operational outcomes — `applied`/`stored` (success), and the +/// device-capability rejections `rejectedPlatformUnavailable`/`applyFailed` +/// (inspect [isSuccess]). A malformed request (incompatible modes, or a +/// non-local track) throws [AudioProcessingException] instead. +class AudioProcessingApplyResult { + AudioProcessingApplyResult(this.code, this.message); + + final AudioProcessingOptionsResultCode code; + final String message; + + bool get isSuccess => code.isSuccess; +} From 57dc54848c096556df04ba7018923134056d212b Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:55:40 +0800 Subject: [PATCH 5/7] feat(audio): engine-wide audio processing state read-back Surfaces the WebRTC-SDK v2 audio processing state on AudioManager: the audio processing module is owned by the native peer connection factory and shared engine-wide, so the snapshot reflects what is actually applied across the engine rather than any single track. Dart models follow the v2 contract per component: requested (caller intent, null when nothing was ever applied) -> software/platform resolved -> active, with effective as the merged verdict. The iOS and Android plugins read factory.audioProcessingState and serialize it over the method channel; Android reaches the factory through the flutter_webrtc getPeerConnectionFactory accessor. --- .../kotlin/io/livekit/plugin/LiveKitPlugin.kt | 57 +++++++ lib/livekit_client.dart | 2 + lib/src/audio/audio_manager.dart | 40 +++++ lib/src/audio/audio_processing_state.dart | 145 ++++++++++++++++++ lib/src/support/native.dart | 19 +++ lib/src/track/local/audio.dart | 10 ++ lib/src/track/options.dart | 8 - shared_swift/LiveKitPlugin.swift | 56 +++++++ 8 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 lib/src/audio/audio_manager.dart create mode 100644 lib/src/audio/audio_processing_state.dart diff --git a/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt b/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt index d3ccd5391..95a242497 100644 --- a/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt +++ b/android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt @@ -30,9 +30,12 @@ import com.cloudwebrtc.webrtc.audio.LocalAudioTrack import io.flutter.plugin.common.BinaryMessenger import org.webrtc.AudioTrack import org.webrtc.audio.AudioProcessingComponentOptions +import org.webrtc.audio.AudioProcessingComponentState +import org.webrtc.audio.AudioProcessingImplementation import org.webrtc.audio.AudioProcessingMode import org.webrtc.audio.AudioProcessingOptions import org.webrtc.audio.AudioProcessingOptionsResult +import org.webrtc.audio.AudioProcessingState /** LiveKitPlugin */ class LiveKitPlugin : FlutterPlugin, MethodCallHandler { @@ -271,6 +274,56 @@ class LiveKitPlugin : FlutterPlugin, MethodCallHandler { AudioProcessingOptionsResult.Code.APPLY_FAILED -> "applyFailed" } + private fun handleGetAudioProcessingState(result: Result) { + val factory = flutterWebRTCPlugin.getPeerConnectionFactory() + if (factory == null) { + result.success(null) + return + } + result.success(audioProcessingStateToMap(factory.audioProcessingState)) + } + + private fun audioProcessingModeString(mode: AudioProcessingMode): String = when (mode) { + AudioProcessingMode.PLATFORM -> "platform" + AudioProcessingMode.SOFTWARE -> "software" + AudioProcessingMode.AUTOMATIC -> "auto" + } + + private fun audioProcessingImplementationString(implementation: AudioProcessingImplementation): String = + when (implementation) { + AudioProcessingImplementation.UNKNOWN -> "unknown" + AudioProcessingImplementation.DISABLED -> "disabled" + AudioProcessingImplementation.SOFTWARE -> "software" + AudioProcessingImplementation.PLATFORM -> "platform" + AudioProcessingImplementation.SOFTWARE_AND_PLATFORM -> "softwareAndPlatform" + } + + private fun requestedToMap(requested: AudioProcessingComponentOptions?): Map? = + requested?.let { + mapOf( + "enabled" to it.isEnabled, + "mode" to audioProcessingModeString(it.mode), + ) + } + + private fun componentToMap(state: AudioProcessingComponentState): Map = mapOf( + "requested" to requestedToMap(state.requested), + "isSoftwareResolved" to state.isSoftwareResolved, + "isSoftwareActive" to state.isSoftwareActive, + "isPlatformAvailable" to state.isPlatformAvailable, + "isPlatformResolved" to state.isPlatformResolved, + "isPlatformActive" to state.isPlatformActive, + "effective" to audioProcessingImplementationString(state.effective), + ) + + private fun audioProcessingStateToMap(state: AudioProcessingState): Map = mapOf( + "hasAudioProcessingModule" to state.hasAudioProcessingModule, + "echoCancellation" to componentToMap(state.echoCancellation), + "noiseSuppression" to componentToMap(state.noiseSuppression), + "autoGainControl" to componentToMap(state.autoGainControl), + "highPassFilter" to componentToMap(state.highPassFilter), + ) + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "startVisualizer" -> { @@ -293,6 +346,10 @@ class LiveKitPlugin : FlutterPlugin, MethodCallHandler { handleSetAudioProcessingOptions(call, result) } + "getAudioProcessingState" -> { + handleGetAudioProcessingState(result) + } + else -> { result.notImplemented() } diff --git a/lib/livekit_client.dart b/lib/livekit_client.dart index cb0408d67..9756d5a07 100644 --- a/lib/livekit_client.dart +++ b/lib/livekit_client.dart @@ -41,12 +41,14 @@ export 'src/agent/room_agent.dart'; export 'src/participant/local.dart'; export 'src/participant/participant.dart'; export 'src/participant/remote.dart' hide ParticipantCreationResult; +export 'src/audio/audio_manager.dart'; export 'src/audio/audio_frame_capture.dart' show AudioFormat, AudioFrame, AudioFrameCallback, AudioRendererOptions; export 'src/preconnect/pre_connect_audio_buffer.dart'; export 'src/publication/local.dart'; export 'src/publication/remote.dart'; export 'src/publication/track_publication.dart'; export 'src/support/platform.dart'; +export 'src/audio/audio_processing_state.dart'; export 'src/track/audio_visualizer.dart'; export 'src/track/local/audio.dart'; export 'src/track/local/local.dart'; diff --git a/lib/src/audio/audio_manager.dart b/lib/src/audio/audio_manager.dart new file mode 100644 index 000000000..3034a3706 --- /dev/null +++ b/lib/src/audio/audio_manager.dart @@ -0,0 +1,40 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../support/native.dart'; +import 'audio_processing_state.dart'; + +/// Controls LiveKit's process-wide platform audio behavior. +/// +/// The platform audio engine and its audio processing module are global to the +/// app process, so engine-scoped audio state lives here rather than on a `Room` +/// or an individual track. +class AudioManager { + AudioManager._(); + + static final AudioManager instance = AudioManager._(); + + /// Diagnostic snapshot of the resolved audio processing state. + /// + /// The audio processing module is owned by the native peer connection factory + /// and shared engine-wide, so this reflects what is actually applied across + /// the engine rather than any single track — use it to verify what a + /// `LocalAudioTrack.setAudioProcessingOptions` request resolved to. Returns + /// `null` when the native side cannot provide it. + Future getAudioProcessingState() async { + final response = await Native.getAudioProcessingState(); + if (response == null) return null; + return AudioProcessingState.fromMap(response); + } +} diff --git a/lib/src/audio/audio_processing_state.dart b/lib/src/audio/audio_processing_state.dart new file mode 100644 index 000000000..a7d21e12c --- /dev/null +++ b/lib/src/audio/audio_processing_state.dart @@ -0,0 +1,145 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../track/options.dart'; + +/// The implementation in effect for an audio processing component. +enum AudioProcessingImplementation { + unknown('unknown'), + disabled('disabled'), + software('software'), + platform('platform'), + softwareAndPlatform('softwareAndPlatform'); + + const AudioProcessingImplementation(this.value); + + final String value; + + static AudioProcessingImplementation fromValue(String? value) => AudioProcessingImplementation.values.firstWhere( + (e) => e.value == value, + orElse: () => AudioProcessingImplementation.unknown, + ); +} + +AudioProcessingMode _modeFromValue(String? value) { + for (final mode in AudioProcessingMode.values) { + if (mode.constraintValue == value) return mode; + } + return AudioProcessingMode.automatic; +} + +/// The caller's request for one audio processing component: enabled flag plus +/// implementation mode. +class AudioProcessingComponentRequest { + const AudioProcessingComponentRequest({ + required this.enabled, + required this.mode, + }); + + factory AudioProcessingComponentRequest.fromMap(Map map) => AudioProcessingComponentRequest( + enabled: (map['enabled'] as bool?) ?? false, + mode: _modeFromValue(map['mode'] as String?), + ); + + final bool enabled; + final AudioProcessingMode mode; +} + +/// Diagnostic state of one audio processing component (echo cancellation, +/// noise suppression, auto gain control or high-pass filter), observed at +/// three stages of one pipeline: requested (caller intent) -> resolved (the +/// engine's per-path decision) -> active (live truth), with [effective] as +/// the merged verdict. +class AudioProcessingComponentState { + const AudioProcessingComponentState({ + this.requested, + required this.isSoftwareResolved, + required this.isSoftwareActive, + required this.isPlatformAvailable, + required this.isPlatformResolved, + required this.isPlatformActive, + required this.effective, + }); + + factory AudioProcessingComponentState.fromMap(Map map) => AudioProcessingComponentState( + requested: map['requested'] is Map + ? AudioProcessingComponentRequest.fromMap(Map.from(map['requested'] as Map)) + : null, + isSoftwareResolved: (map['isSoftwareResolved'] as bool?) ?? false, + isSoftwareActive: (map['isSoftwareActive'] as bool?) ?? false, + isPlatformAvailable: (map['isPlatformAvailable'] as bool?) ?? false, + isPlatformResolved: (map['isPlatformResolved'] as bool?) ?? false, + isPlatformActive: (map['isPlatformActive'] as bool?) ?? false, + effective: AudioProcessingImplementation.fromValue(map['effective'] as String?), + ); + + /// What the caller most recently requested for this component. Null when no + /// audio processing options have ever been applied — "nobody asked". + final AudioProcessingComponentRequest? requested; + + /// Whether the resolver decided the WebRTC software (APM) implementation + /// should run, after weighing the requested mode against platform + /// availability, coupling, and policy. + final bool isSoftwareResolved; + + /// Whether APM's live configuration currently has this component enabled. + final bool isSoftwareActive; + + /// Whether this device/OS offers a built-in implementation at all. + final bool isPlatformAvailable; + + /// Whether the engine asked the OS to run the platform implementation. The + /// OS owns the outcome: it can decline, defer, or couple components. + final bool isPlatformResolved; + + /// Whether the device reports the platform implementation actually running. + final bool isPlatformActive; + + /// The verdict: which implementation is in effect right now. + final AudioProcessingImplementation effective; +} + +/// Diagnostic snapshot of the resolved audio processing state for the shared +/// audio processing module. +/// +/// The module is owned by the native peer connection factory and shared +/// engine-wide, so this reflects what is actually applied (per-component +/// [AudioProcessingComponentState.effective]) versus what was requested — for +/// the whole engine, not a single track. +class AudioProcessingState { + const AudioProcessingState({ + required this.hasAudioProcessingModule, + required this.echoCancellation, + required this.noiseSuppression, + required this.autoGainControl, + required this.highPassFilter, + }); + + factory AudioProcessingState.fromMap(Map map) => AudioProcessingState( + hasAudioProcessingModule: (map['hasAudioProcessingModule'] as bool?) ?? false, + echoCancellation: + AudioProcessingComponentState.fromMap(Map.from(map['echoCancellation'] as Map)), + noiseSuppression: + AudioProcessingComponentState.fromMap(Map.from(map['noiseSuppression'] as Map)), + autoGainControl: + AudioProcessingComponentState.fromMap(Map.from(map['autoGainControl'] as Map)), + highPassFilter: AudioProcessingComponentState.fromMap(Map.from(map['highPassFilter'] as Map)), + ); + + final bool hasAudioProcessingModule; + final AudioProcessingComponentState echoCancellation; + final AudioProcessingComponentState noiseSuppression; + final AudioProcessingComponentState autoGainControl; + final AudioProcessingComponentState highPassFilter; +} diff --git a/lib/src/support/native.dart b/lib/src/support/native.dart index d6fd11c25..9cdfe928a 100644 --- a/lib/src/support/native.dart +++ b/lib/src/support/native.dart @@ -74,6 +74,25 @@ class Native { return {}; } + /// Reads the engine-wide audio processing state from the native peer + /// connection factory. Returns `null` when unavailable (e.g. the factory + /// does not exist yet, or the platform cannot provide it). + @internal + static Future?> getAudioProcessingState() async { + try { + final response = await channel.invokeMethod( + 'getAudioProcessingState', + {}, + ); + if (response is Map) { + return response.map((key, value) => MapEntry(key.toString(), value)); + } + } catch (error) { + logger.warning('getAudioProcessingState did throw $error'); + } + return null; + } + @internal static Future startVisualizer( String trackId, { diff --git a/lib/src/track/local/audio.dart b/lib/src/track/local/audio.dart index 67eab47a8..48468b8cc 100644 --- a/lib/src/track/local/audio.dart +++ b/lib/src/track/local/audio.dart @@ -176,6 +176,16 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi await track.setProcessor(options.processor); } + // Per-component processing modes are not part of standard capture + // constraints; apply them through the native audio processing path. + final processing = options.processing; + if (processing.echoCancellationMode != track_options.AudioProcessingMode.automatic || + processing.noiseSuppressionMode != track_options.AudioProcessingMode.automatic || + processing.autoGainControlMode != track_options.AudioProcessingMode.automatic || + processing.highPassFilterMode != track_options.AudioProcessingMode.automatic) { + await track.setAudioProcessingOptions(processing); + } + return track; } } diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index c31e99513..aa0ba9f62 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -421,10 +421,6 @@ class AudioCaptureOptions extends LocalTrackOptions implements AudioProcessingOp {'autoGainControl': false}, {'voiceIsolation': false}, {'googDAEchoCancellation': false}, - if (!kIsWeb) {'echoCancellationMode': echoCancellationMode.constraintValue}, - if (!kIsWeb) {'noiseSuppressionMode': noiseSuppressionMode.constraintValue}, - if (!kIsWeb) {'autoGainControlMode': autoGainControlMode.constraintValue}, - if (!kIsWeb) {'highPassFilterMode': highPassFilterMode.constraintValue}, ]; } else { /// in we platform it's not possible to provide optional and mandatory parameters. @@ -443,10 +439,6 @@ class AudioCaptureOptions extends LocalTrackOptions implements AudioProcessingOp {'googAutoGainControl': autoGainControl}, {'googHighpassFilter': highPassFilter}, {'googTypingNoiseDetection': typingNoiseDetection}, - if (!kIsWeb) {'echoCancellationMode': echoCancellationMode.constraintValue}, - if (!kIsWeb) {'noiseSuppressionMode': noiseSuppressionMode.constraintValue}, - if (!kIsWeb) {'autoGainControlMode': autoGainControlMode.constraintValue}, - if (!kIsWeb) {'highPassFilterMode': highPassFilterMode.constraintValue}, ]; } } diff --git a/shared_swift/LiveKitPlugin.swift b/shared_swift/LiveKitPlugin.swift index 325087a09..fa2367bbe 100644 --- a/shared_swift/LiveKitPlugin.swift +++ b/shared_swift/LiveKitPlugin.swift @@ -449,6 +449,60 @@ public class LiveKitPlugin: NSObject, FlutterPlugin { } } + public func handleGetAudioProcessingState(result: @escaping FlutterResult) { + guard let factory = FlutterWebRTCPlugin.sharedSingleton()?.peerConnectionFactory else { + result(nil) + return + } + result(LiveKitPlugin.toMap(state: factory.audioProcessingState)) + } + + static func audioProcessingModeString(_ mode: RTCAudioProcessingMode) -> String { + switch mode { + case .platform: return "platform" + case .software: return "software" + default: return "auto" + } + } + + static func audioProcessingImplementationString(_ implementation: RTCAudioProcessingImplementation) -> String { + switch implementation { + case .disabled: return "disabled" + case .software: return "software" + case .platform: return "platform" + case .softwareAndPlatform: return "softwareAndPlatform" + default: return "unknown" + } + } + + static func toMap(component state: RTCAudioProcessingComponentState) -> [String: Any] { + var map: [String: Any] = [ + "isSoftwareResolved": state.isSoftwareResolved, + "isSoftwareActive": state.isSoftwareActive, + "isPlatformAvailable": state.isPlatformAvailable, + "isPlatformResolved": state.isPlatformResolved, + "isPlatformActive": state.isPlatformActive, + "effective": audioProcessingImplementationString(state.effective), + ] + if let requested = state.requested { + map["requested"] = [ + "enabled": requested.isEnabled, + "mode": audioProcessingModeString(requested.mode), + ] + } + return map + } + + static func toMap(state: RTCAudioProcessingState) -> [String: Any] { + [ + "hasAudioProcessingModule": state.hasAudioProcessingModule, + "echoCancellation": toMap(component: state.echoCancellation), + "noiseSuppression": toMap(component: state.noiseSuppression), + "autoGainControl": toMap(component: state.autoGainControl), + "highPassFilter": toMap(component: state.highPassFilter), + ] + } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let args = call.arguments as? [String: Any?] else { print("[LiveKit] arguments must be a dictionary") @@ -469,6 +523,8 @@ public class LiveKitPlugin: NSObject, FlutterPlugin { handleStopAudioRenderer(args: args, result: result) case "setAudioProcessingOptions": handleSetAudioProcessingOptions(args: args, result: result) + case "getAudioProcessingState": + handleGetAudioProcessingState(result: result) case "osVersionString": result(LiveKitPlugin.osVersionString()) #if os(iOS) From 01354e6cc24348e4ebdbaae54d888ca882eadf66 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:54:52 +0800 Subject: [PATCH 6/7] chore: add changeset for runtime audio processing options --- .changes/runtime-audio-options | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/runtime-audio-options diff --git a/.changes/runtime-audio-options b/.changes/runtime-audio-options new file mode 100644 index 000000000..7cb3ff1ec --- /dev/null +++ b/.changes/runtime-audio-options @@ -0,0 +1 @@ +minor type="added" "Runtime audio processing options for local audio tracks and engine-wide audio processing state read-back on AudioManager" From 1d2a1c4faa0a9ec3f6cc168a95559a4a821ca3f1 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:48:32 +0800 Subject: [PATCH 7/7] Update pubspec.lock --- pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 39350a0a9..3aefe0abc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -284,10 +284,10 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "8b220dc006c4891266735e516f7679bd08b7caaf7c36b1a93fb9357cec555f92" + sha256: d8c89028d29e5693742190285b2e3c8a117531b0960ae0693d84273a53968d28 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" frontend_server_client: dependency: transitive description: