rough-addition-41764
12/15/2025, 6:29 PMuseConnectionQualityIndicator hook, but are there any other tools that would give more fine-grained data on the connection quality?tall-airline-16756
12/15/2025, 6:29 PMtall-airline-16756
12/15/2025, 6:29 PMuseConnectionQualityIndicator (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 Cloudrough-addition-41764
12/15/2025, 6:32 PMtall-airline-16756
12/15/2025, 6:33 PMroom.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.
// 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?rough-addition-41764
12/15/2025, 7:15 PMtall-airline-16756
12/15/2025, 7:16 PMlocalParticipant.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):
// 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 & microphone | LiveKit docs | Codecs and more | LiveKit docs