mirror of https://github.com/jetkvm/kvm.git
feat: show audio level metrics in connection stats sidebar
This commit is contained in:
parent
c67b93578f
commit
d68dc4eee8
|
|
@ -208,6 +208,8 @@
|
|||
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
|
||||
"connection_stats_frames_per_second": "Frames per second",
|
||||
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
|
||||
"connection_stats_audio_level": "Audio Level",
|
||||
"connection_stats_audio_level_description": "The level of the audio stream from the JetKVM to the client.",
|
||||
"connection_stats_network_stability": "Network Stability",
|
||||
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
|
||||
"connection_stats_packets_lost": "Packets Lost",
|
||||
|
|
@ -220,8 +222,11 @@
|
|||
"connection_stats_unit_frames_per_second": " fps",
|
||||
"connection_stats_unit_milliseconds": " ms",
|
||||
"connection_stats_unit_packets": " packets",
|
||||
"connection_stats_unit_decibels": " dB",
|
||||
"connection_stats_video": "Video",
|
||||
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
|
||||
"connection_stats_audio": "Audio",
|
||||
"connection_stats_audio_description": "The audio stream from the JetKVM to the client.",
|
||||
"continue": "Continue",
|
||||
"creating_peer_connection": "Creating peer connection…",
|
||||
"dc_power_control_current": "Current",
|
||||
|
|
|
|||
|
|
@ -13,62 +13,15 @@ import { Button } from "@components/Button";
|
|||
import { useCopyToClipboard } from "@components/useCopyToClipBoard";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
export default function ConnectionStatsSidebar() {
|
||||
const { sidebarView, setSidebarView } = useUiStore();
|
||||
const {
|
||||
mediaStream,
|
||||
peerConnection,
|
||||
inboundRtpStats: inboundVideoRtpStats,
|
||||
appendInboundRtpStats: appendInboundVideoRtpStats,
|
||||
candidatePairStats: iceCandidatePairStats,
|
||||
appendCandidatePairStats,
|
||||
appendLocalCandidateStats,
|
||||
appendRemoteCandidateStats,
|
||||
appendDiskDataChannelStats,
|
||||
} = useRTCStore();
|
||||
|
||||
const [remoteIPAddress, setRemoteIPAddress] = useState<string | null>(null);
|
||||
|
||||
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" && report.kind === "video") {
|
||||
appendInboundVideoRtpStats(report);
|
||||
} else if (report.type === "candidate-pair" && report.nominated) {
|
||||
if (report.state === "succeeded") {
|
||||
successfulLocalCandidateId = report.localCandidateId;
|
||||
successfulRemoteCandidateId = report.remoteCandidateId;
|
||||
interface RtpStatChartsProps {
|
||||
inboundRtpStats: Map<number, RTCInboundRtpStreamStats>;
|
||||
showFramesPerSecond?: boolean;
|
||||
showAudioLevel?: boolean;
|
||||
}
|
||||
appendCandidatePairStats(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);
|
||||
setRemoteIPAddress(report.address);
|
||||
}
|
||||
} else if (report.type === "data-channel" && report.label === "disk") {
|
||||
appendDiskDataChannelStats(report);
|
||||
}
|
||||
});
|
||||
})();
|
||||
}, 500);
|
||||
|
||||
const jitterBufferDelay = createChartArray(inboundVideoRtpStats, "jitterBufferDelay");
|
||||
function RtpStatCharts({ inboundRtpStats, showFramesPerSecond, showAudioLevel }: RtpStatChartsProps) {
|
||||
const jitterBufferDelay = createChartArray(inboundRtpStats, "jitterBufferDelay");
|
||||
const jitterBufferEmittedCount = createChartArray(
|
||||
inboundVideoRtpStats,
|
||||
inboundRtpStats,
|
||||
"jitterBufferEmittedCount",
|
||||
);
|
||||
|
||||
|
|
@ -102,6 +55,137 @@ export default function ConnectionStatsSidebar() {
|
|||
return { date: d.date, metric: valueMs };
|
||||
});
|
||||
|
||||
return (<>
|
||||
{/* RTP Jitter */}
|
||||
<Metric
|
||||
title={m.connection_stats_network_stability()}
|
||||
badge={m.connection_stats_badge_jitter()}
|
||||
badgeTheme="light"
|
||||
description={m.connection_stats_network_stability_description()}
|
||||
stream={inboundRtpStats}
|
||||
metric="jitter"
|
||||
map={x => ({
|
||||
date: x.date,
|
||||
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
||||
})}
|
||||
domain={[0, 10]}
|
||||
unit={m.connection_stats_unit_milliseconds()}
|
||||
/>
|
||||
|
||||
{/* Playback Delay */}
|
||||
<Metric
|
||||
title={m.connection_stats_playback_delay()}
|
||||
description={m.connection_stats_playback_delay_description()}
|
||||
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
|
||||
badgeTheme="light"
|
||||
data={jitterBufferAvgDelayData}
|
||||
gate={inboundRtpStats}
|
||||
supported={
|
||||
someIterable(
|
||||
inboundRtpStats,
|
||||
([, x]) => x.jitterBufferDelay != null,
|
||||
) &&
|
||||
someIterable(
|
||||
inboundRtpStats,
|
||||
([, x]) => x.jitterBufferEmittedCount != null,
|
||||
)
|
||||
}
|
||||
domain={[0, 30]}
|
||||
unit={m.connection_stats_unit_milliseconds()}
|
||||
/>
|
||||
|
||||
{/* Packets Lost */}
|
||||
<Metric
|
||||
title={m.connection_stats_packets_lost()}
|
||||
description={m.connection_stats_packets_lost_description()}
|
||||
stream={inboundRtpStats}
|
||||
metric="packetsLost"
|
||||
domain={[0, 100]}
|
||||
unit={m.connection_stats_unit_packets()}
|
||||
/>
|
||||
|
||||
{/* Frames Per Second */}
|
||||
{showFramesPerSecond && <Metric
|
||||
title={m.connection_stats_frames_per_second()}
|
||||
description={m.connection_stats_frames_per_second_description()}
|
||||
stream={inboundRtpStats}
|
||||
metric="framesPerSecond"
|
||||
domain={[0, 80]}
|
||||
unit={m.connection_stats_unit_frames_per_second()}
|
||||
/>}
|
||||
|
||||
{showAudioLevel && <Metric
|
||||
title={m.connection_stats_audio_level()}
|
||||
description={m.connection_stats_audio_level_description()}
|
||||
stream={inboundRtpStats}
|
||||
metric="audioLevel"
|
||||
domain={[0, 1]}
|
||||
unit={m.connection_stats_unit_decibels()}
|
||||
/>}
|
||||
</>);
|
||||
}
|
||||
|
||||
export default function ConnectionStatsSidebar() {
|
||||
const { sidebarView, setSidebarView } = useUiStore();
|
||||
const {
|
||||
mediaStream,
|
||||
peerConnection,
|
||||
inboundVideoRtpStats,
|
||||
appendInboundVideoRtpStats,
|
||||
inboundAudioRtpStats,
|
||||
appendInboundAudioRtpStats,
|
||||
candidatePairStats: iceCandidatePairStats,
|
||||
appendCandidatePairStats,
|
||||
appendLocalCandidateStats,
|
||||
appendRemoteCandidateStats,
|
||||
appendDiskDataChannelStats,
|
||||
} = useRTCStore();
|
||||
|
||||
const [remoteIPAddress, setRemoteIPAddress] = useState<string | null>(null);
|
||||
|
||||
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") {
|
||||
if (report.kind === "video") {
|
||||
appendInboundVideoRtpStats(report);
|
||||
} else if (report.kind === "audio") {
|
||||
appendInboundAudioRtpStats(report);
|
||||
}
|
||||
} else if (report.type === "candidate-pair" && report.nominated) {
|
||||
if (report.state === "succeeded") {
|
||||
successfulLocalCandidateId = report.localCandidateId;
|
||||
successfulRemoteCandidateId = report.remoteCandidateId;
|
||||
}
|
||||
appendCandidatePairStats(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);
|
||||
setRemoteIPAddress(report.address);
|
||||
}
|
||||
} else if (report.type === "data-channel" && report.label === "disk") {
|
||||
appendDiskDataChannelStats(report);
|
||||
}
|
||||
});
|
||||
})();
|
||||
}, 500);
|
||||
|
||||
|
||||
|
||||
const { copy } = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
|
|
@ -159,63 +243,16 @@ export default function ConnectionStatsSidebar() {
|
|||
description={m.connection_stats_video_description()}
|
||||
/>
|
||||
|
||||
{/* RTP Jitter */}
|
||||
<Metric
|
||||
title={m.connection_stats_network_stability()}
|
||||
badge={m.connection_stats_badge_jitter()}
|
||||
badgeTheme="light"
|
||||
description={m.connection_stats_network_stability_description()}
|
||||
stream={inboundVideoRtpStats}
|
||||
metric="jitter"
|
||||
map={x => ({
|
||||
date: x.date,
|
||||
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
||||
})}
|
||||
domain={[0, 10]}
|
||||
unit={m.connection_stats_unit_milliseconds()}
|
||||
/>
|
||||
<RtpStatCharts inboundRtpStats={inboundVideoRtpStats} showFramesPerSecond />
|
||||
</div>
|
||||
|
||||
{/* Playback Delay */}
|
||||
<Metric
|
||||
title={m.connection_stats_playback_delay()}
|
||||
description={m.connection_stats_playback_delay_description()}
|
||||
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
|
||||
badgeTheme="light"
|
||||
data={jitterBufferAvgDelayData}
|
||||
gate={inboundVideoRtpStats}
|
||||
supported={
|
||||
someIterable(
|
||||
inboundVideoRtpStats,
|
||||
([, x]) => x.jitterBufferDelay != null,
|
||||
) &&
|
||||
someIterable(
|
||||
inboundVideoRtpStats,
|
||||
([, x]) => x.jitterBufferEmittedCount != null,
|
||||
)
|
||||
}
|
||||
domain={[0, 30]}
|
||||
unit={m.connection_stats_unit_milliseconds()}
|
||||
/>
|
||||
|
||||
{/* Packets Lost */}
|
||||
<Metric
|
||||
title={m.connection_stats_packets_lost()}
|
||||
description={m.connection_stats_packets_lost_description()}
|
||||
stream={inboundVideoRtpStats}
|
||||
metric="packetsLost"
|
||||
domain={[0, 100]}
|
||||
unit={m.connection_stats_unit_packets()}
|
||||
/>
|
||||
|
||||
{/* Frames Per Second */}
|
||||
<Metric
|
||||
title={m.connection_stats_frames_per_second()}
|
||||
description={m.connection_stats_frames_per_second_description()}
|
||||
stream={inboundVideoRtpStats}
|
||||
metric="framesPerSecond"
|
||||
domain={[0, 80]}
|
||||
unit={m.connection_stats_unit_frames_per_second()}
|
||||
{/* Audio Group */}
|
||||
<div className="space-y-3">
|
||||
<SettingsSectionHeader
|
||||
title={m.connection_stats_audio()}
|
||||
description={m.connection_stats_audio_description()}
|
||||
/>
|
||||
<RtpStatCharts inboundRtpStats={inboundAudioRtpStats} showAudioLevel />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -153,8 +153,10 @@ export interface RTCState {
|
|||
isTurnServerInUse: boolean;
|
||||
setTurnServerInUse: (inUse: boolean) => void;
|
||||
|
||||
inboundRtpStats: Map<number, RTCInboundRtpStreamStats>;
|
||||
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => void;
|
||||
inboundVideoRtpStats: Map<number, RTCInboundRtpStreamStats>;
|
||||
appendInboundVideoRtpStats: (stats: RTCInboundRtpStreamStats) => void;
|
||||
inboundAudioRtpStats: Map<number, RTCInboundRtpStreamStats>;
|
||||
appendInboundAudioRtpStats: (stats: RTCInboundRtpStreamStats) => void;
|
||||
clearInboundRtpStats: () => void;
|
||||
|
||||
candidatePairStats: Map<number, RTCIceCandidatePairStats>;
|
||||
|
|
@ -218,13 +220,19 @@ export const useRTCStore = create<RTCState>(set => ({
|
|||
isTurnServerInUse: false,
|
||||
setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }),
|
||||
|
||||
inboundRtpStats: new Map(),
|
||||
appendInboundRtpStats: stats => {
|
||||
inboundVideoRtpStats: new Map(),
|
||||
appendInboundVideoRtpStats: stats => {
|
||||
set(prevState => ({
|
||||
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
||||
inboundVideoRtpStats: appendStatToMap(stats, prevState.inboundVideoRtpStats),
|
||||
}));
|
||||
},
|
||||
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
||||
inboundAudioRtpStats: new Map(),
|
||||
appendInboundAudioRtpStats: stats => {
|
||||
set(prevState => ({
|
||||
inboundAudioRtpStats: appendStatToMap(stats, prevState.inboundAudioRtpStats),
|
||||
}));
|
||||
},
|
||||
clearInboundRtpStats: () => set({ inboundVideoRtpStats: new Map(), inboundAudioRtpStats: new Map() }),
|
||||
|
||||
candidatePairStats: new Map(),
|
||||
appendCandidatePairStats: stats => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue