t
Hi
t
Thanks for reaching out, Piyush. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Hi — thanks for reaching out! How can I help you today? A couple quick prompts to get us started: • Are you setting up LiveKit for a project or troubleshooting an existing room/connection? • If troubleshooting, can you share the project ID, room ID, and a brief description of the issue (error messages, browser/OS, steps to reproduce)? Whichever you prefer, tell me a bit more and I’ll guide you through the next steps.
t
I’m currently working on a LiveKit-based real-time audio communication app that is live and published on the Google Play Store as part of a community project. We’re facing persistent audio distortion issues that are impacting call quality in production. We’ve reviewed the documentation and tried basic troubleshooting, but the issue still persists. We would really appreciate guidance on identifying the root cause.
t
Thanks — I hear you: audio distortion in production is urgent and frustrating, and you want help finding the root cause. I can guide you through focused diagnostics and likely fixes, but I need a few key details to narrow down causes quickly. Below are targeted questions and quick checks that will help us isolate whether this is audio processing, codec/network, device hardware, or routing related. Please reply with as many answers as you can. 1) Where and when the distortion happens • Does the distortion occur on the sender, the receiver, or both? • Does it start immediately when a call connects, or after some time (e.g., after network changes, after joining multiple participants)? • Is it reproducible for all users or only some (specific geographic regions, carriers, Wi‑Fi vs cellular)? 2) Devices, OS, and app build • Which Android devices and OS versions are mostly affected? • Are affected users on the Play Store build or an internal/test build? If both, are versions identical? • Any patterns by headset vs speakerphone, Bluetooth vs wired? 3) Audio pipeline and SDK settings • Are you using any audio processing features (for example, OS-level processing, LiveKit agent noise cancellation like `BVC()`/`BVCTelephony()`, or third‑party filters)? • Have you set
bypassVoiceProcessing
in the SDK? If so, is it enabled or disabled? • Which encoder/codec configuration are you using (if custom)? Are you forcing Opus parameters? 4) Call topology and participants • Typical call size (1:1, small group, large rooms)? • Do you use Agents/AI in calls? If yes, which noise cancellation model is configured? 5) Logs and metrics (highly helpful) • Can you share relevant SDK logs around the time distortion occurs (RtcError, warnings about audio, reconnections, track state changes)? If you can’t paste logs, summary of repeated warnings is useful. • Do you see spikes/drops in connection quality (uplink/downlink scores) when distortion happens? Any jitter/packet loss or bitrate drops reported? 6) Quick reproduction tests I recommend trying (these will quickly indicate the likely layer) • Reproduce with bypassed processing: enable
bypassVoiceProcessing
(or disable if already enabled) to see if system audio processing is causing artifacts. • Test with a simple reference device/setup: reproduce on a known-good device, on stable Wi‑Fi, with no Bluetooth/headset, 1:1 call—does distortion persist? • Disable any extra noise cancellation chains: if you use Agents + device processing + app filters, disable extras and test with a single cancellation model (for Agents: use
BVC()
or
BVCTelephony()
as recommended). • Record a raw sample: ask an affected user to record local audio (before sending) and the received audio. If local recording is clean but received is distorted, the problem is network/codec/processing on the server or receiver. If local recording is distorted, it’s device/processing on the sender. What I can do next once you share details • If you provide SDK logs, room ID, and timestamps I can help interpret errors and spot network/codec issues. • If you share your audio-init and publish code snippets (Android LiveKit client init, track publish, any noise-cancel config), I’ll review for misconfiguration (for example multiple processing layers or incorrect flags). • I can outline specific step-by-step tests to isolate sender vs network vs receiver causes and follow-up fixes we can try. If you prefer, start by telling me: • One example incident: device model + OS, room ID (or timestamp), whether sender/receiver noticed distortion, and any SDK warnings in logs around that time. Once I have that, I’ll give a prioritized diagnostic plan and likely fixes. Sources: How is Connection Quality determined? | LiveKit | client-sdk-android | livekit/livekit_composite | DeepWiki | client-sdk-flutter | livekit/livekit_composite | DeepWiki | Choosing the right noise cancellation for your agent | LiveKit
t
1.) distortion happens on the sender and receiver, both sides it starts after joining multiple participants I am not sure about reproducing it but it happens when multiple participants joins at the same time (and by multiple participants I mean 6 to 7 users) 2.) I have an android and it happens in locally testing and production app both There is no pattern 4.) small groups - 7 to 10 users when joined rest I am not sure what it has or what problems
t
Thanks — this is helpful. I hear that distortion appears on both sender and receiver, and it reliably shows up when many participants (around 6–7) join at roughly the same time in small group calls. That points us toward issues that appear under load: audio mixing/CPU constraints, client-side audio processing contention, codec/bitrate changes, or network/packet handling when concurrent joins occur. Next steps — focused tests and information that will quickly narrow the root cause Please run these tests and send back results/logs you can capture. Each test targets a specific potential cause so we can isolate the layer that’s failing. 1) Reproduce with a minimal controlled test • Create a 1–1 baseline test (two devices, same network) and confirm audio is clean. • Then reproduce the 7-user scenario on the same network and note when distortion begins (immediate vs after simultaneous joins). Why: confirms whether the problem only appears under concurrent join load. 2) Check CPU / memory and thread contention on sender devices • While reproducing, monitor the Android device CPU and memory (Android Studio Profiler or simple adb) on devices that are joining. Note any CPU spikes or main-thread stalls when the 6th–7th user joins. Why: multiple local encoders + mixing + noise cancellation can saturate CPU and introduce distortion on both send & receive. 3) Test bypassing device processing • Toggle
bypassVoiceProcessing
(if you haven’t tried already): run one test with it enabled and one with it disabled. Why: system-level voice processing + app-level processing can interact badly under load and create artifacts. 4) Simplify audio processing chain • Temporarily disable any extra noise cancellation, echo cancellation, or third-party audio filters. If you use LiveKit Agents/noise models, disable them for the test. Why: multiple stages of noise cancellation (device + app + AI) can produce distortion when the processing pipeline is stressed. 5) Try lower codec bitrate / single-stream publish • Publish with a lower or fixed Opus bitrate and/or single audio stream (no simulcast-like features). If you currently use a custom Opus config, try the SDK defaults. Why: bitrate spikes and encoder reconfiguration during multiple simultaneous publishes can cause artifacts. 6) Network behavior and packet statistics • Collect connection stats in the SDK during a reproduce (uplink/downlink bitrate, packetLoss, jitter). Note any sudden packet loss or bitrate drops when distortion begins. Why: simultaneous joins can cause transient congestion on the SFU or client uplink, producing distorted audio on both ends. 7) Capture recordings to isolate sender vs network • On an affected sender, save a local raw audio recording before sending. Also save the received audio on a receiving client. • Compare: if local recording is clean but received is distorted, the issue is network/SFU/receiver processing. If local is distorted as well, it’s device or local processing. Why: this isolates the origin of distortion. Information to share back (these items let me give precise guidance) • SDK version, Android OS version(s), and example device models that reproduce. • A short code snippet showing how you initialize audio, publish tracks, and any audio-related flags (e.g.,
bypassVoiceProcessing
, noise-cancel model). • Connection stats (u/d bitrate, packet loss, jitter) around the time distortion happens, or screenshots of profiler CPU spikes. • One sample log excerpt (timestamped) from SDK logs showing warnings/errors during the incident (if available). • Room ID and approximate timestamps of an incident (so I can correlate logs if you paste them). Immediate mitigations to try now • Limit concurrent simultaneous join spikes: if your app does a lot at join-time, stagger joins by a few hundred ms client-side to reduce burst load on clients and SFU. • Reduce client-side processing: temporarily disable extra noise cancellation and use a single, recommended cancellation model only. • Lower encoder bitrate during high-concurrency periods to reduce encoder load and SFU forwarding bitrate. What I can do once you reply • Review logs/connection stats and code snippets to identify misconfiguration (e.g., multiple processing layers, wrong flags). • Recommend concrete config values (Opus bitrate, recommended noise model) and step-by-step changes to test. • Help interpret SDK connection stats and profiler outputs to pinpoint CPU vs network vs SFU issues. If you want, start by sharing: • SDK version, one device model and Android version that reproduces, and a short code snippet showing audio initialization and publish. I’ll review for likely misconfigurations and suggest the next exact test to run.
t
port: 7880 room: departure_timeout: 5 auto_create: true empty_timeout: 5 max_participants: 50 rtc: tcp_port: 7881 port_range_start: 40000 port_range_end: 50000 use_external_ip: true node_ip: 34.131.236.43 packet_buffer_size: 1000 packet_buffer_size_audio: 1000 congestion_control: enabled: true pli_throttle: low_quality: 500ms mid_quality: 1s high_quality: 1s keys: xxxxx:xxxxxx turn: enabled: true domain: livekit.aardosolutions.com cert_file: /etc/letsencrypt/live/livekit.aardosolutions.com-0001/fullchain.pem key_file: /etc/letsencrypt/live/livekit.aardosolutions.com-0001/privkey.pem tls_port: 5349 udp_port: 3478 relay_range_start: 50001 relay_range_end: 60000 external_tls: true webhook: api_key: lk_dev_key urls: - https://token.aardosolutions.com/livekit/webhook logging: level: info
t
Thanks — this is very helpful. I can see your server-side LiveKit config (ports, TURN, congestion control, packet buffer sizes, PLI throttles). You’ve confirmed distortion appears on both sender and receiver when ~6–7 participants join at roughly the same time. That strongly suggests the issue happens under concurrent join/load and could be caused by one or more of: client CPU/mixing/processing saturation, encoder bitrate/reconfiguration, SFU buffering/congestion handling, or transient network/packet loss. Next I’ll give a prioritized diagnostic plan you can run (server- and client-focused), what to collect, and a few immediate mitigations to try now. What to test next (priority order) 1) Reproduce with logging and connection stats • Cause targeted: network/SFU vs client. • Action: Reproduce the 6–7 simultaneous join scenario while collecting SDK connection stats on every client. Capture these at 1–2s intervals for the period around the join spike: uplink/downlink bitrate, packetLoss, jitter, rtt, encoder bitrate, and audio track send/recv state. • What to look for: sudden packet loss or bitrate drops at the same time distortion starts; large jitter spikes; repeated reconnections. 2) Capture local vs received audio samples • Cause targeted: sender audio vs network/SFU/receiver processing. • Action: On one sender, record local raw audio (before publishing). On one receiver, record the received audio. Compare: if local is clean but received is distorted, problem is SFU/network/receiver processing. If local is distorted, problem is sender device or local processing. • Format: short 10–30s clips around the incident. 3) Monitor device CPU and thread load during joins • Cause targeted: client-side CPU / mixing / processing overload. • Action: Use Android Studio Profiler or adb dumpsys to sample CPU usage, thread count, and main-thread stalls on clients joining. Note spikes when the 6th–7th participant joins. • What to look for: CPU saturation, GC spikes, long main-thread blocking. If present, audio artifacts often follow. 4) Try bypassing device processing and simplifying pipeline • Cause targeted: conflicting audio processing (multiple cancellation stages). • Action: Temporarily enable
bypassVoiceProcessing
(or disable if you already enabled it previously) on the Android SDK and remove any extra in-app noise/echo processing. If you use Agents/noise models, disable them for the test. • What to look for: If distortion stops, audio processing chain interaction was the cause. 5) Test with lowered/fixed Opus bitrate and single stream • Cause targeted: encoder reconfiguration / bitrate spikes under join load. • Action: Force a conservative Opus bitrate (e.g., 24–32 kbps) and publish only a single audio stream. Use the SDK defaults if you used custom settings. • What to look for: If distortion improves, encoder bitrate/reconfig or SFU bandwidth handling under burst join is implicated. 6) Check SFU/server metrics and packet buffer behavior • Cause targeted: SFU-side buffering or resource limits. • Action: Inspect LiveKit server logs and metrics (CPU, memory, network) during an incident. Pay attention to packet buffer utilization and any warnings around packet drops or rate limiting. • What to look for: server CPU spikes, high packet buffer occupancy, or logs indicating packet drops. Your
packet_buffer_size_audio: 1000
is generous; check whether buffer churn occurs during join spikes. 7) Stagger client joins as a practical mitigation • Cause targeted: burst load on clients and SFU. • Action: Implement a small randomized join delay (100–500 ms) when many users join the same room concurrently. • Why: Reduces simultaneous encoding/publishing bursts to both clients and SFU and often eliminates transient artifacts quickly. Immediate mitigations you can try now • Stagger joins client-side by 100–500 ms. • Temporarily disable extra noise cancellation and use only one cancellation approach. • Lower the audio bitrate to a conservative fixed value during high concurrency. • Limit per-client CPU load: avoid heavy UI work on join, perform heavy tasks off the audio threads. Information I need from you to pinpoint root cause • Android SDK version you’re using and exact LiveKit client version. • Example affected Android device model(s) and OS versions. • A short code snippet showing how you initialize audio, set
bypassVoiceProcessing
(if present), and publish the audio track. • Collected SDK connection stats around an incident (1–2s samples for ~30s spanning the join). • One or two short log excerpts from the server and client around a reproduce (timestamps help). • If possible, the local and received audio clips for one reproduce. Why each item matters • SDK version can reveal known bugs or behavior changes. • Device/OS and profiler info will confirm CPU saturation or audio-route problems. • Connection stats show whether distortion correlates with packet loss/jitter/bitrate drops. • Audio clips immediately show whether distortion originates before or after the network. If you want, start by pasting: • SDK version, one sample device + OS, and the audio initialization/publish code snippet. I’ll review that for misconfigurations (multiple processing layers, wrong flags) and give the exact small code changes to test next. I’m ready to review your logs, stats, code, or audio samples whenever you paste them, and I’ll recommend the next exact action based on what they show.
t
Future<void> setMicEnabled(bool enabled) async { if (_room == null) return; final trackPub = _room!.localParticipant?.audioTrackPublications.firstOrNull; final track = trackPub?.track; if (enabled) { // UNMUTE if (track != null) { await track.unmute(); } else { // Fallback: If track missing, publish it debugPrint('⚠️ [LiveKitEngine] Track missing, republishing...'); await _room!.localParticipant?.setMicrophoneEnabled(true); } } else { // MUTE if (track != null) { await track.mute(); } // DO NOT unpublish (setMicrophoneEnabled(false)) to avoid negotiation delay } _isMicEnabled = enabled; notifyListeners(); } // ------------------------------------------------------------------ // DEAFEN (subscribe/unsubscribe remote audio) // ------------------------------------------------------------------ void setDeafened(bool deafened) { _isDeafened = deafened; _updateDeafenState(); notifyListeners(); } void _updateDeafenState() { if (_room == null) return; final shouldHear = !_isDeafened; for (var p in _room!.remoteParticipants.values) { for (var pub in p.audioTrackPublications) { if (shouldHear) { if (!pub.subscribed) pub.subscribe(); } else { if (pub.subscribed) pub.unsubscribe(); } } } } // ------------------------------------------------------------------ // METADATA (Sync state to others) // ------------------------------------------------------------------ Future<void> updateMetadata(Map<String, dynamic> metadata) async { if (_room == null || _room!.localParticipant == null) return; try { final jsonStr = jsonEncode(metadata); _room!.localParticipant!.setMetadata(jsonStr); } catch (e) { debugPrint('⚠️ [LiveKitEngine] Failed to update metadata: $e'); } } // ------------------------------------------------------------------ // PRIVATE — AudioSession Configuration // ------------------------------------------------------------------ Future<void> _configureAudioSession() async { try { final session = await AudioSession.instance; await session.configure( AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.allowBluetoothA2dp | AVAudioSessionCategoryOptions.defaultToSpeaker, avAudioSessionMode: AVAudioSessionMode.voiceChat, avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, androidAudioAttributes: AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, usage: AndroidAudioUsage.voiceCommunication, ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidWillPauseWhenDucked: false, ), ); } catch (e) { debugPrint('⚠️ [LiveKitEngine] AudioSession config failed: $e'); } } // ------------------------------------------------------------------ // PRIVATE — Room Event Listeners // ------------------------------------------------------------------ Timer? _notifyDebounceTimer; void _debounceNotify() { if (_notifyDebounceTimer?.isActive ?? false) return; _notifyDebounceTimer = Timer(const Duration(milliseconds: 500), () { notifyListeners(); }); } void _setupListeners() { _listener! ..on<RoomConnectedEvent>((_) { notifyListeners(); }) ..on<RoomDisconnectedEvent>((event) { // Notify MeetingController — it decides whether to reconnect. _status = ServiceStatus.disconnected; notifyListeners(); onUnexpectedDisconnect?.call(); }) ..on<ParticipantConnectedEvent>((_) { if (_room == null) return; _participants = _room!.remoteParticipants.values.toList(); _debounceNotify(); }) ..on<ParticipantDisconnectedEvent>((_) { if (_room == null) return; _participants = _room!.remoteParticipants.values.toList(); _debounceNotify(); }) ..on<LocalTrackPublishedEvent>((_) { _isMicEnabled = _room?.localParticipant?.isMicrophoneEnabled() ?? false; notifyListeners(); }) ..on<LocalTrackUnpublishedEvent>((_) { _isMicEnabled = _room?.localParticipant?.isMicrophoneEnabled() ?? false; notifyListeners(); }) ..on<TrackMutedEvent>((e) { if (e.participant == _room?.localParticipant) { _isMicEnabled = false; notifyListeners(); } else { _debounceNotify(); } }) ..on<TrackUnmutedEvent>((e) { if (e.participant == _room?.localParticipant) { _isMicEnabled = true; notifyListeners(); } else { _debounceNotify(); } }) ..on<TrackSubscribedEvent>((e) { _updateDeafenState(); _debounceNotify(); }) ..on<ActiveSpeakersChangedEvent>((e) { _debounceNotify(); }) ..on<RoomReconnectingEvent>((_) { debugPrint('⚠️ [LiveKitEngine] Reconnecting...'); _status = ServiceStatus.connecting; notifyListeners(); onReconnecting?.call(); }) ..on<RoomReconnectedEvent>((_) { debugPrint('✅ [LiveKitEngine] Reconnected'); _status = ServiceStatus.connected; notifyListeners(); onReconnected?.call(); }) ..on<ParticipantConnectionQualityUpdatedEvent>((e) { if (e.connectionQuality == ConnectionQuality.poor) { debugPrint( '⚠️ [LiveKitEngine] Poor connection: ${e.participant.identity}', ); } }) ..on<RoomEvent>((e) { // 🟢 Fallback for any metadata event (ParticipantMetadataChangedEvent, etc.) if (e.runtimeType.toString().contains('Metadata')) { notifyListeners(); } }); }
t
Thanks — this is very helpful. I’ve reviewed your server config and your client code. You’ve shown that distortion appears for both senders and receivers when ~6–7 participants join at once, and your code shows how you publish/unpublish, deafen, and configure audio session. That context strongly points to a load-related problem (client CPU/mixing, encoder reconfig, SFU buffering or transient packet loss) but I still need a few targeted pieces of info and logs to pinpoint the root cause. Summary of likely causes given what you’ve shared • Client-side CPU / audio processing overload when many publishers appear simultaneously (encoding + noise cancellation + mixing). • Conflicting or heavy audio processing chains on devices (system + app + any AI/agent models). • SFU/server transient congestion or buffer churn when many tracks publish at once. • Encoder bitrate reconfiguration or packet loss during the simultaneous-join burst. What I need next (please provide as many as you can) • LiveKit Android SDK version and the client library version you’re using. • One or two example affected Android device models and OS versions (e.g., Pixel 5 / Android 12). • Whether you use any LiveKit Agent noise-cancellation models or other third‑party audio processing in the app. • Whether you set
bypassVoiceProcessing
anywhere (true/false) in your Android initialization. • A short copy-paste of how you create and publish the local audio track (the publish call and any audioOptions/opus settings). • SDK connection stats from a reproduce (1–2s interval samples for ~30s around the join): uplink/downlink bitrate, packetLoss, jitter, rtt, and audio track send/recv state. • A short client SDK log excerpt around an incident (timestamps help), and any server LiveKit logs for the same timeframe if possible. • Optional but very helpful: a 10–30s raw local recording from a sender and the corresponding received recording from a receiver for one reproduce. Priority tests to run (ordered — each isolates one root area) 1) Single-device baseline then controlled 7-user burst • Confirm 1:1 is clean, then reproduce the 7-user simultaneous join on a single Wi‑Fi network. Note exact time when distortion starts. 2) Capture local vs received audio • Record local microphone output on a sender before send, and record received audio on a receiver. If local is clean but received is distorted, the problem is network/SFU/receiver; if local is distorted, it’s the sender or its local processing. 3) Watch CPU/memory during join bursts • Use Android Studio Profiler or adb to capture CPU/memory while 6–7 users join. Look for CPU spikes, GC pauses, or main-thread stalls at the moment distortion starts. 4) Toggle voice processing • Run two tests: one with
bypassVoiceProcessing = true
and one with
false
(or the opposite if you already use one). Also temporarily disable any other app-level noise cancellation or filters. This shows whether audio processing chains cause artifacts under load. 5) Lower and fix Opus bitrate / single stream publish • Force a conservative Opus bitrate (e.g., 24–32 kbps) and publish one audio stream. If distortion improves, encoder bitrate or SFU forwarding during bursts is implicated. 6) Collect connection stats from all clients during a reproduce • Look for synchronized packetLoss, jitter spikes, or bitrate drops right when the join burst occurs. 7) Stagger join times as a practical immediate mitigation • Add a 100–500 ms randomized delay on join for clients that join en masse. This often removes burst-related artifacts quickly while you debug. Small code/config checks from what you sent • Your microphone toggling and subscribe/unsubscribe logic looks fine; you avoid unpublishing which reduces negotiation overhead (good). • Your AudioSession configuration uses voice chat attributes which is correct; confirm Android audio routing behavior matches expected (Bluetooth vs speakerphone differences). • On server side you have congestion_control enabled, TURN enabled, and a wide UDP port range. Check LiveKit server CPU/network during bursts for spikes and check whether your TURN server becomes overloaded when many clients connect simultaneously. If you want, start by pasting: • Your Android LiveKit client initialization snippet (where you set audio options and publish the microphone) and the SDK version. • One short SDK log excerpt (client) and the corresponding server log timestamps for the same event. What I’ll do once you share those • Review the client publish/init code for misconfiguration (multiple processing layers, opus settings, bypass flags). • Interpret the connection stats and logs to tell if the issue is CPU, network/SFU, or audio-processing. • Recommend exact config changes (Opus bitrate, buffer sizes, process flags), and if needed a plan to profile server/client to fix the root cause. If you prefer, run the local vs received recording test first — that single test will most quickly tell us whether the problem originates on the sender or in the network/SFU/receiver. Tell me the results and paste any logs you can, and I’ll give the next exact steps.
t
Future<void> connect(String wsUrl, String token) async { if (_status == ServiceStatus.connecting || _status == ServiceStatus.connected) { return; } _status = ServiceStatus.connecting; _errorMsg = ''; notifyListeners(); try { final roomOptions = RoomOptions( adaptiveStream: false, dynacast: false, defaultAudioPublishOptions: const AudioPublishOptions( name: 'audio', dtx: true, red: true, encoding: AudioEncoding(maxBitrate: 48000), ), defaultAudioCaptureOptions: const AudioCaptureOptions( stopAudioCaptureOnMute: false, noiseSuppression: false, echoCancellation: true, autoGainControl: true, ), defaultVideoPublishOptions: const VideoPublishOptions(simulcast: false), ); _room = Room(roomOptions: roomOptions); _listener = _room!.createListener(); _setupListeners(); // Configure AudioSession ONCE — voiceCommunication mode. // This is the ONLY AudioSession.configure() call in the entire app. await _configureAudioSession(); await _room! .connect( wsUrl, token, connectOptions: const ConnectOptions( autoSubscribe: true, timeouts: Timeouts( connection: Duration(seconds: 15), debounce: Duration(seconds: 2), publish: Duration(seconds: 10), subscribe: Duration(seconds: 15), peerConnection: Duration(seconds: 20), iceRestart: Duration(seconds: 20), ), ), fastConnectOptions: FastConnectOptions( microphone: const TrackOption<bool, LocalAudioTrack>( enabled: true, // 🟢 Publish immediately ), ), ) .timeout( const Duration(seconds: 20), onTimeout: () { throw TimeoutException('WebSocket Connection Timeout'); }, ); // 🟢 Mute immediately after connect (Soft Mute) final trackPub = _room!.localParticipant?.audioTrackPublications.firstOrNull; if (trackPub != null && trackPub.track != null) { await trackPub.track!.mute(); } else { // Fallback: Ensure published but muted await _room!.localParticipant?.setMicrophoneEnabled(true); await _room!.localParticipant?.audioTrackPublications.firstOrNull?.track ?.mute(); } _isMicEnabled = false; _isDeafened = false; _participants = _room!.remoteParticipants.values.toList(); _status = ServiceStatus.connected; notifyListeners(); debugPrint( '✅ [LiveKitEngine] Room connected. Participants: ${_participants.length}', ); } catch (e) { _status = ServiceStatus.error; _errorMsg = _classifyError(e); notifyListeners(); rethrow; } }
t
Thanks — this is great detail. I see your Room connect code and audio publish defaults (no dynacast/adaptive, audio encoding maxBitrate 48kbps, DTX/RED enabled, echo cancellation and auto gain on, noiseSuppression false). That helps narrow things down. Key observations from your config and code • You publish audio immediately on connect (fastConnect microphone enabled) and then mute — this avoids negotiation delays but still creates an immediate publish load when many users join simultaneously. •
adaptiveStream
and
dynacast
are disabled, and
simulcast
for video is off — so the SFU will handle full audio streams for each participant. • Audio encoding uses a maxBitrate of 48,000 (48 kbps) which is reasonable, but simultaneous immediate publishes from many clients could cause encoder and SFU bitrate/congestion spikes. •
noiseSuppression
is false, but
echoCancellation
and
autoGainControl
are enabled; system-level processing plus SDK processing could interact under load. • Server-side packet buffer sizes are large (1000), congestion control is enabled, and TURN is on — good, but bursts of simultaneous joins can still strain client CPU or cause transient SFU churn. Recommended next steps — quick tests to run now (ordered by what will likely isolate cause fastest) 1) Reproduce with delayed publish (fast check) • Change the client to delay publishing the microphone for ~300–700 ms after connect (or stagger joins on clients) and reproduce the 6–7 simultaneous join scenario. • Why: your current fastConnect + immediate publish causes a burst of encoder startup work on many clients and many concurrent publishes to the SFU. Staggering often removes artifacts quickly and confirms a burst-load cause. 2) Run the local vs received audio test • Record a short local raw audio clip on a sender before sending and record the received audio on a receiver during the same incident. • Outcome: If local is clean but received is distorted, focus on SFU/network/receiver. If local is distorted, focus on sender device or local processing/encoding. 3) Toggle audio processing flags • Test A: set
defaultAudioCaptureOptions.noiseSuppression = true
and
bypassVoiceProcessing = true
(if available in your Android client) — or run the inverse (enable bypass if currently not set). • Test B: temporarily disable
echoCancellation
and
autoGainControl
to see if artifacts change. • Why: audio processing chains can interact poorly under CPU pressure and produce distortion. 4) Lower/fix Opus bitrate for a reproduce • Temporarily lower
AudioEncoding(maxBitrate: 24000)
and test. Also try forcing a fixed bitrate (if supported). • Why: reducing encoder load and SFU bandwidth can eliminate distortion caused by encoder reconfig or congestion bursts. 5) Capture SDK connection stats and client CPU during reproduce • Collect connection stats at 1–2s intervals for ~30s around the join: uplink/downlink bitrate, packetLoss, jitter, rtt, audio track send/recv state. • Simultaneously capture Android CPU/threads/GC (Android Studio Profiler or adb) on at least one joining client. • What to look for: ◦ Synchronous packet loss/jitter/bitrate drops across clients at the join spike → SFU/network issue. ◦ CPU spikes/GC pauses on clients at the join spike → client-side encoding/mixing overload. 6) Server logs & metrics during reproduce • Check LiveKit server CPU/memory and network usage when the join burst happens. Look for logs indicating packet drops, rate limiting, or high buffer occupancy despite packet_buffer_size_audio being large. Immediate mitigations to try now • Stagger clients’ publishes/joins by 100–700 ms. • Temporarily lower
maxBitrate
to 24 kbps for testing. • Temporarily disable client-side echo cancellation or auto gain control to see if artifacts change. • If you have control over client behavior at join, avoid heavy UI or background work when publishing starts. What I need from you to diagnose further • Android LiveKit SDK version and the exact client library version (package + version). • One or two example device models (and Android OS versions) where this reproduces. • Whether you use any LiveKit Agents or other server-side audio processing / AI noise cancellation. • Whether you use
bypassVoiceProcessing
on Android (and if so, its current value). • A short snippet showing how you call publish/create local audio track on Android (if different from the Flutter wrapper you shared). • Connection stats (1–2s samples) and one short client SDK log excerpt around a reproduce attempt (timestamps help). • Optional but highly useful: local vs received audio samples for one reproduce. If you want to start with one thing that will give the fastest answer, do this: • Stagger the immediate publishes by ~500 ms (client-side) or add a 500 ms delay before the fastConnect microphone publish, then reproduce the 6–7 join scenario and report whether distortion persists. Paste the SDK version, one device model + OS, and the results of the staggered-publish test or the local vs received recording test, and I’ll analyze the logs/stats you provide and give exact config changes and code suggestions to eliminate the distortion.
t
so basically I have a android device running on android 16, livekit_client: ^2.6.2 flutter_webrtc: 1.3.0 # Compatible with livekit_client I dont use use any LiveKit Agents or other server-side audio processing / AI noise cancellation. not using any
bypassVoiceProcessing
2026-02-15T155700.305Z INFO livekit service/roommanager.go:413 starting RTC session {"room": "StressRoom", "roomID": "RM_XPm6VRNgrgig", "participant": "uqaot_90", "pID": "PA_Noqa7fDA26gi", "remote": false, "room": "StressRoom", "nodeID": "ND_8SK7WcmxYRHA", "numParticipants": 50, "participantInit": {"Identity": "uqaot_90", "Reconnect": false, "ReconnectReason": "RR_UNKNOWN", "AutoSubscribe": false, "AutoSubscribeDataTrack": "not-set", "Client": {"sdk": "GO", "version": "2.13.3", "protocol": 16, "os": "linux"}, "Grants": {"Identity": "uqaot_90", "Kind": "standard", "KindDetails": [], "Video": {"RoomCreate": false, "RoomList": false, "RoomRecord": false, "RoomAdmin": false, "RoomJoin": true, "Room": "StressRoom", "CanPublish": "not-set", "CanSubscribe": "not-set", "CanPublishData": "not-set", "CanPublishSources": [], "CanUpdateOwnMetadata": "not-set", "IngressAdmin": false, "Hidden": false, "Recorder": false, "Agent": false, "CanSubscribeMetrics": "not-set", "DestinationRoom": ""}, "SIP": {}, "Agent": {}, "Inference": {}, "Observability": {}, "RoomConfig": {}, "RoomPreset": ""}, "Region": "", "AdaptiveStream": false, "ID": "", "SubscriberAllowPause": "not-set", "DisableICELite": false, "CreateRoom": {"name": "StressRoom"}, "AddTrackRequests": [], "PublisherOffer": {}, "SyncState": {}, "UseSinglePeerConnection": false}} 2026-02-15T155700.305Z INFO livekit service/roommanager.go:1006 created TURN password {"username": "pdmNyEERmdTYx9mTfKIo4LvyW7L7KjsvWjN", "password": "6QFvyPvhlMcM2XBT4tee45R9Ob68Vc5nw7X9G8zw7JZ"} 2026-02-15T155700.305Z ERROR livekit service/roommanager.go:526 could not join room {"room": "StressRoom", "roomID": "RM_XPm6VRNgrgig", "participant": "uqaot_90", "pID": "PA_Noqa7fDA26gi", "remote": false, "error": "room has exceeded its max participants"} github.com/livekit/livekit-server/pkg/service.(*RoomManager).StartSession /workspace/pkg/service/roommanager.go:526 github.com/livekit/livekit-server/pkg/service.(*defaultSessionHandler).HandleSession /workspace/pkg/service/signal.go:115 github.com/livekit/livekit-server/pkg/service.(*signalService).RelaySignal /workspace/pkg/service/signal.go:183 github.com/livekit/psrpc/pkg/server.(*streamHandler[...]).handleOpenRequest /go/pkg/mod/github.com/livekit/psrpc@v0.7.1/pkg/server/stream.go:202 github.com/livekit/psrpc/pkg/server.(*streamHandler[...]).handleRequest.func1 /go/pkg/mod/github.com/livekit/psrpc@v0.7.1/pkg/server/stream.go:141 2026-02-15T155700.305Z INFO livekit rtc/participant.go:1367 participant closing {"room": "StressRoom", "roomID": "RM_XPm6VRNgrgig", "participant": "uqaot_90", "pID": "PA_Noqa7fDA26gi", "remote": false, "sendLeave": true, "reason": "JOIN_FAILED", "isExpectedToResume": false} 2026-02-15T155700.305Z ERROR livekit service/signal.go:186 could not handle new participant {"room": "StressRoom", "participant": "uqaot_90", "connID": "CO_KkVRvTrNPVts", "error": "room has exceeded its max participants"} github.com/livekit/livekit-server/pkg/service.(*signalService).RelaySignal /workspace/pkg/service/signal.go:186 github.com/livekit/psrpc/pkg/server.(*streamHandler[...]).handleOpenRequest /go/pkg/mod/github.com/livekit/psrpc@v0.7.1/pkg/server/stream.go:202 github.com/livekit/psrpc/pkg/server.(*streamHandler[...]).handleRequest.func1 /go/pkg/mod/github.com/livekit/psrpc@v0.7.1/pkg/server/stream.go:141 2026-02-15T155700.306Z INFO livekit service/roommanager.go:413 starting RTC session {"room": "StressRoom", "roomID": "RM_XPm6VRNgrgig", "participant": "uqaot_83", "pID": "PA_2pX5YC3MaFAD", "remote": false, "room": "StressRoom", "nodeID": "ND_8SK7WcmxYRHA", "numParticipants": 50, "participantInit": {"Identity": "uqaot_83", "Reconnect": false, "ReconnectReason": "RR_UNKNOWN", "AutoSubscribe": false, "AutoSubscribeDataTrack": "not-set", "Client": {"sdk": "GO", "version": "2.13.3", "protocol": 16, "os": "linux"}, "Grants": {"Identity": "uqaot_83", "Kind": "standard", "KindDetails": [], "Video": {"RoomCreate": false, "RoomList": false, "RoomRecord": false, "RoomAdmin": false, "RoomJoin": true, "Room": "StressRoom", "CanPublish": "not-set", "CanSubscribe": "not-set", "CanPublishData": "not-set", "CanPublishSources": [], "CanUpdateOwnMetadata": "not-set", "IngressAdmin": false, "Hidden": false, "Recorder": false, "Agent": false, "CanSubscribeMetrics": "not-set", "DestinationRoom": ""}, "SIP": {}, "Agent": {}, "Inference": {}, "Observability": {}, "RoomConfig": {}, "RoomPreset": ""}, "Region": "", "AdaptiveStream": false, "ID": "", "SubscriberAllowPause": "not-set", "DisableICELite": false, "CreateRoom": {"name": "StressRoom"}, "AddTrackRequests": [], "PublisherOffer": {}, "SyncState": {}, "UseSinglePeerConnection": false}} 2026-02-15T155700.306Z INFO livekit service/roommanager.go:1006 created TURN password {"username": "EFkRh10MDlVNYBnMfKIo4LvyW7L7KjsvWjN", "password": "fJXjr5jeJ0EJC64Md1u7XeO9P9pKlewbhfukkoJvQ1cF"} 2026-02-15T155700.306Z ERROR livekit service/roommanager.go:526 could not join room {"room": "StressRoom", "roomID": "RM_XPm6VRNgrgig", "participant": "uqaot_83", "pID": "PA_2pX5YC3MaFAD", "remote": false, "error": "room has exceeded its max participants"} github.com/livekit/livekit-server/pkg/service.(*RoomManager).StartSession /workspace/pkg/service/roommanager.go:526 github.com/livekit/livekit-server/pkg/service.(*defaultSessionHandler).HandleSession admin joins then users joins one by one not at the same time
t
Thanks — this is very helpful. I can see your environment (Android 16, livekit_client 2.6.2, flutter_webrtc 1.3.0), you’re not using Agents or server-side AI processing, and your server logs show many join attempts plus errors like “room has exceeded its max participants” during the stress test. You also publish immediately on connect (fastConnect) and then mute, so many clients are starting audio encoders and publishing simultaneously. Given everything you’ve shared, the issue is most likely a burst-load problem: either client-side CPU/audio-processing overload when many publishers start at once, or transient SFU/network congestion/buffering during the simultaneous publish spike. Below I’ll give a short prioritized checklist of the next tests to run, immediate mitigations you can apply, and exactly what logs/metrics to collect so I can pinpoint the root cause. Prioritized tests (do these in order) 1) Stagger publishes (quick verification) • Change fastConnect/publish so each client delays publishing the microphone by 300–700 ms after connect (or add a small randomized delay when many users join). • Reproduce the 6–7 user simultaneous-join test. • Why: If distortion disappears or reduces, the cause is burst-load (client CPU or SFU burst handling). 2) Local vs received audio capture (definitive isolation) • On a sender, record a short local raw microphone clip (before sending). • On a receiver, record the corresponding received audio. • Compare: if local is clean but received is distorted → SFU/network/receiver processing; if local is distorted → sender/device processing or encoder issue. 3) Client CPU / thread & GC profiling during reproduce • On one affected Android device, run Android Studio Profiler or capture
adb
CPU/heap traces while reproducing the join spike. • Look for CPU saturation, GC pauses, or main-thread stalls when the 6th–7th participant joins. • Why: High CPU/GC correlates strongly with encoder artifacts. 4) Collect per-client connection stats during reproduce • Capture SDK stats at 1–2s intervals for ~30s around the join spike: uplink/downlink bitrate, packetLoss, jitter, rtt, and audio track send/recv state. • What to look for: ◦ Simultaneous packetLoss/jitter spikes across clients → SFU/network problem. ◦ Bitrate drops or re-negotiations coinciding with distortion → encoder/SFU handling. 5) Try conservative audio config changes • Temporarily set
AudioEncoding(maxBitrate: 24000)
and test. Also try disabling `dtx`/`red` temporarily to compare. • Temporarily disable
echoCancellation
and
autoGainControl
in
defaultAudioCaptureOptions
(you currently have noiseSuppression: false, echoCancellation: true, autoGainControl: true). • Why: Reducing encoder load and simplifying capture pipeline helps isolate codec vs processing issues. 6) Server-side checks (simultaneously) • Watch LiveKit server CPU, memory, network, and packet-buffer occupancy during the spike. Your server log shows many join failures due to max participants — ensure your stress test isn’t inadvertently hitting that limit and causing churn. • Check whether TURN is being used heavily (TURN CPU/network could be a bottleneck if many clients relay). Immediate mitigations to apply now • Stagger client publishes / add randomized small join delays (100–700 ms). This is the fastest change likely to reduce distortion. • Lower audio maxBitrate to 24–32 kbps for testing. • Temporarily disable echo cancellation / auto gain control to see if artifacts change. • Avoid heavy UI or background processing on join which could increase CPU load. Exactly what to send me next (so I can analyze and advise precise fixes) • Results of the staggered-publish test (did distortion go away or reduce?). • One local vs received audio pair (10–30s) from a reproduce attempt. • Per-client connection stats (1–2s interval) spanning ~30s around the incident: uplink/downlink bitrate, packetLoss, jitter, rtt, audio send/recv state. • Android profiling output (CPU%/threads or a short screenshot) from a joining client during reproduce. • Client SDK logs (timestamped) around the incident and server LiveKit logs for the same window. The server logs you pasted show many join attempts and “room has exceeded its max participants” — confirm whether your stress test room maxParticipants is being reached and adjust if needed. Small configuration notes based on your current setup • Publishing immediately via fastConnect reduces negotiation delays but creates an encoder/publish burst across users. A short publish delay or stagger prevents simultaneous encoder startups. • Your audio maxBitrate 48 kbps is fine normally, but under burst load lowering it helps the SFU and clients handle multiple incoming/outgoing streams. •
echoCancellation
and
autoGainControl
are enabled; on Android default system processing plus SDK processing may interact under stress—toggle these while testing. If you want, try the staggered publish test now and paste: • One short client SDK log around the reproduce, • The connection stats for that reproduce, • And whether distortion improved when staggered. I’ll review those and give the exact configuration and code changes to remove the artifact (or the next server-side checks if it looks like SFU/network). Sources: [Flutter iOS] SoundWaveformWidget / AudioVisualizerWidget Not Working (Miss... | Optimizing Video Calls for Low-Powered Devices | LiveKit | How to support rooms over 1000 users | LiveKit | client-sdk-flutter | livekit/livekit_composite | DeepWiki