Skip to content

Commit 4ff3293

Browse files
authored
feat: add video controls to main dashboard (#7)
* feat: add video controls to main dashboard * Apply suggestions from code review * fix: remove unnecessary buffer zone * fix: code review suggestions
1 parent 6280ae7 commit 4ff3293

15 files changed

Lines changed: 649 additions & 691 deletions

src/components/SrtStats.tsx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use client";
2+
3+
import React, { useEffect, useMemo, useState } from "react";
4+
import ROSLIB from "roslib";
5+
import { useROS } from "@/ros/ROSContext";
6+
7+
type SrtStatsMsg = {
8+
rtt: number; // seconds
9+
bandwidth: number; // bits/sec
10+
packets_sent: number;
11+
packets_lost: number;
12+
packets_retransmitted: number;
13+
};
14+
15+
const formatNumber = (v: number | null | undefined) => {
16+
if (v === null || v === undefined) return "—";
17+
return v.toLocaleString();
18+
};
19+
20+
const formatSecondsMs = (sec: number | null | undefined) => {
21+
if (sec === null || sec === undefined) return "—";
22+
const ms = sec * 1000.0;
23+
if (!Number.isFinite(ms)) return "—";
24+
return ms >= 10 ? `${ms.toFixed(0)} ms` : `${ms.toFixed(1)} ms`;
25+
};
26+
27+
const formatBandwidth = (bps: number | null | undefined) => {
28+
if (bps === null || bps === undefined) return "—";
29+
if (!Number.isFinite(bps)) return "—";
30+
const abs = Math.abs(bps);
31+
const units = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"];
32+
let i = 0;
33+
let val = abs;
34+
while (val >= 1000 && i < units.length - 1) {
35+
val /= 1000;
36+
i++;
37+
}
38+
const fixed = val >= 100 ? 0 : val >= 10 ? 1 : 2;
39+
return `${val.toFixed(fixed)} ${units[i]}`;
40+
};
41+
42+
const SrtStats: React.FC = () => {
43+
const { ros, connectionStatus } = useROS();
44+
45+
const [stats, setStats] = useState<SrtStatsMsg | null>(null);
46+
const [lastUpdateMs, setLastUpdateMs] = useState<number | null>(null);
47+
48+
useEffect(() => {
49+
if (!ros || connectionStatus !== "connected") {
50+
setStats(null);
51+
setLastUpdateMs(null);
52+
return;
53+
}
54+
55+
const topic = new ROSLIB.Topic({
56+
ros,
57+
name: "/srt_node/stats",
58+
messageType: "interfaces/msg/SrtStats",
59+
});
60+
61+
const onMsg = (msg: any) => {
62+
const newData: SrtStatsMsg = {
63+
rtt: msg.rtt,
64+
bandwidth: msg.bandwidth,
65+
packets_sent: msg.packets_sent,
66+
packets_lost: msg.packets_lost,
67+
packets_retransmitted: msg.packets_retransmitted,
68+
};
69+
setStats(newData);
70+
setLastUpdateMs(Date.now());
71+
};
72+
73+
topic.subscribe(onMsg);
74+
75+
return () => {
76+
topic.unsubscribe(onMsg);
77+
};
78+
}, [ros, connectionStatus]);
79+
80+
const derived = useMemo(() => {
81+
if (!stats) return { lossPct: null as number | null, retransPct: null as number | null };
82+
83+
const sent = stats.packets_sent;
84+
const lost = stats.packets_lost;
85+
const retrans = stats.packets_retransmitted;
86+
87+
const lossPct =
88+
Number.isFinite(sent) && sent > 0 && Number.isFinite(lost) ? (lost / sent) * 100 : null;
89+
90+
const retransPct =
91+
Number.isFinite(sent) && sent > 0 && Number.isFinite(retrans) ? (retrans / sent) * 100 : null;
92+
93+
return { lossPct, retransPct };
94+
}, [stats]);
95+
96+
return (
97+
<div
98+
style={{
99+
borderTop: "1px solid #444",
100+
marginTop: "0.75rem",
101+
paddingTop: "0.75rem",
102+
}}
103+
>
104+
<div
105+
style={{
106+
border: "1px solid #444",
107+
borderRadius: "6px",
108+
backgroundColor: "#2a2a2a",
109+
padding: "0.6rem",
110+
}}
111+
>
112+
<div
113+
style={{
114+
display: "flex",
115+
justifyContent: "space-between",
116+
alignItems: "baseline",
117+
borderBottom: "1px solid #444",
118+
paddingBottom: "0.3rem",
119+
flex: "0 0 auto",
120+
}}
121+
>
122+
<h2 style={{ margin: 0, fontSize: "1.1rem", color: "#f1f1f1" }}>
123+
SRT Stats:
124+
</h2>
125+
</div>
126+
<div
127+
style={{
128+
display: "grid",
129+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
130+
gap: "0.5rem",
131+
fontSize: "0.85rem",
132+
}}
133+
>
134+
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
135+
<span style={{ color: "#aaa" }}>RTT</span>
136+
<span style={{ color: "#f1f1f1" }}>{formatSecondsMs(stats?.rtt)}</span>
137+
</div>
138+
139+
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
140+
<span style={{ color: "#aaa" }}>Bandwidth</span>
141+
<span style={{ color: "#f1f1f1" }}>{formatBandwidth(stats?.bandwidth)}</span>
142+
</div>
143+
144+
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
145+
<span style={{ color: "#aaa" }}>Sent</span>
146+
<span style={{ color: "#f1f1f1" }}>{formatNumber(stats?.packets_sent)}</span>
147+
</div>
148+
149+
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
150+
<span style={{ color: "#aaa" }}>Lost</span>
151+
<span style={{ color: "#f1f1f1" }}>
152+
{formatNumber(stats?.packets_lost)}
153+
{derived.lossPct !== null ? ` (${derived.lossPct.toFixed(2)}%)` : ""}
154+
</span>
155+
</div>
156+
157+
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
158+
<span style={{ color: "#aaa" }}>Retrans</span>
159+
<span style={{ color: "#f1f1f1" }}>
160+
{formatNumber(stats?.packets_retransmitted)}
161+
{derived.retransPct !== null ? ` (${derived.retransPct.toFixed(2)}%)` : ""}
162+
</span>
163+
</div>
164+
165+
<div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
166+
<span style={{ color: "#aaa" }}>Updated</span>
167+
<span style={{ color: "#f1f1f1" }}>
168+
{lastUpdateMs ? `${Math.max(0, Date.now() - lastUpdateMs)} ms ago` : "—"}
169+
</span>
170+
</div>
171+
</div>
172+
</div>
173+
</div>
174+
);
175+
};
176+
177+
export default SrtStats;

0 commit comments

Comments
 (0)