billowy-hairdresser-80384
03/16/2026, 10:28 AMtall-airline-16756
03/16/2026, 10:28 AMtall-airline-16756
03/16/2026, 10:28 AMconnection_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)
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 | LiveKitbillowy-hairdresser-80384
03/16/2026, 10:31 AMtall-airline-16756
03/16/2026, 10:31 AMRTCPeerConnection.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?billowy-hairdresser-80384
03/16/2026, 10:38 AMawait track.get_stats() on any RemoteTrack or LocalTrack.
in my agent code if I use livekit cloud is it possibletall-airline-16756
03/16/2026, 10:39 AMawait 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 Documentationbillowy-hairdresser-80384
03/16/2026, 10:40 AMRTCPeerConnection.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?tall-airline-16756
03/16/2026, 10:41 AMgetStats() 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)
// 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?billowy-hairdresser-80384
03/16/2026, 10:42 AMbillowy-hairdresser-80384
03/16/2026, 10:42 AMtall-airline-16756
03/16/2026, 10:42 AMRTCPeerConnection.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?billowy-hairdresser-80384
03/16/2026, 10:42 AMtall-airline-16756
03/16/2026, 10:43 AMconnectionQualityChanged and track updateStats.
Example (TypeScript)
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).
// 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):
// 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:
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 | LiveKitbillowy-hairdresser-80384
03/16/2026, 11:04 AMtall-airline-16756
03/16/2026, 11:05 AMimport { 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.
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):
// 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).
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)
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?billowy-hairdresser-80384
03/16/2026, 11:08 AMtall-airline-16756
03/16/2026, 11:09 AMupdateStats (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)
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
// 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?