Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/runtime-audio-options
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minor type="added" "Runtime audio processing options for local audio tracks and engine-wide audio processing state read-back on AudioManager"
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand Down
122 changes: 122 additions & 0 deletions android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ 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.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 {
Expand Down Expand Up @@ -210,6 +217,113 @@ class LiveKitPlugin : FlutterPlugin, MethodCallHandler {
result.success(true)
}

private fun handleSetAudioProcessingOptions(call: MethodCall, result: Result) {
val trackId = call.argument<String>("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<Boolean>("echoCancellation") ?: true,
audioProcessingMode(call.argument<String>("echoCancellationMode")),
),
AudioProcessingComponentOptions(
call.argument<Boolean>("noiseSuppression") ?: true,
audioProcessingMode(call.argument<String>("noiseSuppressionMode")),
),
AudioProcessingComponentOptions(
call.argument<Boolean>("autoGainControl") ?: true,
audioProcessingMode(call.argument<String>("autoGainControlMode")),
),
AudioProcessingComponentOptions(
call.argument<Boolean>("highPassFilter") ?: false,
audioProcessingMode(call.argument<String>("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"
}

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<String, Any?>? =
requested?.let {
mapOf(
"enabled" to it.isEnabled,
"mode" to audioProcessingModeString(it.mode),
)
}

private fun componentToMap(state: AudioProcessingComponentState): Map<String, Any?> = 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<String, Any?> = 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" -> {
Expand All @@ -228,6 +342,14 @@ class LiveKitPlugin : FlutterPlugin, MethodCallHandler {
handleStopAudioRenderer(call, result)
}

"setAudioProcessingOptions" -> {
handleSetAudioProcessingOptions(call, result)
}

"getAudioProcessingState" -> {
handleGetAudioProcessingState(result)
}

else -> {
result.notImplemented()
}
Expand Down
2 changes: 1 addition & 1 deletion ios/livekit_client.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions lib/livekit_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
40 changes: 40 additions & 0 deletions lib/src/audio/audio_manager.dart
Original file line number Diff line number Diff line change
@@ -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<AudioProcessingState?> getAudioProcessingState() async {
final response = await Native.getAudioProcessingState();
if (response == null) return null;
return AudioProcessingState.fromMap(response);
}
}
145 changes: 145 additions & 0 deletions lib/src/audio/audio_processing_state.dart
Original file line number Diff line number Diff line change
@@ -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<dynamic, dynamic> 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<dynamic, dynamic> map) => AudioProcessingComponentState(
requested: map['requested'] is Map
? AudioProcessingComponentRequest.fromMap(Map<dynamic, dynamic>.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<dynamic, dynamic> map) => AudioProcessingState(
hasAudioProcessingModule: (map['hasAudioProcessingModule'] as bool?) ?? false,
echoCancellation:
AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['echoCancellation'] as Map)),
noiseSuppression:
AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['noiseSuppression'] as Map)),
autoGainControl:
AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['autoGainControl'] as Map)),
highPassFilter: AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['highPassFilter'] as Map)),
);

final bool hasAudioProcessingModule;
final AudioProcessingComponentState echoCancellation;
final AudioProcessingComponentState noiseSuppression;
final AudioProcessingComponentState autoGainControl;
final AudioProcessingComponentState highPassFilter;
}
Loading
Loading