Skip to content

Commit 0f4df3c

Browse files
authored
feat(react): Extend the statistics report with audio stats (#2020)
### 💡 Overview Adds audio performance metrics to the existing call statistics report in the client package. Extends CallStatsReport with audio stats (latency, jitter, packet loss, bitrate, codec) for publisher and subscriber. 🎫 Ticket: https://linear.app/stream/issue/REACT-181/extend-the-callstatsreport-with-audio-stats 📑 Docs: GetStream/docs-content#797
1 parent b06e130 commit 0f4df3c

File tree

4 files changed

+267
-17
lines changed

4 files changed

+267
-17
lines changed

packages/client/src/devices/DeviceManager.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ export abstract class DeviceManager<
258258
};
259259

260260
protected async applySettingsToStream() {
261-
console.log('applySettingsToStream ');
262261
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
263262
if (this.enabled) {
264263
try {

packages/client/src/stats/CallStateStatsReporter.ts

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
AggregatedStatsReport,
3+
AudioAggregatedStats,
34
BaseStats,
45
ParticipantsStatsReport,
56
RTCCodecStats,
@@ -158,20 +159,32 @@ export const createStatsReporter = ({
158159
publisher ? getRawStatsForTrack('publisher') : undefined,
159160
]);
160161

161-
const process = (report: RTCStatsReport, kind: PeerConnectionKind) =>
162-
aggregate(transform(report, { kind, trackKind: 'video', publisher }));
162+
const process = (report: RTCStatsReport, kind: PeerConnectionKind) => {
163+
const videoStats = aggregate(
164+
transform(report, { kind, trackKind: 'video', publisher }),
165+
);
166+
const audioStats = aggregateAudio(
167+
transform(report, { kind, trackKind: 'audio', publisher }),
168+
);
169+
return {
170+
videoStats,
171+
audioStats,
172+
};
173+
};
163174

164-
const subscriberStats = subscriberRawStats
175+
const subscriberResult = subscriberRawStats
165176
? process(subscriberRawStats, 'subscriber')
166-
: getEmptyStats();
167-
const publisherStats = publisherRawStats
177+
: { videoStats: getEmptyVideoStats(), audioStats: getEmptyAudioStats() };
178+
const publisherResult = publisherRawStats
168179
? process(publisherRawStats, 'publisher')
169-
: getEmptyStats();
180+
: { videoStats: getEmptyVideoStats(), audioStats: getEmptyAudioStats() };
170181

171182
state.setCallStatsReport({
172183
datacenter,
173-
publisherStats,
174-
subscriberStats,
184+
publisherStats: publisherResult.videoStats,
185+
publisherAudioStats: publisherResult.audioStats,
186+
subscriberStats: subscriberResult.videoStats,
187+
subscriberAudioStats: subscriberResult.audioStats,
175188
subscriberRawStats,
176189
publisherRawStats,
177190
participants: participantStats,
@@ -266,6 +279,11 @@ const transform = (
266279
}
267280

268281
let trackType: TrackType | undefined;
282+
let audioLevel: number | undefined;
283+
let concealedSamples: number | undefined;
284+
let concealmentEvents: number | undefined;
285+
let packetsReceived: number | undefined;
286+
let packetsLost: number | undefined;
269287
if (kind === 'publisher' && publisher) {
270288
const firefox = isFirefox();
271289
const mediaSource = stats.find(
@@ -276,7 +294,23 @@ const transform = (
276294
) as RTCMediaSourceStats | undefined;
277295
if (mediaSource) {
278296
trackType = publisher.getTrackType(mediaSource.trackIdentifier);
297+
if (
298+
trackKind === 'audio' &&
299+
typeof mediaSource.audioLevel === 'number'
300+
) {
301+
audioLevel = mediaSource.audioLevel;
302+
}
303+
}
304+
} else if (kind === 'subscriber' && trackKind === 'audio') {
305+
const inboundStats = rtcStreamStats as RTCInboundRtpStreamStats;
306+
const inboundLevel = inboundStats.audioLevel;
307+
if (typeof inboundLevel === 'number') {
308+
audioLevel = inboundLevel;
279309
}
310+
concealedSamples = inboundStats.concealedSamples;
311+
concealmentEvents = inboundStats.concealmentEvents;
312+
packetsReceived = inboundStats.packetsReceived;
313+
packetsLost = inboundStats.packetsLost;
280314
}
281315

282316
return {
@@ -294,6 +328,11 @@ const transform = (
294328
rid: rtcStreamStats.rid,
295329
ssrc: rtcStreamStats.ssrc,
296330
trackType,
331+
audioLevel,
332+
concealedSamples,
333+
concealmentEvents,
334+
packetsReceived,
335+
packetsLost,
297336
};
298337
});
299338

@@ -304,7 +343,7 @@ const transform = (
304343
};
305344
};
306345

307-
const getEmptyStats = (stats?: StatsReport): AggregatedStatsReport => {
346+
const getEmptyVideoStats = (stats?: StatsReport): AggregatedStatsReport => {
308347
return {
309348
rawReport: stats ?? { streams: [], timestamp: Date.now() },
310349
totalBytesSent: 0,
@@ -321,13 +360,29 @@ const getEmptyStats = (stats?: StatsReport): AggregatedStatsReport => {
321360
};
322361
};
323362

363+
const getEmptyAudioStats = (): AudioAggregatedStats => {
364+
return {
365+
totalBytesSent: 0,
366+
totalBytesReceived: 0,
367+
averageJitterInMs: 0,
368+
averageRoundTripTimeInMs: 0,
369+
codec: '',
370+
codecPerTrackType: {},
371+
timestamp: Date.now(),
372+
totalConcealedSamples: 0,
373+
totalConcealmentEvents: 0,
374+
totalPacketsReceived: 0,
375+
totalPacketsLost: 0,
376+
};
377+
};
378+
324379
/**
325380
* Aggregates generic stats.
326381
*
327382
* @param stats the stats to aggregate.
328383
*/
329384
const aggregate = (stats: StatsReport): AggregatedStatsReport => {
330-
const aggregatedStats = getEmptyStats(stats);
385+
const aggregatedStats = getEmptyVideoStats(stats);
331386

332387
let maxArea = -1;
333388
const area = (w: number, h: number) => w * h;
@@ -386,3 +441,49 @@ const aggregate = (stats: StatsReport): AggregatedStatsReport => {
386441

387442
return report;
388443
};
444+
445+
/**
446+
* Aggregates audio stats from a stats report.
447+
*
448+
* @param stats the stats report containing audio streams.
449+
* @returns aggregated audio stats.
450+
*/
451+
const aggregateAudio = (stats: StatsReport): AudioAggregatedStats => {
452+
const streams = stats.streams;
453+
454+
const audioStats = getEmptyAudioStats();
455+
456+
const report = streams.reduce((acc, stream) => {
457+
acc.totalBytesSent += stream.bytesSent || 0;
458+
acc.totalBytesReceived += stream.bytesReceived || 0;
459+
acc.averageJitterInMs += stream.jitter || 0;
460+
acc.averageRoundTripTimeInMs += stream.currentRoundTripTime || 0;
461+
acc.totalConcealedSamples += stream.concealedSamples || 0;
462+
acc.totalConcealmentEvents += stream.concealmentEvents || 0;
463+
acc.totalPacketsReceived += stream.packetsReceived || 0;
464+
acc.totalPacketsLost += stream.packetsLost || 0;
465+
466+
return acc;
467+
}, audioStats);
468+
469+
if (streams.length > 0) {
470+
report.averageJitterInMs = Math.round(
471+
(report.averageJitterInMs / streams.length) * 1000,
472+
);
473+
report.averageRoundTripTimeInMs = Math.round(
474+
(report.averageRoundTripTimeInMs / streams.length) * 1000,
475+
);
476+
report.codec = streams[0].codec || '';
477+
report.codecPerTrackType = streams.reduce(
478+
(acc, stream) => {
479+
if (stream.trackType) {
480+
acc[stream.trackType] = stream.codec || '';
481+
}
482+
return acc;
483+
},
484+
{} as Record<TrackType, string>,
485+
);
486+
}
487+
488+
return report;
489+
};

packages/client/src/stats/types.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export type BaseStats = {
1616
rid?: string;
1717
ssrc?: number;
1818
trackType?: TrackType;
19+
concealedSamples?: number;
20+
concealmentEvents?: number;
21+
packetsReceived?: number;
22+
packetsLost?: number;
1923
};
2024

2125
export type StatsReport = {
@@ -24,6 +28,20 @@ export type StatsReport = {
2428
timestamp: number;
2529
};
2630

31+
export type AudioAggregatedStats = {
32+
totalBytesSent: number;
33+
totalBytesReceived: number;
34+
averageJitterInMs: number;
35+
averageRoundTripTimeInMs: number;
36+
codec: string;
37+
codecPerTrackType: Partial<Record<TrackType, string>>;
38+
timestamp: number;
39+
totalConcealedSamples: number;
40+
totalConcealmentEvents: number;
41+
totalPacketsReceived: number;
42+
totalPacketsLost: number;
43+
};
44+
2745
export type AggregatedStatsReport = {
2846
totalBytesSent: number;
2947
totalBytesReceived: number;
@@ -50,18 +68,30 @@ export type CallStatsReport = {
5068
*/
5169
datacenter: string;
5270
/**
53-
* Aggregated stats for the publisher, which is the local participant.
71+
* Aggregated video stats for the publisher, which is the local participant.
72+
* Note: For audio stats, see publisherAudioStats.
5473
*/
5574
publisherStats: AggregatedStatsReport;
75+
/**
76+
* Aggregated audio stats for the publisher, which is the local participant.
77+
* Includes bandwidth, latency, jitter, and codec information.
78+
*/
79+
publisherAudioStats: AudioAggregatedStats;
5680
/**
5781
* Raw stats for the publisher, which is the local participant.
5882
* Holds the raw RTCStatsReport object provided by the WebRTC API.
5983
*/
6084
publisherRawStats?: RTCStatsReport;
6185
/**
62-
* Aggregated stats for the subscribers, which are all remote participants.
86+
* Aggregated video stats for the subscribers, which are all remote participants.
87+
* Note: For audio stats, see subscriberAudioStats.
6388
*/
6489
subscriberStats: AggregatedStatsReport;
90+
/**
91+
* Aggregated audio stats for the subscribers, which are all remote participants.
92+
* Includes bandwidth, latency, jitter, and codec information.
93+
*/
94+
subscriberAudioStats: AudioAggregatedStats;
6595
/**
6696
* Raw stats for the subscribers, which are all remote participants.
6797
* Holds the raw RTCStatsReport object provided by the WebRTC API.
@@ -87,6 +117,7 @@ export interface RTCMediaSourceStats {
87117
timestamp: number;
88118
kind: string;
89119
trackIdentifier: string;
120+
audioLevel?: number;
90121
}
91122

92123
// shim for RTCCodecStats, not yet available in the standard types

0 commit comments

Comments
 (0)