Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions src/lib/ClientMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any[]> = {};

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();
};
5 changes: 3 additions & 2 deletions src/modules/StreamingClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { TalkMessageStream } from '../types/TalkMessageStream';
import { TalkStreamInterruptedSignalMessage } from '../types/signalling/TalkStreamInterruptedSignalMessage';
import {
ClientMetricMeasurement,
createRTCStatsReport,
sendClientMetric,
} from '../lib/ClientMetrics';

Expand Down Expand Up @@ -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
Expand Down