From 7dfc7e271dd37276947478aa1f115fb7e73ee0b3 Mon Sep 17 00:00:00 2001 From: ao-anam Date: Mon, 30 Jun 2025 12:54:01 +0100 Subject: [PATCH] feat: build improved rtc stats report --- src/lib/ClientMetrics.ts | 216 +++++++++++++++++++++++++++++++++ src/modules/StreamingClient.ts | 5 +- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/src/lib/ClientMetrics.ts b/src/lib/ClientMetrics.ts index 1097195..e271113 100644 --- a/src/lib/ClientMetrics.ts +++ b/src/lib/ClientMetrics.ts @@ -78,3 +78,219 @@ export const sendClientMetric = async ( console.error('Failed to send error metric:', error); } }; + +export const createRTCStatsReport = (stats: RTCStatsReport) => { + /** + * constructs a report of the RTC stats for logging to the console + */ + console.group('📊 WebRTC Session Statistics Report'); + + // Collect stats by type for organized reporting + const statsByType: Record = {}; + + stats.forEach((report) => { + if (!statsByType[report.type]) { + statsByType[report.type] = []; + } + statsByType[report.type].push(report); + }); + + // Report connection overview + if (statsByType['peer-connection']) { + console.group('🔗 Connection Overview'); + statsByType['peer-connection'].forEach((report) => { + console.log(`Connection State: ${report.connectionState || 'unknown'}`); + console.log(`Data Channels Opened: ${report.dataChannelsOpened || 0}`); + console.log(`Data Channels Closed: ${report.dataChannelsClosed || 0}`); + }); + console.groupEnd(); + } + + // Report video statistics (AI video output) + const inboundVideo = + statsByType['inbound-rtp']?.filter((r) => r.kind === 'video') || []; + if (inboundVideo.length > 0) { + console.group('📹 AI Video Stream (Inbound)'); + inboundVideo.forEach((report) => { + console.log(`Frames Received: ${report.framesReceived || 0}`); + console.log(`Frames Dropped: ${report.framesDropped || 0}`); + console.log(`Frames Per Second: ${report.framesPerSecond || 0}`); + console.log( + `Bytes Received: ${(report.bytesReceived || 0).toLocaleString()}`, + ); + console.log( + `Packets Received: ${(report.packetsReceived || 0).toLocaleString()}`, + ); + console.log(`Packets Lost: ${report.packetsLost || 0}`); + if (report.frameWidth && report.frameHeight) { + console.log(`Resolution: ${report.frameWidth}x${report.frameHeight}`); + } + if (report.jitter !== undefined) { + console.log(`Jitter: ${report.jitter.toFixed(3)}ms`); + } + }); + console.groupEnd(); + } + + // Report audio statistics (AI audio output) + const inboundAudio = + statsByType['inbound-rtp']?.filter((r) => r.kind === 'audio') || []; + if (inboundAudio.length > 0) { + console.group('🔊 AI Audio Stream (Inbound)'); + inboundAudio.forEach((report) => { + console.log( + `Bytes Received: ${(report.bytesReceived || 0).toLocaleString()}`, + ); + console.log( + `Packets Received: ${(report.packetsReceived || 0).toLocaleString()}`, + ); + console.log(`Packets Lost: ${report.packetsLost || 0}`); + console.log(`Audio Level: ${report.audioLevel || 0}`); + if (report.jitter !== undefined) { + console.log(`Jitter: ${report.jitter.toFixed(3)}ms`); + } + if (report.totalAudioEnergy !== undefined) { + console.log( + `Total Audio Energy: ${report.totalAudioEnergy.toFixed(6)}`, + ); + } + }); + console.groupEnd(); + } + + // Report user audio input statistics + const outboundAudio = + statsByType['outbound-rtp']?.filter((r) => r.kind === 'audio') || []; + if (outboundAudio.length > 0) { + console.group('🎤 User Audio Input (Outbound)'); + outboundAudio.forEach((report) => { + console.log(`Bytes Sent: ${(report.bytesSent || 0).toLocaleString()}`); + console.log( + `Packets Sent: ${(report.packetsSent || 0).toLocaleString()}`, + ); + if (report.retransmittedPacketsSent) { + console.log( + `Retransmitted Packets: ${report.retransmittedPacketsSent}`, + ); + } + if (report.totalPacketSendDelay !== undefined) { + console.log( + `Avg Packet Send Delay: ${((report.totalPacketSendDelay / (report.packetsSent || 1)) * 1000).toFixed(2)}ms`, + ); + } + }); + console.groupEnd(); + } + + // Report codec information + if (statsByType['codec']) { + console.group('🔧 Codecs Used'); + statsByType['codec'].forEach((report) => { + const direction = report.payloadType ? 'Active' : 'Available'; + console.log( + `${direction} ${report.mimeType || 'Unknown'} - Payload Type: ${report.payloadType || 'N/A'}`, + ); + if (report.clockRate) { + console.log(` Clock Rate: ${report.clockRate}Hz`); + } + if (report.channels) { + console.log(` Channels: ${report.channels}`); + } + }); + console.groupEnd(); + } + + // Report connection quality metrics + const candidatePairs = + statsByType['candidate-pair']?.filter((r) => r.state === 'succeeded') || []; + if (candidatePairs.length > 0) { + console.group('📡 Connection Quality'); + candidatePairs.forEach((report) => { + console.log( + `Connection Type: ${report.localCandidateType || 'unknown'} → ${report.remoteCandidateType || 'unknown'}`, + ); + if (report.currentRoundTripTime !== undefined) { + console.log( + `Round Trip Time: ${(report.currentRoundTripTime * 1000).toFixed(1)}ms`, + ); + } + if (report.availableOutgoingBitrate !== undefined) { + console.log( + `Available Outgoing Bitrate: ${Math.round(report.availableOutgoingBitrate / 1000)}kbps`, + ); + } + if (report.availableIncomingBitrate !== undefined) { + console.log( + `Available Incoming Bitrate: ${Math.round(report.availableIncomingBitrate / 1000)}kbps`, + ); + } + console.log(`Bytes Sent: ${(report.bytesSent || 0).toLocaleString()}`); + console.log( + `Bytes Received: ${(report.bytesReceived || 0).toLocaleString()}`, + ); + }); + console.groupEnd(); + } + + // Report any transport issues + if (statsByType['transport']) { + console.group('🚚 Transport Layer'); + statsByType['transport'].forEach((report) => { + console.log(`DTLS State: ${report.dtlsState || 'unknown'}`); + console.log(`ICE State: ${report.iceState || 'unknown'}`); + if (report.bytesReceived || report.bytesSent) { + console.log( + `Data Transfer - Sent: ${(report.bytesSent || 0).toLocaleString()}, Received: ${(report.bytesReceived || 0).toLocaleString()}`, + ); + } + }); + console.groupEnd(); + } + + // Summary of potential issues + const issues: string[] = []; + + // Check for video issues + inboundVideo.forEach((report) => { + if (report.framesDropped > 0) { + issues.push(`Video: ${report.framesDropped} frames dropped`); + } + if (report.packetsLost > 0) { + issues.push(`Video: ${report.packetsLost} packets lost`); + } + if (report.framesPerSecond < 15) { + issues.push(`Video: Low frame rate (${report.framesPerSecond} fps)`); + } + }); + + // Check for audio issues + inboundAudio.forEach((report) => { + if (report.packetsLost > 0) { + issues.push(`Audio: ${report.packetsLost} packets lost`); + } + if (report.jitter > 0.1) { + issues.push( + `Audio: High jitter (${(report.jitter * 1000).toFixed(1)}ms)`, + ); + } + }); + + // Check for connection issues + candidatePairs.forEach((report) => { + if (report.currentRoundTripTime > 0.5) { + issues.push( + `Connection: High latency (${(report.currentRoundTripTime * 1000).toFixed(1)}ms RTT)`, + ); + } + }); + + if (issues.length > 0) { + console.group('⚠️ Potential Issues Detected'); + issues.forEach((issue) => console.warn(issue)); + console.groupEnd(); + } else { + console.log('✅ No significant issues detected'); + } + + console.groupEnd(); +}; diff --git a/src/modules/StreamingClient.ts b/src/modules/StreamingClient.ts index d40e543..a8a3564 100644 --- a/src/modules/StreamingClient.ts +++ b/src/modules/StreamingClient.ts @@ -22,6 +22,7 @@ import { TalkMessageStream } from '../types/TalkMessageStream'; import { TalkStreamInterruptedSignalMessage } from '../types/signalling/TalkStreamInterruptedSignalMessage'; import { ClientMetricMeasurement, + createRTCStatsReport, sendClientMetric, } from '../lib/ClientMetrics'; @@ -563,8 +564,8 @@ export class StreamingClient { private shutdown() { if (this.showPeerConnectionStatsReport) { - this.peerConnection?.getStats().then((stats) => { - console.log('StreamingClient - shutdown: peer connection stats', stats); + this.peerConnection?.getStats().then((stats: RTCStatsReport) => { + createRTCStatsReport(stats); }); } // reset video frame polling