how can I get the jitter, packet loss kind of all ...
# ask-ai
b
how can I get the jitter, packet loss kind of all the webrtc related metrics from the livekit cloud webrtc in python
✅ 1
t
Thanks for reaching out, Yethu. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m responding to your question about how to get WebRTC metrics (jitter, packet loss, etc.) from LiveKit Cloud using Python. Short answer • You can get connection quality (Poor / Good / Excellent) and monitor packet-loss/bitrate signals via participant connection quality and telemetry. • Jitter and RTT are not exposed in LiveKit Cloud’s connection quality scoring (they are disabled), so raw jitter/RTT values are not available from the Cloud Python SDK. If you need raw RTP stats you’ll need a self-hosted SFU or another telemetry export. What you can get from the Python client • participant.connection_quality — current quality level for a participant (Poor / Good / Excellent). • connection quality events — listen for
connection_quality_changed
to react to quality changes in real time. • Track-level stats and metrics (bitrate, packet loss trends, simulcast layer changes) are available via LiveKit Cloud observability/analytics (dashboard and telemetry). The Python SDK exposes the participant and event hooks so you can surface the connection quality values in your app. Example (how to listen for connection quality changes)
Copy code
from livekit import rtc

async def on_connection_quality_changed(quality: rtc.ConnectionQuality):
    print(f"Connection quality changed: {quality}")

@room.on("participant_connected")
def on_participant_connected(participant: rtc.RemoteParticipant):
    print(f"Participant {participant.identity} joined with connection quality: {participant.connection_quality}")
    participant.on("connection_quality_changed", on_connection_quality_changed)
Notes about jitter and RTT • Jitter and RTT are explicitly disabled in LiveKit’s connection quality scoring and are not exposed as direct metrics through the Cloud Python SDK. That means you cannot read per-participant jitter or RTT values from LiveKit Cloud in Python. • If you require raw RTP-level stats (including jitter/RTT), you would need to self-host the LiveKit SFU and export telemetry or collect RTP stats from the client side (e.g., browser WebRTC getStats) and send them to your backend. Recommended approaches depending on your need • If you want simple, actionable network health: use
participant.connection_quality
and react to
connection_quality_changed
. This gives an immediate signal (Poor/Good/Excellent) derived from packet loss, bitrate, and layer availability. • If you need numeric packet-loss or bitrate values over time: use LiveKit Cloud’s analytics/observability dashboard (or telemetry exports) to retrieve track-level stats. • If you absolutely require jitter/RTT numbers: consider self-hosting the SFU and exporting telemetry, or collect getStats() from client browsers/SDKs and send those stats to your backend. If you want, I can: • Show a fuller Python example to collect and log connection quality changes per participant. • Describe how to collect client-side getStats() in a browser or native SDK and forward those metrics to your backend for analysis. • Outline steps to enable telemetry exports if you plan to self-host. Which of the above would help you most right now? Sources: examples/survey | livekit/livekit_composite | DeepWiki | How is Connection Quality determined? | examples/voice_agents | livekit/livekit_composite | DeepWiki | Announcing LiveKit Cloud | LiveKit | Say hello to improved Cloud analytics | LiveKit
b
getStats() so this is not available in livekit cloud?
t
Thanks — I’m following up on your question about whether WebRTC’s getStats() (the API that returns jitter, RTT, per-RTP metrics, etc.) is available from LiveKit Cloud. Short answer • getStats() is a browser / client-side WebRTC API and is not exposed directly through LiveKit Cloud’s Python SDK. LiveKit Cloud does not provide raw per-RTP getStats() values (jitter/RTT) from the managed Cloud SFU. Why and what that means • LiveKit Cloud exposes a derived connection quality score (Poor / Good / Excellent) and provides track-level signals (packet loss trends, bitrate, layer changes) via its observability features, but per-RTP numeric stats like jitter and RTT are not returned from the Cloud SFU. • The WebRTC getStats() API remains available on the client (browser or native SDK). You can call getStats() in the client to collect jitter/RTT and other raw metrics. Options you can use • Collect client-side stats: ◦ Call
RTCPeerConnection.getStats()
in the browser or the equivalent in native SDKs, then send those metrics to your backend for storage/analysis. • Use LiveKit Cloud observability: ◦ Use the LiveKit dashboard / telemetry exports for track-level metrics (bitrate, packet loss trends, frame rates, simulcast layer info). This is good for monitoring but won’t give raw jitter/RTT. • Self-host LiveKit SFU: ◦ If you need server-side access to raw RTP telemetry (including jitter/RTT), self-host the SFU and enable telemetry/webhook exports to collect raw stats from the SFU. If you want, I can: • Show a short example of how to call
getStats()
in the browser and forward results to your Python backend. • Outline how to aggregate and store those metrics for analysis. • Describe where to find the LiveKit observability features if you want to compare trends without raw jitter/RTT. Which would you like next — a getStats() code example or steps to wire client stats into your backend?
b
1. Direct Python Collection: If your Python code is a participant in the room, call
await track.get_stats()
on any
RemoteTrack
or
LocalTrack
. in my agent code if I use livekit cloud is it possible
t
Thanks — I’m replying about your specific line: “If your Python code is a participant in the room, call
await track.get_stats()
on any
RemoteTrack
or
LocalTrack
— in my agent code if I use LiveKit Cloud is it possible?” Short answer • No — you cannot call
await track.get_stats()
from an Agent running on LiveKit Cloud. Agent Python code does not maintain a full WebRTC peer connection, so WebRTC getStats()–style RTP metrics (jitter, RTT, per-RTP stats) are not available from the agent. Why • Agents use a server-side SDK and server-side media streams (e.g.,
AudioStream
/
VideoStream
) rather than a browser/native WebRTC peer connection. The
get_stats()
method exists in client SDKs (browser, iOS, Android) but is not implemented for the agent/server context. What you can do instead • Collect client-side stats: Call
RTCPeerConnection.getStats()
in the browser or the equivalent in native client SDKs and forward those results to your backend (e.g., POST to your Python service). This gives you jitter, RTT, per-RTP metrics. • Use LiveKit Cloud observability: Use the LiveKit dashboard / telemetry for track-level trends (bitrate, packet loss, layer changes) to monitor room health—these are exposed by the Cloud but do not include raw jitter/RTT values. • Self-host SFU for raw telemetry: If you need server-side access to raw RTP stats (including jitter/RTT), self-host the LiveKit SFU and enable telemetry/webhook exports. If you want, I can: • Show a short browser
getStats()
example and a simple pattern to forward those stats to your Python agent backend. • Outline how to store/aggregate those metrics server-side for monitoring and alerts. Which option would you like help with next — a client getStats() snippet or the backend aggregation pattern? Sources: Client SDKs | livekit/livekit_composite | DeepWiki | client-sdk-js | livekit/livekit_composite | DeepWiki | LiveKit Agent Observability | Debug and optimize voice agents | LiveKit | LiveKit Platform | Build, run, and observe voice AI agents | LiveKit | LLM Metrics | LiveKit Documentation
b
Collect client-side stats: Call
RTCPeerConnection.getStats()
in the browser or the equivalent in native client SDKs and forward those results to your backend (e.g., POST to your Python service). This gives you jitter, RTT, per-RTP metrics. if I use ivekit coud also?
t
Thanks — you’re asking whether you can use client-side
getStats()
together with LiveKit Cloud and forward those stats to your Python backend/agent. Short answer: yes. What this looks like • LiveKit Cloud does not expose raw per-RTP stats (jitter/RTT) from the managed SFU to agent/server code. • But any participant running a client SDK (browser, iOS, Android) has a real WebRTC peer connection and can call
RTCPeerConnection.getStats()
(or the platform equivalent). • You can collect those client-side stats and POST them to your Python backend (or your agent) while using LiveKit Cloud for signaling and media routing. High-level steps to implement 1. On the client (browser or native SDK): ◦ Access the underlying
RTCPeerConnection
(Web SDKs expose it or let you call
getStats()
on tracks/pc). ◦ Periodically call
getStats()
(for example, every 5–30s) and extract fields you need (jitter, RTT, packetsLost, bytesReceived, etc.). ◦ Send the collected metrics to your backend via an authenticated HTTP POST or WebSocket. 1. Backend / Agent: ◦ Receive the posted stats and store/aggregate them (database, time-series store, dashboard). ◦ Correlate stats with participant identity, room ID, and timestamps so you can combine client-side metrics with LiveKit Cloud observability. 1. Combine with LiveKit Cloud observability: ◦ Use LiveKit’s dashboards/telemetry for track-level and session-level trends, and use your client-supplied getStats() data when you need raw RTP metrics like jitter or RTT. Browser getStats() example (concept)
Copy code
// assume `pc` is your RTCPeerConnection
setInterval(async () => {
  const stats = await pc.getStats();
  const out = [];
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' || report.type === 'candidate-pair') {
      out.push(report);
    }
  });
  // POST the selected reports to your backend
  await fetch('/collect-stats', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({room: ROOM_ID, participant: IDENTITY, stats: out, ts: Date.now()}),
  });
}, 10000);
Notes and limitations • Frequency & volume: getStats() can produce a lot of data—choose a sensible interval and filter out only the fields you need. • Security/Privacy: authenticate and rate-limit the endpoint receiving stats; avoid sending sensitive media content. • Correlation: include participant identity and room ID when posting so you can match client stats to LiveKit room sessions. • LiveKit Cloud still won’t expose server-side jitter/RTT for agents; client-reported stats are the way to obtain those numbers while using LiveKit Cloud. If you want, I can: • Provide a full browser getStats() snippet that extracts jitter/RTT/packetsLost cleanly. • Show a minimal Flask/FastAPI endpoint to receive and store stats in Python. • Suggest which getStats() fields are most useful to store for monitoring and alerting. Which of those would you like next?
b
If I would use livekit cloud webrtc also it is possible on client side
right?
t
Yes — that’s correct. Quick confirmation • If you use LiveKit Cloud, any participant running a client SDK (browser, iOS, Android) has a real WebRTC peer connection and can call the client-side getStats() API to collect jitter, RTT, per-RTP metrics, packetsLost, bytesReceived, etc. • LiveKit Cloud does not expose those raw getStats() values from the managed SFU to agents or server-side SDKs. To get jitter/RTT while using LiveKit Cloud, you must collect them on the client and forward them to your backend. Recommended implementation pattern 1. Client side ◦ Periodically call
RTCPeerConnection.getStats()
(or SDK-equivalent) and filter the reports you need (e.g.,
inbound-rtp
,
outbound-rtp
,
candidate-pair
). ◦ Include identifying context with each sample (room ID, participant identity, timestamp). ◦ Send the filtered data to your backend via an authenticated POST or WebSocket. 1. Backend / Agent ◦ Receive, store, and aggregate the client-reported stats (time-series DB, logs, or dashboards). ◦ Correlate with LiveKit room/session IDs or LiveKit observability data for end-to-end analysis. 1. Combine with LiveKit Cloud observability ◦ Use LiveKit’s dashboard/telemetry for session- and track-level trends (bitrate, packet loss trends, layer switches). ◦ Use your client getStats() data for raw RTP metrics (jitter/RTT) and fine-grained troubleshooting. Practical offers • I can provide a focused browser
getStats()
snippet that extracts jitter/RTT/packetsLost cleanly. • I can show a minimal Python (Flask/FastAPI) endpoint to receive and store those stats, plus recommended fields to persist for monitoring. Which would you like next — the client snippet or a backend receiver example?
b
ok give me the sdk and code for front end
t
Thanks — I’ll give you a focused frontend solution you can drop into a LiveKit Cloud web app. This includes: • Installing the LiveKit JS SDK • Using LiveKit events to get high-level track/connection metrics • A browser getStats() snippet to extract jitter/RTT/packet-loss • How to POST those stats to your Python backend Install the SDK 1. npm: ◦ npm install livekit-client High-level LiveKit approach (recommended) • LiveKit emits connection quality and track-level stats. Use these for quick monitoring and lightweight apps. • Use Room and Participant events to listen for
connectionQualityChanged
and track
updateStats
. Example (TypeScript)
Copy code
import { Room, RoomEvent } from "livekit-client";

const room = new Room();

// when a participant connects
room.on(RoomEvent.ParticipantConnected, (participant) => {
  console.log("participant connected:", participant.identity);

  // connection quality changes: "excellent" | "good" | "poor"
  participant.on("connectionQualityChanged", (quality) => {
    console.log("connection quality:", quality);
  });

  // track subscription events
  participant.on(RoomEvent.TrackSubscribed, (track) => {
    console.log("track subscribed:", track.kind);
    // Track may emit periodic 'updateStats' events with useful fields
    track.on("updateStats", (stats) => {
      // stats is an array of per-layer/track stat objects
      console.log("track stats:", stats);
    });
  });
});

// connect (replace with your LiveKit Cloud URL + token)
await room.connect("<wss://your-server.livekit.cloud>", "JWT_TOKEN");
Client-side getStats() (for jitter, RTT, per-RTP metrics) • Use getStats() on the page’s RTCPeerConnection to obtain raw RTP-level metrics (jitter, RTT, packetsLost, bytesReceived). • LiveKit client hides the peer connection internally, but you can still access the native RTCPeerConnection via the LiveKit internals (advanced / browser-dependent). If you prefer not to rely on internals, call getStats() on a MediaStreamTrack’s sender/receiver where available or use the native PC if you can access it. Safe / portable pattern: call getStats() against the page’s RTCPeerConnection (example assumes you can get
pc
).
Copy code
// Example: polling getStats() every 10s, extracts useful fields and posts to backend.
async function pollAndSendStats(pc, roomId, participantId, url) {
  setInterval(async () => {
    try {
      const stats = await pc.getStats();
      const out = [];

      stats.forEach((report) => {
        // inbound-rtp contains jitter, packetsLost, bytesReceived
        if (report.type === "inbound-rtp" || report.type === "outbound-rtp") {
          out.push({
            id: report.id,
            type: report.type,
            timestamp: report.timestamp,
            jitter: report.jitter,               // seconds (convert to ms if you want)
            packetsLost: report.packetsLost,
            bytesReceived: report.bytesReceived,
            bytesSent: report.bytesSent,
            roundTripTime: report.roundTripTime, // may be on candidate-pair instead
            ssrc: report.ssrc,
            codecId: report.codecId,
          });
        }

        // candidate-pair often has currentRoundTripTime
        if (report.type === "candidate-pair" && report.selected) {
          out.push({
            id: report.id,
            type: report.type,
            timestamp: report.timestamp,
            currentRoundTripTime: report.currentRoundTripTime,
            availableOutgoingBitrate: report.availableOutgoingBitrate,
            availableIncomingBitrate: report.availableIncomingBitrate,
          });
        }
      });

      // POST filtered stats to your backend; include room/participant context
      await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          roomId,
          participantId,
          ts: Date.now(),
          stats: out,
        }),
      });
    } catch (e) {
      console.warn("error collecting stats", e);
    }
  }, 10000); // adjust interval to your needs
}
How to get the RTCPeerConnection instance (options) • Preferred: if your app directly creates and manages the RTCPeerConnection (custom integration), call
pc.getStats()
directly. • If using livekit-client and you need the native PC: ◦ There’s an internal path to the publisher/subscriber PC (browser-only and not guaranteed across versions). Example (advanced):
Copy code
// WARNING: uses internal API; may change across versions
    const engine = (room as any).engine;
    const pc = engine?.pcManager?.publisher?.pc || engine?.pcManager?.subscriber?.pc;
• If you use this, guard with checks and fall back to LiveKit events if pc is not present. Server endpoint (Python minimal example) • Simple FastAPI endpoint to receive and store stats:
Copy code
from fastapi import FastAPI, Request
import uvicorn
import json

app = FastAPI()

@app.post("/collect-stats")
async def collect_stats(req: Request):
    payload = await req.json()
    # payload: {roomId, participantId, ts, stats}
    # store / process / send to time-series DB
    print("received stats:", json.dumps(payload)[:1000])
    return {"status": "ok"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
Practical tips • Convert jitter from seconds to milliseconds for easier dashboards (jitter_ms = jitter * 1000). • Reduce payload: only send fields you need;
getStats()
can be large. • Sampling interval: 5–30 seconds is typical. Shorter intervals increase load. • Always include roomId and participantId to correlate with LiveKit logs/observability. • Secure the endpoint (auth, rate-limit) and validate participant identity. Would you like: • A ready-to-run complete example (HTML + JS + FastAPI) wired end-to-end? • A TypeScript example using the livekit-client internals to access the peer connection? • Recommended fields to persist in a time-series DB for monitoring and alerting? Which one should I prepare next? Sources: How is Connection Quality determined? | Announcing LiveKit Cloud | LiveKit
b
See give me from the client SDK how can we integrate this?
t
Thanks — I’ll walk you through integrating client-side metrics collection using the LiveKit JavaScript client SDK. This will show both the recommended high-level LiveKit events and a safe way to call the browser getStats() to capture jitter/RTT/packetsLost, then forward those to a Python backend. What you’ll get • How to install and connect with livekit-client • Use LiveKit events (connectionQuality, track updateStats) • Access the native RTCPeerConnection and call getStats() • Example to POST filtered stats to a Python endpoint 1) Install and basic connect • Install: ◦ npm install livekit-client • Connect (very basic)
Copy code
import { Room } from "livekit-client";

const room = new Room();
await room.connect("<wss://your-server.livekit.cloud>", "YOUR_JWT_TOKEN");
// room now has participants and tracks
2) Recommended: use LiveKit high-level events • These are stable and good for most monitoring use cases.
Copy code
import { Room, RoomEvent } from "livekit-client";

room.on(RoomEvent.ParticipantConnected, participant => {
  console.log("participant connected:", participant.identity);

  // connection quality changes: "excellent" | "good" | "poor"
  participant.on("connectionQualityChanged", (quality) => {
    console.log("connection quality:", quality);
    // send to backend if you want
  });

  // track subscription -> listen for periodic track stats
  participant.on(RoomEvent.TrackSubscribed, (track) => {
    console.log("track subscribed:", track.kind);
    track.on("updateStats", (statsArray) => {
      // statsArray contains per-layer or per-track stat objects
      console.log("livekit track stats:", statsArray);
      // filter and POST if desired
    });
  });
});
3) Low-level: call browser getStats() for jitter, RTT, per-RTP metrics • The raw WebRTC stats (jitter, currentRoundTripTime, packetsLost) come from
RTCPeerConnection.getStats()
. LiveKit internally manages the PC, but you can access the native PC (advanced) or call getStats() on track senders/receivers where available. • Preferred approach if you control the PC: call
pc.getStats()
directly. • If you use livekit-client, you can access the internal PC (browser-only, internal API — guard against missing fields):
Copy code
// WARNING: uses internal APIs; these internals may change across SDK versions.
// Use guards to avoid runtime errors.
function getNativePC(room) {
  const engine = room && room.engine ? room.engine : (room as any)?.engine;
  if (!engine) return null;
  const pc = engine?.pcManager?.publisher?.pc || engine?.pcManager?.subscriber?.pc;
  return pc || null;
}
4) Poll getStats(), filter useful fields, and POST to backend • Example polling function. Poll interval and fields can be tuned to your needs (5–30s typical).
Copy code
async function pollAndPostStats(room, backendUrl, roomId, participantId, intervalMs = 10000) {
  const pc = getNativePC(room);
  if (!pc) {
    console.warn("Native RTCPeerConnection not found; falling back to LiveKit events");
    return;
  }

  setInterval(async () => {
    try {
      const stats = await pc.getStats();
      const filtered = [];
      stats.forEach(report => {
        // inbound/outbound RTP reports contain jitter/packetsLost/bytes
        if (report.type === "inbound-rtp" || report.type === "outbound-rtp") {
          filtered.push({
            id: report.id,
            type: report.type,
            ssrc: report.ssrc,
            timestamp: report.timestamp,
            jitter_s: report.jitter,               // seconds; multiply by 1000 for ms
            packetsLost: report.packetsLost,
            bytesReceived: report.bytesReceived,
            bytesSent: report.bytesSent,
            codecId: report.codecId,
          });
        }

        // candidate-pair (selected) often contains round-trip time
        if (report.type === "candidate-pair" && report.selected) {
          filtered.push({
            id: report.id,
            type: report.type,
            timestamp: report.timestamp,
            currentRoundTripTime_s: report.currentRoundTripTime,
            availableOutgoingBitrate: report.availableOutgoingBitrate,
            availableIncomingBitrate: report.availableIncomingBitrate,
          });
        }
      });

      // convert jitter/RTT to ms for easier dashboards
      filtered.forEach(r => {
        if (r.jitter_s !== undefined) r.jitter_ms = r.jitter_s * 1000;
        if (r.currentRoundTripTime_s !== undefined) r.rtt_ms = r.currentRoundTripTime_s * 1000;
      });

      // POST to backend with identifying context
      await fetch(backendUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          roomId,
          participantId,
          ts: Date.now(),
          stats: filtered,
        }),
      });
    } catch (err) {
      console.warn("error collecting or posting stats:", err);
    }
  }, intervalMs);
}
5) Minimal Python receiver (FastAPI)
Copy code
from fastapi import FastAPI, Request
import uvicorn
import json

app = FastAPI()

@app.post("/collect-stats")
async def collect_stats(req: Request):
    payload = await req.json()
    # payload includes roomId, participantId, ts, stats
    print("received stats sample:", json.dumps(payload)[:1000])
    # TODO: validate, store in DB or timeseries store
    return {"ok": True}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
6) Practical tips and best practices • Sampling interval: 5–30s is usual. Short intervals increase CPU/network cost. • Reduce payload: only send needed fields (jitter, rtt, packetsLost, bitrate). • Convert units to something consistent (jitter/RTT -> ms). • Include roomId, participantId, and timestamp on each sample for correlation with LiveKit dashboards. • Secure the endpoint: authenticate client posts and rate-limit. • Use LiveKit high-level events for quick monitoring and getStats() only when you need raw RTP numbers. If you’d like, I can: • Provide a complete working example (single HTML file + JS + FastAPI server) you can run locally. • Produce a TypeScript example that subscribes to LiveKit track events and falls back to getStats() when the native PC is available. • Suggest which getStats() fields to persist for alerts and dashboards (I can list recommended fields). Which would you like next — a full runnable example, TypeScript integration, or recommended telemetry schema?
b
see is this based on polling can i do on each stream basis both upstream and downstream
t
Thanks — I’m responding to your question: can you collect metrics per stream (both upstream and downstream) rather than only by polling? Short answer: yes — you can collect metrics on a per-track/stream basis. There are two complementary approaches you can use together: • Event-driven (per-track) via LiveKit SDK: LiveKit emits track-level events and periodic track stats updates you can listen to for each subscribed/published track. This is the recommended, simple approach for most use cases. • getStats() (polling) per RTCRtpSender/Receiver or per RTCPeerConnection: browser getStats() is the only way to get raw RTP fields (jitter, currentRoundTripTime, exact packetsLost in some browsers). getStats() is inherently sampled (you call it on a schedule) but you can call it per track by filtering reports for that track’s SSRC or sender/receiver. Below I show patterns for both upstream (your local published tracks) and downstream (remote/subscribed tracks), and how to correlate and forward data to your backend. 1) Per-track using LiveKit SDK events (recommended first) • For downstream (remote) tracks: ◦ Listen for RoomEvent.TrackSubscribed (or participant event). Each Track can emit periodic
updateStats
(the SDK provides high-level stat objects). • For upstream (local) tracks: ◦ The LocalParticipant / LocalTrack also exposes events and stats; check the SDK’s local track API for
updateStats
or similar events. Example (TypeScript — event-driven per-track)
Copy code
import { Room, RoomEvent } from "livekit-client";

const room = new Room();

room.on(RoomEvent.ParticipantConnected, (participant) => {
  participant.on(RoomEvent.TrackSubscribed, (track) => {
    // per-remote-track handler
    track.on("updateStats", (statsArray) => {
      // statsArray contains per-layer/per-track stat objects
      // each object will have fields like bitrate, packetsLost, frameRate, maybe jitter
      sendStatsToBackend({
        streamType: "downstream",
        participant: participant.identity,
        trackSid: track.sid,
        stats: statsArray,
        ts: Date.now(),
      });
    });
  });

  // if you publish local tracks, they may have similar events
  participant.on("trackPublished", (publication) => {
    const localTrack = publication.track; // may be available for local participant
    if (localTrack) {
      localTrack.on("updateStats", (statsArray) => {
        sendStatsToBackend({
          streamType: "upstream",
          participant: participant.identity,
          trackSid: localTrack.sid,
          stats: statsArray,
          ts: Date.now(),
        });
      });
    }
  });
});
Notes: • LiveKit’s
updateStats
emits per-track summaries at intervals; you don’t need to manually poll. • The exact fields in
statsArray
depend on SDK version; typical fields: bitrate, packetsLost, frameRate, layer, jitter (if present). Use these first — they’re stable and lightweight. 2) Per-track raw RTP metrics via getStats() (per-track filtering) • getStats() returns many reports; you filter per-track by matching SSRC, mediaType, or sender/receiver IDs. • This approach gives you jitter (report.jitter), candidate-pair RTT, and more precise RTP numbers. • Because getStats() is a call, you must schedule it (poll) — but you can filter results to a specific track to effectively get per-stream metrics. Example (polling but per-track) — client-side JavaScript
Copy code
// helper to find native pc from room (guarded)
function getNativePC(room) {
  const engine = (room as any)?.engine;
  if (!engine) return null;
  return engine.pcManager?.publisher?.pc || engine.pcManager?.subscriber?.pc || null;
}

async function collectPerTrackStats(pc, targetSsrcOrTrackId) {
  const reports = await pc.getStats();
  const filtered = [];
  reports.forEach(r => {
    // inbound-rtp or outbound-rtp often includes ssrc and packetsLost/jitter
    if ((r.type === "inbound-rtp" || r.type === "outbound-rtp") &&
        (r.ssrc === targetSsrcOrTrackId || r.trackId === targetSsrcOrTrackId || r.id.includes(targetSsrcOrTrackId))) {
      filtered.push(r);
    }
    // candidate-pair selected for RTT
    if (r.type === "candidate-pair" && r.selected) {
      filtered.push({
        type: r.type,
        currentRoundTripTime: r.currentRoundTripTime,
      });
    }
  });
  return filtered;
}

// call periodically for each track you want to monitor
setInterval(async () => {
  for (const trackInfo of tracksToMonitor) {
    const stats = await collectPerTrackStats(pc, trackInfo.ssrcOrTrackId);
    postStats({ streamType: trackInfo.type, participant: trackInfo.participant, trackId: trackInfo.trackId, stats });
  }
}, 10000);
Notes: • Identify each track’s SSRC or trackId when the track is created/subscribed; store it so you can filter getStats results. • Poll frequency: 5–30s typical. Shorter intervals increase CPU/ bandwidth. 3) Upstream vs Downstream specifics and correlation • Upstream (local) metrics: filter
outbound-rtp
reports for the local sender’s SSRC; this shows bytesSent, packetsSent, packetsLost (some browsers), jitter may be absent for outbound. • Downstream (remote) metrics: filter
inbound-rtp
reports for the receiver’s SSRC; this shows bytesReceived, packetsLost, jitter (often present). • Candidate-pair or
remote-candidate
reports give RTT/currentRoundTripTime (often on the selected candidate-pair). 4) Combining LiveKit events + getStats() • Use LiveKit track
updateStats
for lightweight per-track monitoring and alerts. • Use getStats() for deeper troubleshooting on specific tracks (call it only for tracks flagged by the event-driven path to avoid polling everything). • Example flow: ◦ All tracks: subscribe to LiveKit
updateStats
. If a track’s quality becomes poor or packet loss spikes, start getStats() polling just for that track for 30–60s to collect raw jitter/RTT and diagnose. 5) Forwarding to backend • Include identifiers: roomId, participant identity, trackSid, SSRC, direction (upstream/downstream), and timestamp. • Send filtered minimal fields (jitter_ms, rtt_ms, packetsLost, bitrate) to reduce payload. • Secure the endpoint (auth token, rate-limiting). 6) Practical tips • Prefer event-driven first (lower overhead). Poll only when needed for raw RTP values. • Track SSRCs and publisher/receiver ids at subscription/publish time to map getStats() reports to tracks. • Convert units: jitter and RTT are in seconds in getStats() — multiply by 1000 for ms in dashboards. • Test across browsers — getStats() field availability varies by browser/version. If you’d like, I can: • Provide a complete runnable example (single HTML/TS file) that: ◦ Connects to LiveKit Cloud ◦ Subscribes/publishes a track ◦ Listens to LiveKit per-track events ◦ Falls back to targeted getStats() polling per-track when the LiveKit event indicates a problem ◦ Posts metrics to a sample FastAPI endpoint • Or produce a smaller snippet showing exactly how to map track.sid to SSRC and filter getStats() for inbound/outbound per-track. Which of those would you prefer — the full runnable example or the focused SSRC mapping + targeted polling snippet?