import SidebarHeader from "@components/SidebarHeader"; import { GridCard } from "@components/Card"; import { useRTCStore, useUiStore } from "@/hooks/stores"; import StatChart from "@components/StatChart"; import { useInterval } from "usehooks-ts"; function createChartArray( stream: Map, metric: K, ): { date: number; stat: T[K] | null }[] { const stat = Array.from(stream).map(([key, stats]) => { return { date: key, stat: stats[metric] }; }); // Sort the dates to ensure they are in chronological order const sortedStat = stat.map(x => x.date).sort((a, b) => a - b); // Determine the earliest statistic date const earliestStat = sortedStat[0]; // Current time in seconds since the Unix epoch const now = Math.floor(Date.now() / 1000); // Determine the starting point for the chart data const firstChartDate = earliestStat ? Math.min(earliestStat, now - 120) : now - 120; // Generate the chart array for the range between 'firstChartDate' and 'now' return Array.from({ length: now - firstChartDate }, (_, i) => { const currentDate = firstChartDate + i; return { date: currentDate, // Find the statistic for 'currentDate', or use the last known statistic if none exists for that date stat: stat.find(x => x.date === currentDate)?.stat ?? null, }; }); } export default function ConnectionStatsSidebar() { const inboundRtpStats = useRTCStore(state => state.inboundRtpStats); const candidatePairStats = 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 appendInboundRtpStats = 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 videoTrack = mediaStream.getVideoTracks()[0]; if (!videoTrack) return; const stats = await peerConnection?.getStats(); let successfulLocalCandidateId: string | null = null; let successfulRemoteCandidateId: string | null = null; stats?.forEach(report => { if (report.type === "inbound-rtp") { appendInboundRtpStats(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); return (
{/* The entire sidebar component is always rendered, with a display none when not visible The charts below, need a height and width, otherwise they throw. So simply don't render them unless the thing is visible */} {sidebarView === "connection-stats" && (

Packets Lost

Number of data packets lost during transmission.

{inboundRtpStats.size === 0 ? (

Waiting for data...

) : isMetricSupported(inboundRtpStats, "packetsLost") ? ( ) : (

Metric not supported

)}

Round-Trip Time

Time taken for data to travel from source to destination and back

{inboundRtpStats.size === 0 ? (

Waiting for data...

) : isMetricSupported(candidatePairStats, "currentRoundTripTime") ? ( { return { date: x.date, stat: x.stat ? Math.round(x.stat * 1000) : null, }; })} domain={[0, 600]} unit=" ms" /> ) : (

Metric not supported

)}

Jitter

Variation in packet delay, affecting video smoothness.{" "}

{inboundRtpStats.size === 0 ? (

Waiting for data...

) : ( { return { date: x.date, stat: x.stat ? Math.round(x.stat * 1000) : null, }; })} domain={[0, 300]} unit=" ms" /> )}

Frames per second

Number of video frames displayed per second.

{inboundRtpStats.size === 0 ? (

Waiting for data...

) : ( { return { date: x.date, stat: x.stat ? x.stat : null, }; }, )} domain={[0, 80]} unit=" fps" /> )}
)}
); }