import { useInterval } from "usehooks-ts"; import SidebarHeader from "@/components/SidebarHeader"; import { useRTCStore, useUiStore } from "@/hooks/stores"; import { createChartArray, Metric } from "../Metric"; import { SettingsSectionHeader } from "../SettingsSectionHeader"; export default function ConnectionStatsSidebar() { const inboundVideoRtpStats = useRTCStore(state => state.inboundRtpStats); const iceCandidatePairStats = useRTCStore(state => state.candidatePairStats); const setSidebarView = useUiStore(state => state.setSidebarView); function isMetricSupported( stream: Map, metric: K, ): boolean { return Array.from(stream).some(([, stat]) => stat[metric] !== undefined); } const appendInboundVideoRtpStats = useRTCStore(state => state.appendInboundRtpStats); const appendIceCandidatePair = useRTCStore(state => state.appendCandidatePairStats); const appendDiskDataChannelStats = useRTCStore( state => state.appendDiskDataChannelStats, ); const appendLocalCandidateStats = useRTCStore(state => state.appendLocalCandidateStats); const appendRemoteCandidateStats = useRTCStore( state => state.appendRemoteCandidateStats, ); const peerConnection = useRTCStore(state => state.peerConnection); const mediaStream = useRTCStore(state => state.mediaStream); const sidebarView = useUiStore(state => state.sidebarView); useInterval(function collectWebRTCStats() { (async () => { if (!mediaStream) return; const stats = await peerConnection?.getStats(); let successfulLocalCandidateId: string | null = null; let successfulRemoteCandidateId: string | null = null; stats?.forEach(report => { if (report.type === "inbound-rtp" && report.kind === "video") { appendInboundVideoRtpStats(report); } else if (report.type === "candidate-pair" && report.nominated) { if (report.state === "succeeded") { successfulLocalCandidateId = report.localCandidateId; successfulRemoteCandidateId = report.remoteCandidateId; } appendIceCandidatePair(report); } else if (report.type === "local-candidate") { // We only want to append the local candidate stats that were used in nominated candidate pair if (successfulLocalCandidateId === report.id) { appendLocalCandidateStats(report); } } else if (report.type === "remote-candidate") { if (successfulRemoteCandidateId === report.id) { appendRemoteCandidateStats(report); } } else if (report.type === "data-channel" && report.label === "disk") { appendDiskDataChannelStats(report); } }); })(); }, 500); const jitterBufferDelay = createChartArray(inboundVideoRtpStats, "jitterBufferDelay"); const jitterBufferEmittedCount = createChartArray( inboundVideoRtpStats, "jitterBufferEmittedCount", ); const jitterBufferAvgDelayData = jitterBufferDelay.map((d, idx) => { if (idx === 0) return { date: d.date, stat: null }; const prevDelay = jitterBufferDelay[idx - 1]?.stat as number | null | undefined; const currDelay = d.stat as number | null | undefined; const prevEmitted = (jitterBufferEmittedCount[idx - 1]?.stat as number | null | undefined) ?? null; const currEmitted = (jitterBufferEmittedCount[idx]?.stat as number | null | undefined) ?? null; if ( prevDelay == null || currDelay == null || prevEmitted == null || currEmitted == null ) { return { date: d.date, stat: null }; } const deltaDelay = currDelay - prevDelay; const deltaEmitted = currEmitted - prevEmitted; // Guard counter resets or no emitted frames if (deltaDelay < 0 || deltaEmitted <= 0) { return { date: d.date, stat: null }; } const valueMs = Math.round((deltaDelay / deltaEmitted) * 1000); return { date: d.date, stat: valueMs }; }); // Rolling average over the last N seconds for the reference line const rollingWindowSeconds = 20; const recent = jitterBufferAvgDelayData .slice(-rollingWindowSeconds) .filter(x => x.stat != null) as { date: number; stat: number }[]; const referenceValue = recent.length > 0 ? Math.round(recent.reduce((sum, x) => sum + (x.stat as number), 0) / recent.length) : undefined; return (
{sidebarView === "connection-stats" && (
{/* Connection Group */}
({ date: x.date, stat: x.stat ? Math.round((x.stat as number) * 1000) : null, })} domain={[0, 600]} unit=" ms" />
{/* Video Group */}
{/* RTP Jitter */} ({ date: x.date, stat: x.stat ? Math.round((x.stat as number) * 1000) : null, })} domain={[0, 10]} unit=" ms" /> {/* Playback Delay */} {/* Packets Lost */} {/* Frames Per Second */}
)}
); }