feat: enhance call connection quality metrics with detailed statistics display; fix: package-lock

This commit is contained in:
Marco Beretta 2025-04-05 09:48:33 +02:00
parent 25bd556933
commit 20a2a20a6b
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
3 changed files with 113 additions and 6164 deletions

View file

@ -9,6 +9,9 @@ import {
Volume2,
VolumeX,
Activity,
ChevronDown,
ChevronUp,
Wifi,
} from 'lucide-react';
import { OGDialog, OGDialogContent, Button } from '~/components';
import { useWebSocket, useCall } from '~/hooks';
@ -26,6 +29,7 @@ export const Call: React.FC = () => {
localStream,
remoteStream,
connectionQuality,
connectionMetrics,
isMuted,
toggleMute,
} = useCall();
@ -33,6 +37,7 @@ export const Call: React.FC = () => {
const [open, setOpen] = useRecoilState(store.callDialogOpen(0));
const [eventLog, setEventLog] = React.useState<string[]>([]);
const [isAudioEnabled, setIsAudioEnabled] = React.useState(true);
const [showMetrics, setShowMetrics] = React.useState(false);
const remoteAudioRef = useRef<HTMLAudioElement>(null);
@ -101,6 +106,38 @@ export const Call: React.FC = () => {
const isActive = callState === CallState.ACTIVE;
const isError = callState === CallState.ERROR;
const getQualityColor = (quality: string) => {
switch (quality) {
case 'excellent':
return 'bg-emerald-100 text-emerald-700';
case 'good':
return 'bg-green-100 text-green-700';
case 'fair':
return 'bg-yellow-100 text-yellow-700';
case 'poor':
return 'bg-orange-100 text-orange-700';
case 'bad':
return 'bg-red-100 text-red-700';
default:
return 'bg-gray-100 text-gray-700';
}
};
const getQualityIcon = (quality: string) => {
switch (quality) {
case 'excellent':
case 'good':
return <Wifi size={16} />;
case 'fair':
case 'poor':
return <Wifi size={16} className="opacity-75" />;
case 'bad':
return <Wifi size={16} className="opacity-50" />;
default:
return <Activity size={16} />;
}
};
// TESTS
useEffect(() => {
@ -152,18 +189,41 @@ export const Call: React.FC = () => {
{isActive && (
<div
className={`flex items-center gap-2 rounded-full px-4 py-2 ${
(connectionQuality === 'good' && 'bg-green-100 text-green-700') ||
(connectionQuality === 'poor' && 'bg-yellow-100 text-yellow-700') ||
'bg-gray-100 text-gray-700'
}`}
className={`flex items-center gap-2 rounded-full px-4 py-2 ${getQualityColor(connectionQuality)}`}
onClick={() => setShowMetrics(!showMetrics)}
style={{ cursor: 'pointer' }}
title="Click to show detailed metrics"
>
<Activity size={16} />
{getQualityIcon(connectionQuality)}
<span className="text-sm font-medium capitalize">{connectionQuality} Quality</span>
{showMetrics ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</div>
)}
</div>
{/* Quality Metrics Panel */}
{isActive && showMetrics && (
<div className="w-full rounded-md bg-surface-secondary p-3 text-sm shadow-inner">
<h4 className="mb-2 font-medium">Connection Metrics</h4>
<ul className="space-y-1 text-text-secondary">
<li className="flex justify-between">
<span>Round Trip Time:</span>
<span className="font-mono">{(connectionMetrics.rtt * 1000).toFixed(1)} ms</span>
</li>
<li className="flex justify-between">
<span>Packet Loss:</span>
<span className="font-mono">{connectionMetrics.packetsLost?.toFixed(2)}%</span>
</li>
<li className="flex justify-between">
<span>Jitter:</span>
<span className="font-mono">
{((connectionMetrics.jitter ?? 0) * 1000).toFixed(1)} ms
</span>
</li>
</ul>
</div>
)}
{/* Error Display */}
{error && (
<div className="flex w-full items-center gap-2 rounded-md bg-red-100 p-3 text-red-700">

View file

@ -21,7 +21,12 @@ interface CallStatus {
error: CallError | null;
localStream: MediaStream | null;
remoteStream: MediaStream | null;
connectionQuality: 'good' | 'poor' | 'unknown';
connectionQuality: 'excellent' | 'good' | 'fair' | 'poor' | 'bad' | 'unknown';
connectionMetrics: {
rtt?: number;
packetsLost?: number;
jitter?: number;
};
isUserSpeaking: boolean;
remoteAISpeaking: boolean;
}
@ -33,6 +38,7 @@ const INITIAL_STATUS: CallStatus = {
localStream: null,
remoteStream: null,
connectionQuality: 'unknown',
connectionMetrics: {},
isUserSpeaking: false,
remoteAISpeaking: false,
};
@ -133,18 +139,56 @@ const useCall = () => {
}
let totalRoundTripTime = 0;
let totalPacketsLost = 0;
let totalPackets = 0;
let totalJitter = 0;
let samplesCount = 0;
let samplesJitterCount = 0;
stats.forEach((report) => {
if (report.type === 'candidate-pair' && report.currentRoundTripTime) {
totalRoundTripTime += report.currentRoundTripTime;
samplesCount++;
}
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
if (report.packetsLost !== undefined && report.packetsReceived !== undefined) {
totalPacketsLost += report.packetsLost;
totalPackets += report.packetsReceived + report.packetsLost;
}
if (report.jitter !== undefined) {
totalJitter += report.jitter;
samplesJitterCount++;
}
}
});
const averageRTT = samplesCount > 0 ? totalRoundTripTime / samplesCount : 0;
const packetLossRate = totalPackets > 0 ? (totalPacketsLost / totalPackets) * 100 : 0;
const averageJitter = samplesJitterCount > 0 ? totalJitter / samplesJitterCount : 0;
let quality: CallStatus['connectionQuality'] = 'unknown';
if (averageRTT < 0.15 && packetLossRate < 0.5 && averageJitter < 0.015) {
quality = 'excellent';
} else if (averageRTT < 0.25 && packetLossRate < 2 && averageJitter < 0.025) {
quality = 'good';
} else if (averageRTT < 0.4 && packetLossRate < 5 && averageJitter < 0.04) {
quality = 'fair';
} else if (averageRTT < 0.6 && packetLossRate < 10 && averageJitter < 0.06) {
quality = 'poor';
} else if (averageRTT >= 0.6 || packetLossRate >= 10 || averageJitter >= 0.06) {
quality = 'bad';
}
updateStatus({
connectionQuality: averageRTT < 0.3 ? 'good' : 'poor',
connectionQuality: quality,
connectionMetrics: {
rtt: averageRTT,
packetsLost: packetLossRate,
jitter: averageJitter,
},
});
}, 2000);
}, [updateStatus]);

6157
package-lock.json generated

File diff suppressed because it is too large Load diff