diff --git a/ui/src/components/sidebar/connectionStats.tsx b/ui/src/components/sidebar/connectionStats.tsx index 404deb1..f0988e0 100644 --- a/ui/src/components/sidebar/connectionStats.tsx +++ b/ui/src/components/sidebar/connectionStats.tsx @@ -1,45 +1,14 @@ import { useInterval } from "usehooks-ts"; import SidebarHeader from "@/components/SidebarHeader"; -import { GridCard } from "@/components/Card"; import { useRTCStore, useUiStore } from "@/hooks/stores"; -import StatChart from "@/components/StatChart"; -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, - }; - }); -} +import { createChartArray, Metric } from "../Metric"; +import { SettingsSectionHeader } from "../SettingsSectionHeader"; export default function ConnectionStatsSidebar() { - const inboundRtpStats = useRTCStore(state => state.inboundRtpStats); - - const candidatePairStats = useRTCStore(state => state.candidatePairStats); + const inboundVideoRtpStats = useRTCStore(state => state.inboundRtpStats); + const iceCandidatePairStats = useRTCStore(state => state.candidatePairStats); const setSidebarView = useUiStore(state => state.setSidebarView); function isMetricSupported( @@ -49,7 +18,7 @@ export default function ConnectionStatsSidebar() { return Array.from(stream).some(([, stat]) => stat[metric] !== undefined); } - const appendInboundRtpStats = useRTCStore(state => state.appendInboundRtpStats); + const appendInboundVideoRtpStats = useRTCStore(state => state.appendInboundRtpStats); const appendIceCandidatePair = useRTCStore(state => state.appendCandidatePairStats); const appendDiskDataChannelStats = useRTCStore( state => state.appendDiskDataChannelStats, @@ -66,15 +35,13 @@ export default function ConnectionStatsSidebar() { 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); + 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; @@ -98,144 +65,139 @@ export default function ConnectionStatsSidebar() { })(); }, 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 (
- {/* - 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

-
- )} -
-
+
+ {/* Connection Group */} +
+ + ({ + date: x.date, + stat: x.stat ? Math.round((x.stat as number) * 1000) : null, + })} + domain={[0, 600]} + unit=" ms" + />
-
-
-

- 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" - /> - )} -
-
+ + {/* 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 */} +
)}