diff --git a/spec/unit/webrtc/stats/statsReportBuilder.spec.ts b/spec/unit/webrtc/stats/statsReportBuilder.spec.ts index fd75c90546d..4b4faa1ca05 100644 --- a/spec/unit/webrtc/stats/statsReportBuilder.spec.ts +++ b/spec/unit/webrtc/stats/statsReportBuilder.spec.ts @@ -91,6 +91,13 @@ describe("StatsReportBuilder", () => { ["REMOTE_AUDIO_TRACK_ID", 0.1], ["REMOTE_VIDEO_TRACK_ID", 50], ]), + audioConcealment: new Map([ + ["REMOTE_AUDIO_TRACK_ID", { concealedAudio: 3000, totalAudioDuration: 3000 * 20 }], + ]), + totalAudioConcealment: { + concealedAudio: 3000, + totalAudioDuration: (1 / 0.05) * 3000, + }, }); }); }); @@ -104,6 +111,7 @@ describe("StatsReportBuilder", () => { remoteAudioTrack.setLoss({ packetsTotal: 20, packetsLost: 0, isDownloadStream: true }); remoteAudioTrack.setBitrate({ download: 4000, upload: 0 }); remoteAudioTrack.setJitter(0.1); + remoteAudioTrack.setAudioConcealment(3000, 3000 * 20); localVideoTrack.setCodec("v8"); localVideoTrack.setLoss({ packetsTotal: 30, packetsLost: 6, isDownloadStream: false }); diff --git a/spec/unit/webrtc/stats/statsReportGatherer.spec.ts b/spec/unit/webrtc/stats/statsReportGatherer.spec.ts index aad12b4a36a..0eb847ce01d 100644 --- a/spec/unit/webrtc/stats/statsReportGatherer.spec.ts +++ b/spec/unit/webrtc/stats/statsReportGatherer.spec.ts @@ -43,8 +43,22 @@ describe("StatsReportGatherer", () => { receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }); expect(collector.getActive()).toBeTruthy(); }); @@ -74,8 +88,22 @@ describe("StatsReportGatherer", () => { receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }); expect(getStats).toHaveBeenCalled(); expect(collector.getActive()).toBeFalsy(); diff --git a/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts b/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts index 2692dbf03eb..117231ee2de 100644 --- a/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts +++ b/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts @@ -37,29 +37,85 @@ describe("SummaryStatsReporter", () => { receivedMedia: 10, receivedAudioMedia: 4, receivedVideoMedia: 6, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, { receivedMedia: 13, receivedAudioMedia: 0, receivedVideoMedia: 13, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 5, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, { receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 10, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, { receivedMedia: 15, receivedAudioMedia: 6, receivedVideoMedia: 9, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -69,6 +125,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 0.75, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0.0375, }); }); @@ -78,8 +135,22 @@ describe("SummaryStatsReporter", () => { receivedMedia: 10, receivedAudioMedia: 10, receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 1, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -89,6 +160,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 1, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0, }); }); @@ -98,8 +170,22 @@ describe("SummaryStatsReporter", () => { receivedMedia: 10, receivedAudioMedia: 10, receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 2, muted: 1, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 2, + muted: 1, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -109,6 +195,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 0, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0, }); }); @@ -118,8 +205,22 @@ describe("SummaryStatsReporter", () => { receivedMedia: 100, receivedAudioMedia: 0, receivedVideoMedia: 100, - audioTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 1, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -129,6 +230,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 1, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0, }); }); @@ -138,29 +240,85 @@ describe("SummaryStatsReporter", () => { receivedMedia: 1, receivedAudioMedia: 1, receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, { receivedMedia: 1, receivedAudioMedia: 1, receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 20, maxPacketLoss: 5 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 20, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, { receivedMedia: 1, receivedAudioMedia: 1, receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, }, { receivedMedia: 1, receivedAudioMedia: 1, receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 40 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 40, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -170,6 +328,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 1, maxJitter: 20, maxPacketLoss: 40, + percentageConcealedAudio: 0, }); }); @@ -179,8 +338,22 @@ describe("SummaryStatsReporter", () => { receivedMedia: 10, receivedAudioMedia: 0, receivedVideoMedia: 10, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -190,6 +363,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 1, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0, }); }); @@ -199,8 +373,22 @@ describe("SummaryStatsReporter", () => { receivedMedia: 1, receivedAudioMedia: 22, receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -210,6 +398,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 1, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0, }); }); @@ -219,8 +408,22 @@ describe("SummaryStatsReporter", () => { receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, }, ]; reporter.build(summary); @@ -230,6 +433,7 @@ describe("SummaryStatsReporter", () => { percentageReceivedVideoMedia: 1, maxJitter: 0, maxPacketLoss: 0, + percentageConcealedAudio: 0, }); }); }); diff --git a/spec/unit/webrtc/stats/trackStatsReporter.spec.ts b/spec/unit/webrtc/stats/trackStatsReporter.spec.ts index 79b1ad680aa..0c22cdaf6a0 100644 --- a/spec/unit/webrtc/stats/trackStatsReporter.spec.ts +++ b/spec/unit/webrtc/stats/trackStatsReporter.spec.ts @@ -226,12 +226,16 @@ describe("TrackStatsReporter", () => { muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); @@ -245,12 +249,16 @@ describe("TrackStatsReporter", () => { muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 3, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); @@ -266,12 +274,16 @@ describe("TrackStatsReporter", () => { muted: 1, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 3, muted: 1, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); @@ -287,17 +299,21 @@ describe("TrackStatsReporter", () => { muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 3, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); - it("and returns summary and build max jitter and packet loss", async () => { + it("and returns summary and build max jitter, packet loss and audio conealment", async () => { const trackStatsList = buildMockTrackStatsList(); // video remote trackStatsList[1].setJitter(12); @@ -311,6 +327,8 @@ describe("TrackStatsReporter", () => { trackStatsList[5].setJitter(15); trackStatsList[2].setLoss({ packetsLost: 5, packetsTotal: 0, isDownloadStream: true }); trackStatsList[5].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true }); + trackStatsList[2].setAudioConcealment(220, 2000); + trackStatsList[5].setAudioConcealment(180, 2000); const summary = TrackStatsReporter.buildTrackSummary(trackStatsList); expect(summary).toEqual({ @@ -319,12 +337,16 @@ describe("TrackStatsReporter", () => { muted: 0, maxJitter: 15, maxPacketLoss: 5, + concealedAudio: 400, + totalAudio: 4000, }, videoTrackSummary: { count: 3, muted: 0, maxJitter: 66, maxPacketLoss: 55, + concealedAudio: 0, + totalAudio: 0, }, }); }); diff --git a/src/webrtc/stats/connectionStats.ts b/src/webrtc/stats/connectionStats.ts index dbde6e50327..ef1c36797ea 100644 --- a/src/webrtc/stats/connectionStats.ts +++ b/src/webrtc/stats/connectionStats.ts @@ -33,7 +33,7 @@ export interface ConnectionStatsBitrate extends Bitrate { video?: Bitrate; } -export interface PacketLoos { +export interface PacketLoss { total: number; download: number; upload: number; @@ -42,6 +42,6 @@ export interface PacketLoos { export class ConnectionStats { public bandwidth: ConnectionStatsBitrate = {} as ConnectionStatsBitrate; public bitrate: ConnectionStatsBitrate = {} as ConnectionStatsBitrate; - public packetLoss: PacketLoos = {} as PacketLoos; + public packetLoss: PacketLoss = {} as PacketLoss; public transport: TransportStats[] = []; } diff --git a/src/webrtc/stats/media/mediaTrackStats.ts b/src/webrtc/stats/media/mediaTrackStats.ts index 66475e19ce6..7835ceb8a65 100644 --- a/src/webrtc/stats/media/mediaTrackStats.ts +++ b/src/webrtc/stats/media/mediaTrackStats.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { AudioConcealment } from "../statsReport"; import { TrackId } from "./mediaTrackHandler"; export interface PacketLoss { @@ -32,7 +33,14 @@ export interface Bitrate { */ upload: number; } +export interface ConcealedAudio { + /** + * duration in ms + */ + duration: number; + ratio: number; +} export interface Resolution { width: number; height: number; @@ -44,6 +52,7 @@ export class MediaTrackStats { private loss: PacketLoss = { packetsTotal: 0, packetsLost: 0, isDownloadStream: false }; private bitrate: Bitrate = { download: 0, upload: 0 }; private resolution: Resolution = { width: -1, height: -1 }; + private audioConcealment: AudioConcealment = { concealedAudio: 0, totalAudioDuration: 0 }; private framerate = 0; private jitter = 0; private codec = ""; @@ -61,8 +70,8 @@ export class MediaTrackStats { return this.type; } - public setLoss(loos: PacketLoss): void { - this.loss = loos; + public setLoss(loss: PacketLoss): void { + this.loss = loss; } public getLoss(): PacketLoss { @@ -152,4 +161,16 @@ export class MediaTrackStats { public getJitter(): number { return this.jitter; } + + /** + * Audio concealment ration (conceled duration / total duration) + */ + public setAudioConcealment(concealedAudioDuration: number, totalAudioDuration: number): void { + this.audioConcealment.concealedAudio = concealedAudioDuration; + this.audioConcealment.totalAudioDuration = totalAudioDuration; + } + + public getAudioConcealment(): AudioConcealment { + return this.audioConcealment; + } } diff --git a/src/webrtc/stats/statsReport.ts b/src/webrtc/stats/statsReport.ts index 64bb6aba440..67fd3513bcc 100644 --- a/src/webrtc/stats/statsReport.ts +++ b/src/webrtc/stats/statsReport.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoos } from "./connectionStats"; +import { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoss } from "./connectionStats"; import { TransportStats } from "./transportStats"; import { Resolution } from "./media/mediaTrackStats"; @@ -34,7 +34,9 @@ export interface ByteSentStatsReport extends Map { export interface ConnectionStatsReport { bandwidth: ConnectionStatsBandwidth; bitrate: ConnectionStatsBitrate; - packetLoss: PacketLoos; + packetLoss: PacketLoss; + audioConcealment: Map; + totalAudioConcealment: AudioConcealment; resolution: ResolutionMap; framerate: FramerateMap; codec: CodecMap; @@ -42,6 +44,11 @@ export interface ConnectionStatsReport { transport: TransportStats[]; } +export interface AudioConcealment { + concealedAudio: number; + totalAudioDuration: number; +} + export interface ResolutionMap { local: Map; remote: Map; @@ -70,4 +77,5 @@ export interface SummaryStatsReport { percentageReceivedVideoMedia: number; maxJitter: number; maxPacketLoss: number; + percentageConcealedAudio: number; } diff --git a/src/webrtc/stats/statsReportBuilder.ts b/src/webrtc/stats/statsReportBuilder.ts index eeca4ed4074..566648fb0e9 100644 --- a/src/webrtc/stats/statsReportBuilder.ts +++ b/src/webrtc/stats/statsReportBuilder.ts @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport"; +import { AudioConcealment, CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport"; import { MediaTrackStats, Resolution } from "./media/mediaTrackStats"; export class StatsReportBuilder { @@ -38,12 +38,16 @@ export class StatsReportBuilder { const framerates: FramerateMap = { local: new Map(), remote: new Map() }; const codecs: CodecMap = { local: new Map(), remote: new Map() }; const jitter = new Map(); + const audioConcealment = new Map(); let audioBitrateDownload = 0; let audioBitrateUpload = 0; let videoBitrateDownload = 0; let videoBitrateUpload = 0; + let totalConcealedAudio = 0; + let totalAudioDuration = 0; + for (const [trackId, trackStats] of stats) { // process packet loss stats const loss = trackStats.getLoss(); @@ -58,6 +62,11 @@ export class StatsReportBuilder { // collect resolutions and framerates if (trackStats.kind === "audio") { + // process audio quality stats + const audioConcealmentForTrack = trackStats.getAudioConcealment(); + totalConcealedAudio += audioConcealmentForTrack.concealedAudio; + totalAudioDuration += audioConcealmentForTrack.totalAudioDuration; + audioBitrateDownload += trackStats.getBitrate().download; audioBitrateUpload += trackStats.getBitrate().upload; } else { @@ -70,6 +79,9 @@ export class StatsReportBuilder { codecs[trackStats.getType()].set(trackId, trackStats.getCodec()); if (trackStats.getType() === "remote") { jitter.set(trackId, trackStats.getJitter()); + if (trackStats.kind === "audio") { + audioConcealment.set(trackId, trackStats.getAudioConcealment()); + } } trackStats.resetBitrate(); @@ -98,6 +110,12 @@ export class StatsReportBuilder { download: StatsReportBuilder.calculatePacketLoss(lostPackets.download, totalPackets.download), upload: StatsReportBuilder.calculatePacketLoss(lostPackets.upload, totalPackets.upload), }; + report.audioConcealment = audioConcealment; + report.totalAudioConcealment = { + concealedAudio: totalConcealedAudio, + totalAudioDuration, + }; + report.framerate = framerates; report.resolution = resolutions; report.codec = codecs; diff --git a/src/webrtc/stats/statsReportGatherer.ts b/src/webrtc/stats/statsReportGatherer.ts index 25f9b93c862..e5146122fb6 100644 --- a/src/webrtc/stats/statsReportGatherer.ts +++ b/src/webrtc/stats/statsReportGatherer.ts @@ -53,8 +53,8 @@ export class StatsReportGatherer { receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 }, + audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0, concealedAudio: 0, totalAudio: 0 }, + videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0, concealedAudio: 0, totalAudio: 0 }, } as SummaryStats; if (this.isActive) { const statsPromise = this.pc.getStats(); @@ -138,6 +138,7 @@ export class StatsReportGatherer { const ts = this.trackStats.findTransceiverByTrackId(trackStats.trackId); TrackStatsReporter.setTrackStatsState(trackStats, ts); TrackStatsReporter.buildJitter(trackStats, now); + TrackStatsReporter.buildAudioConcealment(trackStats, now); } else if (before) { byteSentStats.set(trackStats.trackId, StatsValueFormatter.getNonNegativeValue(now.bytesSent)); TrackStatsReporter.buildBitrateSend(trackStats, now, before); diff --git a/src/webrtc/stats/summaryStats.ts b/src/webrtc/stats/summaryStats.ts index f708a5bad09..e5591749db9 100644 --- a/src/webrtc/stats/summaryStats.ts +++ b/src/webrtc/stats/summaryStats.ts @@ -23,4 +23,6 @@ export interface TrackSummary { muted: number; maxJitter: number; maxPacketLoss: number; + concealedAudio: number; + totalAudio: number; } diff --git a/src/webrtc/stats/summaryStatsReporter.ts b/src/webrtc/stats/summaryStatsReporter.ts index 66f738b1ca5..8ad0c5591d3 100644 --- a/src/webrtc/stats/summaryStatsReporter.ts +++ b/src/webrtc/stats/summaryStatsReporter.ts @@ -14,59 +14,76 @@ import { StatsReportEmitter } from "./statsReportEmitter"; import { SummaryStats } from "./summaryStats"; import { SummaryStatsReport } from "./statsReport"; -interface ReceivedMedia { - audio: number; - video: number; - media: number; +interface SummaryCounter { + receivedAudio: number; + receivedVideo: number; + receivedMedia: number; + concealedAudio: number; + totalAudio: number; } export class SummaryStatsReporter { public constructor(private emitter: StatsReportEmitter) {} public build(summary: SummaryStats[]): void { - const entiretyTracksCount = summary.length; - if (entiretyTracksCount === 0) { + const summaryTotalCount = summary.length; + if (summaryTotalCount === 0) { return; } - const receivedCounter: ReceivedMedia = { audio: 0, video: 0, media: 0 }; + const summaryCounter: SummaryCounter = { + receivedAudio: 0, + receivedVideo: 0, + receivedMedia: 0, + concealedAudio: 0, + totalAudio: 0, + }; let maxJitter = 0; let maxPacketLoss = 0; - summary.forEach((stats) => { - this.countTrackListReceivedMedia(receivedCounter, stats); + this.countTrackListReceivedMedia(summaryCounter, stats); + this.countConcealedAudio(summaryCounter, stats); maxJitter = this.buildMaxJitter(maxJitter, stats); maxPacketLoss = this.buildMaxPacketLoss(maxPacketLoss, stats); }); - + const decimalPlaces = 5; const report = { - percentageReceivedMedia: Math.round((receivedCounter.media / entiretyTracksCount) * 100) / 100, - percentageReceivedVideoMedia: Math.round((receivedCounter.video / entiretyTracksCount) * 100) / 100, - percentageReceivedAudioMedia: Math.round((receivedCounter.audio / entiretyTracksCount) * 100) / 100, + percentageReceivedMedia: Number((summaryCounter.receivedMedia / summaryTotalCount).toFixed(decimalPlaces)), + percentageReceivedVideoMedia: Number( + (summaryCounter.receivedVideo / summaryTotalCount).toFixed(decimalPlaces), + ), + percentageReceivedAudioMedia: Number( + (summaryCounter.receivedAudio / summaryTotalCount).toFixed(decimalPlaces), + ), maxJitter, maxPacketLoss, + percentageConcealedAudio: Number( + summaryCounter.totalAudio > 0 + ? (summaryCounter.concealedAudio / summaryCounter.totalAudio).toFixed(decimalPlaces) + : 0, + ), } as SummaryStatsReport; this.emitter.emitSummaryStatsReport(report); } - private countTrackListReceivedMedia(counter: ReceivedMedia, stats: SummaryStats): void { + private countTrackListReceivedMedia(counter: SummaryCounter, stats: SummaryStats): void { let hasReceivedAudio = false; let hasReceivedVideo = false; if (stats.receivedAudioMedia > 0 || stats.audioTrackSummary.count === 0) { - counter.audio++; + counter.receivedAudio++; hasReceivedAudio = true; } if (stats.receivedVideoMedia > 0 || stats.videoTrackSummary.count === 0) { - counter.video++; + counter.receivedVideo++; hasReceivedVideo = true; } else { if (stats.videoTrackSummary.muted > 0 && stats.videoTrackSummary.muted === stats.videoTrackSummary.count) { - counter.video++; + counter.receivedVideo++; hasReceivedVideo = true; } } if (hasReceivedVideo && hasReceivedAudio) { - counter.media++; + counter.receivedMedia++; } } @@ -91,4 +108,9 @@ export class SummaryStatsReporter { } return maxPacketLoss; } + + private countConcealedAudio(summaryCounter: SummaryCounter, stats: SummaryStats): void { + summaryCounter.concealedAudio += stats.audioTrackSummary.concealedAudio; + summaryCounter.totalAudio += stats.audioTrackSummary.totalAudio; + } } diff --git a/src/webrtc/stats/trackStatsReporter.ts b/src/webrtc/stats/trackStatsReporter.ts index e243cb32603..75409f26afa 100644 --- a/src/webrtc/stats/trackStatsReporter.ts +++ b/src/webrtc/stats/trackStatsReporter.ts @@ -140,23 +140,44 @@ export class TrackStatsReporter { audioTrackSummary: TrackSummary; videoTrackSummary: TrackSummary; } { - const audioTrackSummary = { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }; - const videoTrackSummary = { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }; - trackStatsList - .filter((t) => t.getType() === "remote") - .forEach((stats) => { - const trackSummary = stats.kind === "video" ? videoTrackSummary : audioTrackSummary; - trackSummary.count++; - if (stats.alive && stats.muted) { - trackSummary.muted++; - } - if (trackSummary.maxJitter < stats.getJitter()) { - trackSummary.maxJitter = stats.getJitter(); - } - if (trackSummary.maxPacketLoss < stats.getLoss().packetsLost) { - trackSummary.maxPacketLoss = stats.getLoss().packetsLost; - } - }); + const videoTrackSummary: TrackSummary = { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }; + const audioTrackSummary: TrackSummary = { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }; + + const remoteTrackList = trackStatsList.filter((t) => t.getType() === "remote"); + const audioTrackList = remoteTrackList.filter((t) => t.kind === "audio"); + + remoteTrackList.forEach((stats) => { + const trackSummary = stats.kind === "video" ? videoTrackSummary : audioTrackSummary; + trackSummary.count++; + if (stats.alive && stats.muted) { + trackSummary.muted++; + } + if (trackSummary.maxJitter < stats.getJitter()) { + trackSummary.maxJitter = stats.getJitter(); + } + if (trackSummary.maxPacketLoss < stats.getLoss().packetsLost) { + trackSummary.maxPacketLoss = stats.getLoss().packetsLost; + } + if (audioTrackList.length > 0) { + trackSummary.concealedAudio += stats.getAudioConcealment()?.concealedAudio; + trackSummary.totalAudio += stats.getAudioConcealment()?.totalAudioDuration; + } + }); + return { audioTrackSummary, videoTrackSummary }; } @@ -173,4 +194,14 @@ export class TrackStatsReporter { trackStats.setJitter(-1); } } + + public static buildAudioConcealment(trackStats: MediaTrackStats, statsReport: any): void { + if (statsReport.type !== "inbound-rtp") { + return; + } + const msPerSample = (1000 * statsReport?.totalSamplesDuration) / statsReport?.totalSamplesReceived; + const concealedAudioDuration = msPerSample * statsReport?.concealedSamples; + const totalAudioDuration = 1000 * statsReport?.totalSamplesDuration; + trackStats.setAudioConcealment(concealedAudioDuration, totalAudioDuration); + } }