191 lines
8.5 KiB
JavaScript
191 lines
8.5 KiB
JavaScript
|
|
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
|
||
|
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
||
|
|
/*
|
||
|
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||
|
|
|
||
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
you may not use this file except in compliance with the License.
|
||
|
|
You may obtain a copy of the License at
|
||
|
|
|
||
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
|
||
|
|
Unless required by applicable law or agreed to in writing, software
|
||
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
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 { ConnectionStats } from "./connectionStats.js";
|
||
|
|
import { ConnectionStatsBuilder } from "./connectionStatsBuilder.js";
|
||
|
|
import { TransportStatsBuilder } from "./transportStatsBuilder.js";
|
||
|
|
import { MediaSsrcHandler } from "./media/mediaSsrcHandler.js";
|
||
|
|
import { MediaTrackHandler } from "./media/mediaTrackHandler.js";
|
||
|
|
import { MediaTrackStatsHandler } from "./media/mediaTrackStatsHandler.js";
|
||
|
|
import { TrackStatsBuilder } from "./trackStatsBuilder.js";
|
||
|
|
import { ConnectionStatsReportBuilder } from "./connectionStatsReportBuilder.js";
|
||
|
|
import { ValueFormatter } from "./valueFormatter.js";
|
||
|
|
import { logger } from "../../logger.js";
|
||
|
|
import { CallFeedStatsReporter } from "./callFeedStatsReporter.js";
|
||
|
|
export class CallStatsReportGatherer {
|
||
|
|
constructor(callId, opponentMemberId, pc, emitter, isFocus = true) {
|
||
|
|
_defineProperty(this, "isActive", true);
|
||
|
|
_defineProperty(this, "previousStatsReport", void 0);
|
||
|
|
_defineProperty(this, "currentStatsReport", void 0);
|
||
|
|
_defineProperty(this, "connectionStats", new ConnectionStats());
|
||
|
|
_defineProperty(this, "trackStats", void 0);
|
||
|
|
this.callId = callId;
|
||
|
|
this.opponentMemberId = opponentMemberId;
|
||
|
|
this.pc = pc;
|
||
|
|
this.emitter = emitter;
|
||
|
|
this.isFocus = isFocus;
|
||
|
|
pc.addEventListener("signalingstatechange", this.onSignalStateChange.bind(this));
|
||
|
|
this.trackStats = new MediaTrackStatsHandler(new MediaSsrcHandler(), new MediaTrackHandler(pc));
|
||
|
|
}
|
||
|
|
async processStats(groupCallId, localUserId) {
|
||
|
|
const summary = {
|
||
|
|
isFirstCollection: this.previousStatsReport === undefined,
|
||
|
|
receivedMedia: 0,
|
||
|
|
receivedAudioMedia: 0,
|
||
|
|
receivedVideoMedia: 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
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if (this.isActive) {
|
||
|
|
const statsPromise = this.pc.getStats();
|
||
|
|
if (typeof statsPromise?.then === "function") {
|
||
|
|
return statsPromise.then(report => {
|
||
|
|
// @ts-ignore
|
||
|
|
this.currentStatsReport = typeof report?.result === "function" ? report.result() : report;
|
||
|
|
try {
|
||
|
|
this.processStatsReport(groupCallId, localUserId);
|
||
|
|
} catch (error) {
|
||
|
|
this.handleError(error);
|
||
|
|
return summary;
|
||
|
|
}
|
||
|
|
this.previousStatsReport = this.currentStatsReport;
|
||
|
|
summary.receivedMedia = this.connectionStats.bitrate.download;
|
||
|
|
summary.receivedAudioMedia = this.connectionStats.bitrate.audio?.download || 0;
|
||
|
|
summary.receivedVideoMedia = this.connectionStats.bitrate.video?.download || 0;
|
||
|
|
const trackSummary = TrackStatsBuilder.buildTrackSummary(Array.from(this.trackStats.getTrack2stats().values()));
|
||
|
|
return _objectSpread(_objectSpread({}, summary), {}, {
|
||
|
|
audioTrackSummary: trackSummary.audioTrackSummary,
|
||
|
|
videoTrackSummary: trackSummary.videoTrackSummary
|
||
|
|
});
|
||
|
|
}).catch(error => {
|
||
|
|
this.handleError(error);
|
||
|
|
return summary;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.isActive = false;
|
||
|
|
}
|
||
|
|
return Promise.resolve(summary);
|
||
|
|
}
|
||
|
|
processStatsReport(groupCallId, localUserId) {
|
||
|
|
const byteSentStatsReport = new Map();
|
||
|
|
byteSentStatsReport.callId = this.callId;
|
||
|
|
byteSentStatsReport.opponentMemberId = this.opponentMemberId;
|
||
|
|
this.currentStatsReport?.forEach(now => {
|
||
|
|
const before = this.previousStatsReport ? this.previousStatsReport.get(now.id) : null;
|
||
|
|
// RTCIceCandidatePairStats - https://w3c.github.io/webrtc-stats/#candidatepair-dict*
|
||
|
|
if (now.type === "candidate-pair" && now.nominated && now.state === "succeeded") {
|
||
|
|
this.connectionStats.bandwidth = ConnectionStatsBuilder.buildBandwidthReport(now);
|
||
|
|
this.connectionStats.transport = TransportStatsBuilder.buildReport(this.currentStatsReport, now, this.connectionStats.transport, this.isFocus);
|
||
|
|
|
||
|
|
// RTCReceivedRtpStreamStats
|
||
|
|
// https://w3c.github.io/webrtc-stats/#receivedrtpstats-dict*
|
||
|
|
// RTCSentRtpStreamStats
|
||
|
|
// https://w3c.github.io/webrtc-stats/#sentrtpstats-dict*
|
||
|
|
} else if (now.type === "inbound-rtp" || now.type === "outbound-rtp") {
|
||
|
|
const trackStats = this.trackStats.findTrack2Stats(now, now.type === "inbound-rtp" ? "remote" : "local");
|
||
|
|
if (!trackStats) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (before) {
|
||
|
|
TrackStatsBuilder.buildPacketsLost(trackStats, now, before);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the resolution and framerate for only remote video sources here. For the local video sources,
|
||
|
|
// 'track' stats will be used since they have the updated resolution based on the simulcast streams
|
||
|
|
// currently being sent. Promise based getStats reports three 'outbound-rtp' streams and there will be
|
||
|
|
// more calculations needed to determine what is the highest resolution stream sent by the client if the
|
||
|
|
// 'outbound-rtp' stats are used.
|
||
|
|
if (now.type === "inbound-rtp") {
|
||
|
|
TrackStatsBuilder.buildFramerateResolution(trackStats, now);
|
||
|
|
if (before) {
|
||
|
|
TrackStatsBuilder.buildBitrateReceived(trackStats, now, before);
|
||
|
|
}
|
||
|
|
const ts = this.trackStats.findTransceiverByTrackId(trackStats.trackId);
|
||
|
|
TrackStatsBuilder.setTrackStatsState(trackStats, ts);
|
||
|
|
TrackStatsBuilder.buildJitter(trackStats, now);
|
||
|
|
TrackStatsBuilder.buildAudioConcealment(trackStats, now);
|
||
|
|
} else if (before) {
|
||
|
|
byteSentStatsReport.set(trackStats.trackId, ValueFormatter.getNonNegativeValue(now.bytesSent));
|
||
|
|
TrackStatsBuilder.buildBitrateSend(trackStats, now, before);
|
||
|
|
}
|
||
|
|
TrackStatsBuilder.buildCodec(this.currentStatsReport, trackStats, now);
|
||
|
|
} else if (now.type === "track" && now.kind === "video" && !now.remoteSource) {
|
||
|
|
const trackStats = this.trackStats.findLocalVideoTrackStats(now);
|
||
|
|
if (!trackStats) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
TrackStatsBuilder.buildFramerateResolution(trackStats, now);
|
||
|
|
TrackStatsBuilder.calculateSimulcastFramerate(trackStats, now, before, this.trackStats.mediaTrackHandler.getActiveSimulcastStreams());
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.emitter.emitByteSendReport(byteSentStatsReport);
|
||
|
|
this.emitter.emitCallFeedReport(CallFeedStatsReporter.buildCallFeedReport(this.callId, this.opponentMemberId, this.pc));
|
||
|
|
this.processAndEmitConnectionStatsReport();
|
||
|
|
}
|
||
|
|
setActive(isActive) {
|
||
|
|
this.isActive = isActive;
|
||
|
|
}
|
||
|
|
getActive() {
|
||
|
|
return this.isActive;
|
||
|
|
}
|
||
|
|
handleError(error) {
|
||
|
|
this.isActive = false;
|
||
|
|
logger.warn(`CallStatsReportGatherer ${this.callId} processStatsReport fails and set to inactive ${error}`);
|
||
|
|
}
|
||
|
|
processAndEmitConnectionStatsReport() {
|
||
|
|
const report = ConnectionStatsReportBuilder.build(this.trackStats.getTrack2stats());
|
||
|
|
report.callId = this.callId;
|
||
|
|
report.opponentMemberId = this.opponentMemberId;
|
||
|
|
this.connectionStats.bandwidth = report.bandwidth;
|
||
|
|
this.connectionStats.bitrate = report.bitrate;
|
||
|
|
this.connectionStats.packetLoss = report.packetLoss;
|
||
|
|
this.emitter.emitConnectionStatsReport(_objectSpread(_objectSpread({}, report), {}, {
|
||
|
|
transport: this.connectionStats.transport
|
||
|
|
}));
|
||
|
|
this.connectionStats.transport = [];
|
||
|
|
}
|
||
|
|
stopProcessingStats() {}
|
||
|
|
onSignalStateChange() {
|
||
|
|
if (this.pc.signalingState === "stable") {
|
||
|
|
if (this.pc.currentRemoteDescription) {
|
||
|
|
this.trackStats.mediaSsrcHandler.parse(this.pc.currentRemoteDescription.sdp, "remote");
|
||
|
|
}
|
||
|
|
if (this.pc.currentLocalDescription) {
|
||
|
|
this.trackStats.mediaSsrcHandler.parse(this.pc.currentLocalDescription.sdp, "local");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setOpponentMemberId(id) {
|
||
|
|
this.opponentMemberId = id;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=callStatsReportGatherer.js.map
|