How can I gauge the quality of a client's connecti...
# ask-ai
r
How can I gauge the quality of a client's connection to livekit in realtime? We are leveraging the
useConnectionQualityIndicator
hook, but are there any other tools that would give more fine-grained data on the connection quality?
t
Thanks for reaching out, Riley. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
You're already using
useConnectionQualityIndicator
(high-level). To get finer-grained, realtime visibility you can use three complementary approaches: 1. Poll WebRTC metrics via
room.getStats()
• Use
room.getStats()
(or participant/track-level getStats) every 1–2s. Key fields to monitor:
bitrate
,
packetsLost
,
packetsReceived
,
jitter
,
roundTripTime
,
frameWidth/frameHeight
, and
codecMimeType
. • Use these to compute packet-loss %, actual vs expected bitrate, and detect sudden drops or variance. 1. Listen for SDK events that indicate constraints or failures • Useful events: local CPU-constrained, track subscription failures, local track published/updated, and layer changes. These tell you whether limits are from CPU, capture, or bandwidth and when simulcast layers switch. 1. Use LiveKit Cloud telemetry (if you’re on LiveKit Cloud) • The Cloud Console provides per-participant realtime dashboards (bitrate, packet loss, jitter, end-to-end latency, layer subscriptions, protocol/region) so you can visualize trends without custom instrumentation. How to act on that data (examples): • If packet loss or RTT rises: reduce outgoing resolution/bitrate or switch to a lower simulcast layer. • If
LocalTrackCpuConstrained
fires: lower encoder settings or reduce effects. • If layer availability changes: surface a user warning or auto-adjust UI (hide/highlight poor-quality streams). Would you like a short example showing a polling loop using
room.getStats()
and thresholds to trigger actions? Sources: examples/survey | livekit/livekit_composite | DeepWiki | Optimizing Video Calls for Low-Powered Devices in LiveKit (JS SDK ≥ v2.14.0... | An introduction to WebRTC Simulcast | Announcing LiveKit Cloud
r
Yes please. One thing to note: we are using audio-only tracks, so please do not include any video optimizations. What optimizations can be made with CPU or bandwidth constraints for an audio-only track?
t
Responding to your request about an audio-only example and audio-specific optimizations. Below is a concise example that polls
room.getStats()
every 1–2s, filters for audio track stats, evaluates key metrics (packet loss, bitrate, jitter, RTT), and triggers simple actions when thresholds are crossed. Actions are audio-focused (lower bitrate, switch codec profile, mute/unpublish, notify user). No video optimizations are included.
Copy code
// Example: poll room.getStats() for audio-only tracks and act on thresholds
type AudioStats = {
  trackId?: string;
  participantSid?: string;
  kind?: 'audio';
  packetsLost?: number;
  packetsReceived?: number;
  bitrate?: number; // bytes/sec (SDK may report bits/sec depending on SDK)
  jitter?: number; // seconds
  roundTripTime?: number; // seconds
  codecMimeType?: string;
};

const POLL_INTERVAL_MS = 1500;
const LOSS_THRESHOLD_PERCENT = 5;        // trigger on sustained >5% packet loss
const BITRATE_THRESHOLD_BPS = 12_000;   // e.g., 12 kbps is low for good audio
const JITTER_THRESHOLD_MS = 30;         // jitter >30ms is concerning
const RTT_THRESHOLD_MS = 250;           // high latency

let pollHandle: number | undefined;

function startAudioStatsPolling(room: Room) {
  stopAudioStatsPolling();
  pollHandle = window.setInterval(async () => {
    try {
      const stats = await room.getStats();
      handleAudioStats(stats);
    } catch (err) {
      console.warn('getStats failed', err);
    }
  }, POLL_INTERVAL_MS);
}

function stopAudioStatsPolling() {
  if (pollHandle) {
    clearInterval(pollHandle);
    pollHandle = undefined;
  }
}

function handleAudioStats(reports: any[]) {
  // group or iterate through reports and pick audio-kind reports
  for (const r of reports) {
    if (r.kind !== 'audio') continue;

    const s: AudioStats = {
      trackId: r.trackId,
      participantSid: r.participantSid,
      kind: r.kind,
      packetsLost: r.packetsLost,
      packetsReceived: r.packetsReceived,
      bitrate: r.bitrate, // confirm units in your SDK; may be bits/sec
      jitter: r.jitter,
      roundTripTime: r.roundTripTime,
      codecMimeType: r.codecMimeType,
    };

    // compute packet loss %
    const total = (s.packetsReceived ?? 0) + (s.packetsLost ?? 0);
    const lossPct = total > 0 ? ((s.packetsLost ?? 0) / total) * 100 : 0;
    const bitrateBps = s.bitrate ?? 0;
    const jitterMs = (s.jitter ?? 0) * 1000;
    const rttMs = (s.roundTripTime ?? 0) * 1000;

    // Example decision logic:
    if (lossPct >= LOSS_THRESHOLD_PERCENT || bitrateBps <= BITRATE_THRESHOLD_BPS || jitterMs >= JITTER_THRESHOLD_MS || rttMs >= RTT_THRESHOLD_MS) {
      onAudioQualityDegraded({
        trackId: s.trackId,
        participantSid: s.participantSid,
        lossPct,
        bitrateBps,
        jitterMs,
        rttMs,
        codec: s.codecMimeType,
      });
    } else {
      onAudioQualityRecovered(s.trackId, s.participantSid);
    }
  }
}

// Actions you can take for audio-only tracks
function onAudioQualityDegraded(info: { trackId?: string; participantSid?: string; lossPct: number; bitrateBps: number; jitterMs: number; rttMs: number; codec?: string; }) {
  console.warn('Audio quality degraded', info);

  // Possible mitigation strategies (pick based on your app UX & controls):
  // 1) Reduce audio bitrate / encoder profile (publisher-side)
  //    - If using Opus, lower max average bitrate or set `audioBitrate` when re-negotiating.
  //    - Example: prefer 12-16 kbps for low-bandwidth speech; 24-48 kbps for higher fidelity.
  reduceOutgoingAudioBitrate(info.participantSid, 12_000);

  // 2) Switch to a more resilient codec/profile if available
  //    - e.g., prefer lower-complexity Opus settings or enable FEC (if supported).
  switchToResilientAudioProfile(info.participantSid);

  // 3) Temporarily disable non-essential audio processing (effects) to save CPU
  //    - e.g., turn off local noise-suppression or advanced processing if CPU is constrained.
  disableLocalAudioProcessing();

  // 4) If conditions are severe, notify user or fallback to audio-only low-bandwidth mode
  //    - show UI toast: "Poor network — switching to low-bandwidth audio"
  notifyUserPoorAudio();

  // 5) As last resort, pause/unpublish to reduce uplink usage
  //    - only if user consents or app policy allows auto-unpublish
  considerUnpublishLocalTrackIfNecessary();
}

function onAudioQualityRecovered(trackId?: string, participantSid?: string) {
  // Restore higher bitrate/profile or re-enable processing as needed
  restoreOutgoingAudioBitrate(participantSid);
  enableLocalAudioProcessing();
  clearUserNotifications();
}

/*
Below are placeholders — implement according to your SDK and app:
- reduceOutgoingAudioBitrate: update encoder params or republish with lower bitrate
- switchToResilientAudioProfile: change Opus settings, enable FEC, or choose different codec params
- disableLocalAudioProcessing / enableLocalAudioProcessing: toggle effects like AGC/NS/ANS
- notifyUserPoorAudio / clearUserNotifications: UX for informing users
- considerUnpublishLocalTrackIfNecessary: unpublish or mute local track
*/

function reduceOutgoingAudioBitrate(participantSid: string | undefined, targetBps: number) {
  // Example approach:
  // - If you control the publisher settings, update the encoder's maxAverageBitrate or set constraints
  // - For LiveKit, you can update local track encoding parameters when creating or re-publishing a track.
  console.log('Request reduce outgoing audio bitrate to', targetBps, 'bps for', participantSid);
}

function switchToResilientAudioProfile(participantSid: string | undefined) {
  console.log('Switching to resilient audio profile for', participantSid);
}

function disableLocalAudioProcessing() {
  console.log('Disabling non-essential audio processing to save CPU');
}

function enableLocalAudioProcessing() {
  console.log('Re-enabling audio processing');
}

function restoreOutgoingAudioBitrate(participantSid: string | undefined) {
  console.log('Restoring outgoing audio bitrate for', participantSid);
}

function notifyUserPoorAudio() {
  // show UI hint to user
  console.log('Notifying user about poor audio conditions');
}

function clearUserNotifications() {
  console.log('Clearing audio notifications');
}

function considerUnpublishLocalTrackIfNecessary() {
  console.log('Consider unpublishing local audio track');
}
Audio-specific optimization suggestions (concise): • Reduce outgoing bitrate: lower Opus target/max bitrate (12–24 kbps for speech). • Use codec features: enable FEC/comfort noise where available to help recover from packet loss. • Lower encoder complexity: choose lower CPU complexity settings for Opus if device CPU is limited. • Disable extra processing: turn off noise suppression/echo cancellation or other high-cost effects when CPU constrained. • Retry or re-negotiate with stricter bandwidth limits before unpublishing. • Notify users and provide a manual "low-bandwidth mode" toggle to reduce uplink usage. Would you like a concrete snippet showing how to change outgoing Opus bitrate or toggle audio processing in your client code?
r
How can I update the bitrate of the local participant's published audio track?
t
You're correct to focus on audio-only. Regarding updating the local participant's published audio bitrate: • Bitrate is set when you publish the track via
localParticipant.publishTrack(track, options)
using the
audioBitrate
field in
TrackPublishOptions
. • You cannot dynamically change the bitrate of an already-published audio track. To change it, unpublish the track (or stop it), then re-publish with the new
audioBitrate
. Example (TypeScript):
Copy code
// publish with initial bitrate
const track = await createLocalAudioTrack();
await localParticipant.publishTrack(track, { audioBitrate: 24000 }); // 24 kbps

// to reduce/increase bitrate later:
await localParticipant.unpublishTrack(track);
await localParticipant.publishTrack(track, { audioBitrate: 12000 }); // 12 kbps
Options you can set at publish time include
audioBitrate
,
dtx
, and
red
. If you want, I can provide a safer re-publish flow that preserves mute/state and minimizes interruption. Would you like that? Sources: Camera &amp; microphone | LiveKit docs | Codecs and more | LiveKit docs