This commit is contained in:
unfunny
2026-09-12 23:57:45 -04:00
parent 986c0af682
commit dfe4cc537f
1775 changed files with 246421 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import { type CallFeedReport } from "./statsReport.ts";
import { type CallFeed } from "../callFeed.ts";
export declare class CallFeedStatsReporter {
static buildCallFeedReport(callId: string, opponentMemberId: string, pc: RTCPeerConnection): CallFeedReport;
private static buildTrackStats;
static expandCallFeedReport(report: CallFeedReport, callFeeds: CallFeed[], prefix?: string): CallFeedReport;
}
//# sourceMappingURL=callFeedStatsReporter.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"callFeedStatsReporter.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/callFeedStatsReporter.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,cAAc,EAA8D,MAAM,kBAAkB,CAAC;AACnH,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,qBAAa,qBAAqB;IAC9B,OAAc,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,iBAAiB,GAAG,cAAc,CAuBjH;IAED,OAAO,CAAC,MAAM,CAAC,eAAe;IAgB9B,OAAc,oBAAoB,CAC9B,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,QAAQ,EAAE,EACrB,MAAM,SAAY,GACnB,cAAc,CAyBhB;CACJ"}

View File

@@ -0,0 +1,75 @@
/*
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.
*/
export class CallFeedStatsReporter {
static buildCallFeedReport(callId, opponentMemberId, pc) {
const rtpTransceivers = pc.getTransceivers();
const transceiver = [];
const callFeeds = [];
rtpTransceivers.forEach(t => {
const sender = t.sender?.track ? CallFeedStatsReporter.buildTrackStats(t.sender.track, "sender") : null;
const receiver = CallFeedStatsReporter.buildTrackStats(t.receiver.track, "receiver");
transceiver.push({
mid: t.mid == null ? "null" : t.mid,
direction: t.direction,
currentDirection: t.currentDirection == null ? "null" : t.currentDirection,
sender,
receiver
});
});
return {
callId,
opponentMemberId,
transceiver,
callFeeds
};
}
static buildTrackStats(track, label = "--") {
const settingDeviceId = track.getSettings()?.deviceId;
const constrainDeviceId = track.getConstraints()?.deviceId;
return {
id: track.id,
kind: track.kind,
settingDeviceId: settingDeviceId ?? "unknown",
constrainDeviceId: constrainDeviceId ?? "unknown",
muted: track.muted,
enabled: track.enabled,
readyState: track.readyState,
label
};
}
static expandCallFeedReport(report, callFeeds, prefix = "unknown") {
callFeeds.forEach(feed => {
const audioTracks = feed.stream.getAudioTracks();
const videoTracks = feed.stream.getVideoTracks();
const audio = audioTracks.length > 0 ? CallFeedStatsReporter.buildTrackStats(feed.stream.getAudioTracks()[0], feed.purpose) : null;
const video = videoTracks.length > 0 ? CallFeedStatsReporter.buildTrackStats(feed.stream.getVideoTracks()[0], feed.purpose) : null;
const feedStats = {
stream: feed.stream.id,
type: feed.isLocal() ? "local" : "remote",
audio,
video,
purpose: feed.purpose,
prefix,
isVideoMuted: feed.isVideoMuted(),
isAudioMuted: feed.isAudioMuted()
};
report.callFeeds.push(feedStats);
});
return report;
}
}
//# sourceMappingURL=callFeedStatsReporter.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
import { type StatsReportEmitter } from "./statsReportEmitter.ts";
import { type CallStatsReportSummary } from "./callStatsReportSummary.ts";
export declare class CallStatsReportGatherer {
readonly callId: string;
private opponentMemberId;
private readonly pc;
private readonly emitter;
private readonly isFocus;
private isActive;
private previousStatsReport;
private currentStatsReport;
private readonly connectionStats;
private readonly trackStats;
constructor(callId: string, opponentMemberId: string, pc: RTCPeerConnection, emitter: StatsReportEmitter, isFocus?: boolean);
processStats(groupCallId: string, localUserId: string): Promise<CallStatsReportSummary>;
private processStatsReport;
setActive(isActive: boolean): void;
getActive(): boolean;
private handleError;
private processAndEmitConnectionStatsReport;
stopProcessingStats(): void;
private onSignalStateChange;
setOpponentMemberId(id: string): void;
}
//# sourceMappingURL=callStatsReportGatherer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"callStatsReportGatherer.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/callStatsReportGatherer.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAUlE,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAI1E,qBAAa,uBAAuB;aASZ,MAAM,EAAE,MAAM;IAC9B,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZ5B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IAEzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IAEpD,YACoB,MAAM,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,EACf,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,UAAO,EAIlC;IAEY,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA6CnG;IAED,OAAO,CAAC,kBAAkB;IA2EnB,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAExC;IAEM,SAAS,IAAI,OAAO,CAE1B;IAED,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,mCAAmC;IAiBpC,mBAAmB,IAAI,IAAI,CAAG;IAErC,OAAO,CAAC,mBAAmB;IAWpB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3C;CACJ"}

View File

@@ -0,0 +1,191 @@
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,17 @@
export interface CallStatsReportSummary {
receivedMedia: number;
receivedAudioMedia: number;
receivedVideoMedia: number;
audioTrackSummary: TrackSummary;
videoTrackSummary: TrackSummary;
isFirstCollection: boolean;
}
export interface TrackSummary {
count: number;
muted: number;
maxJitter: number;
maxPacketLoss: number;
concealedAudio: number;
totalAudio: number;
}
//# sourceMappingURL=callStatsReportSummary.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"callStatsReportSummary.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/callStatsReportSummary.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,sBAAsB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,YAAY,CAAC;IAChC,iBAAiB,EAAE,YAAY,CAAC;IAEhC,iBAAiB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACtB"}

View File

@@ -0,0 +1,3 @@
export {};
export {};
//# sourceMappingURL=callStatsReportSummary.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"callStatsReportSummary.js","names":[],"sources":["../../../src/webrtc/stats/callStatsReportSummary.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\nexport interface CallStatsReportSummary {\n receivedMedia: number;\n receivedAudioMedia: number;\n receivedVideoMedia: number;\n audioTrackSummary: TrackSummary;\n videoTrackSummary: TrackSummary;\n\n isFirstCollection: boolean;\n}\n\nexport interface TrackSummary {\n count: number;\n muted: number;\n maxJitter: number;\n maxPacketLoss: number;\n concealedAudio: number;\n totalAudio: number;\n}\n"],"mappings":"","ignoreList":[]}

View File

@@ -0,0 +1,28 @@
import { type TransportStats } from "./transportStats.ts";
import { type Bitrate } from "./media/mediaTrackStats.ts";
export interface ConnectionStatsBandwidth {
/**
* bytes per second
*/
download: number;
/**
* bytes per second
*/
upload: number;
}
export interface ConnectionStatsBitrate extends Bitrate {
audio?: Bitrate;
video?: Bitrate;
}
export interface PacketLoss {
total: number;
download: number;
upload: number;
}
export declare class ConnectionStats {
bandwidth: ConnectionStatsBitrate;
bitrate: ConnectionStatsBitrate;
packetLoss: PacketLoss;
transport: TransportStats[];
}
//# sourceMappingURL=connectionStats.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"connectionStats.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/connectionStats.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,MAAM,WAAW,wBAAwB;IACrC;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAuB,SAAQ,OAAO;IACnD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IACjB,SAAS,EAAE,sBAAsB,CAAgC;IACjE,OAAO,EAAE,sBAAsB,CAAgC;IAC/D,UAAU,EAAE,UAAU,CAAoB;IAC1C,SAAS,EAAE,cAAc,EAAE,CAAM;CAC3C"}

View File

@@ -0,0 +1,26 @@
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.
*/
export class ConnectionStats {
constructor() {
_defineProperty(this, "bandwidth", {});
_defineProperty(this, "bitrate", {});
_defineProperty(this, "packetLoss", {});
_defineProperty(this, "transport", []);
}
}
//# sourceMappingURL=connectionStats.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"connectionStats.js","names":[],"sources":["../../../src/webrtc/stats/connectionStats.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { type TransportStats } from \"./transportStats.ts\";\nimport { type Bitrate } from \"./media/mediaTrackStats.ts\";\n\nexport interface ConnectionStatsBandwidth {\n /**\n * bytes per second\n */\n download: number;\n /**\n * bytes per second\n */\n upload: number;\n}\n\nexport interface ConnectionStatsBitrate extends Bitrate {\n audio?: Bitrate;\n video?: Bitrate;\n}\n\nexport interface PacketLoss {\n total: number;\n download: number;\n upload: number;\n}\n\nexport class ConnectionStats {\n public bandwidth: ConnectionStatsBitrate = {} as ConnectionStatsBitrate;\n public bitrate: ConnectionStatsBitrate = {} as ConnectionStatsBitrate;\n public packetLoss: PacketLoss = {} as PacketLoss;\n public transport: TransportStats[] = [];\n}\n"],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AA2BA,OAAO,MAAM,eAAe,CAAC;EAAA;IAAA,mCACkB,CAAC,CAAC;IAAA,iCACJ,CAAC,CAAC;IAAA,oCACX,CAAC,CAAC;IAAA,mCACG,EAAE;EAAA;AAC3C","ignoreList":[]}

View File

@@ -0,0 +1,5 @@
import { type Bitrate } from "./media/mediaTrackStats.ts";
export declare class ConnectionStatsBuilder {
static buildBandwidthReport(now: RTCIceCandidatePairStats): Bitrate;
}
//# sourceMappingURL=connectionStatsBuilder.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"connectionStatsBuilder.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/connectionStatsBuilder.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,qBAAa,sBAAsB;IAC/B,OAAc,oBAAoB,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAQzE;CACJ"}

View File

@@ -0,0 +1,27 @@
/*
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.
*/
export class ConnectionStatsBuilder {
static buildBandwidthReport(now) {
const availableIncomingBitrate = now.availableIncomingBitrate;
const availableOutgoingBitrate = now.availableOutgoingBitrate;
return {
download: availableIncomingBitrate ? Math.round(availableIncomingBitrate / 1000) : 0,
upload: availableOutgoingBitrate ? Math.round(availableOutgoingBitrate / 1000) : 0
};
}
}
//# sourceMappingURL=connectionStatsBuilder.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"connectionStatsBuilder.js","names":[],"sources":["../../../src/webrtc/stats/connectionStatsBuilder.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\nimport { type Bitrate } from \"./media/mediaTrackStats.ts\";\n\nexport class ConnectionStatsBuilder {\n public static buildBandwidthReport(now: RTCIceCandidatePairStats): Bitrate {\n const availableIncomingBitrate = now.availableIncomingBitrate;\n const availableOutgoingBitrate = now.availableOutgoingBitrate;\n\n return {\n download: availableIncomingBitrate ? Math.round(availableIncomingBitrate / 1000) : 0,\n upload: availableOutgoingBitrate ? Math.round(availableOutgoingBitrate / 1000) : 0,\n };\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA,OAAO,MAAM,sBAAsB,CAAC;EAChC,OAAc,oBAAoB,CAAC,GAA6B,EAAW;IACvE,MAAM,wBAAwB,GAAG,GAAG,CAAC,wBAAwB;IAC7D,MAAM,wBAAwB,GAAG,GAAG,CAAC,wBAAwB;IAE7D,OAAO;MACH,QAAQ,EAAE,wBAAwB,GAAG,IAAI,CAAC,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,GAAG,CAAC;MACpF,MAAM,EAAE,wBAAwB,GAAG,IAAI,CAAC,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,GAAG;IACrF,CAAC;EACL;AACJ","ignoreList":[]}

View File

@@ -0,0 +1,7 @@
import { type ConnectionStatsReport, type TrackID } from "./statsReport.ts";
import { type MediaTrackStats } from "./media/mediaTrackStats.ts";
export declare class ConnectionStatsReportBuilder {
static build(stats: Map<TrackID, MediaTrackStats>): ConnectionStatsReport;
private static calculatePacketLoss;
}
//# sourceMappingURL=connectionStatsReportBuilder.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"connectionStatsReportBuilder.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/connectionStatsReportBuilder.ts"],"names":[],"mappings":"AAeA,OAAO,EAGH,KAAK,qBAAqB,EAG1B,KAAK,OAAO,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,eAAe,EAAmB,MAAM,4BAA4B,CAAC;AAEnF,qBAAa,4BAA4B;IACrC,OAAc,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,qBAAqB,CAwG/E;IAED,OAAO,CAAC,MAAM,CAAC,mBAAmB;CAOrC"}

View File

@@ -0,0 +1,121 @@
/*
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.
*/
export class ConnectionStatsReportBuilder {
static build(stats) {
const report = {};
// process stats
const totalPackets = {
download: 0,
upload: 0
};
const lostPackets = {
download: 0,
upload: 0
};
let bitrateDownload = 0;
let bitrateUpload = 0;
const resolutions = {
local: new Map(),
remote: new Map()
};
const framerates = {
local: new Map(),
remote: new Map()
};
const codecs = {
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();
const type = loss.isDownloadStream ? "download" : "upload";
totalPackets[type] += loss.packetsTotal;
lostPackets[type] += loss.packetsLost;
// process bitrate stats
bitrateDownload += trackStats.getBitrate().download;
bitrateUpload += trackStats.getBitrate().upload;
// 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 {
videoBitrateDownload += trackStats.getBitrate().download;
videoBitrateUpload += trackStats.getBitrate().upload;
}
resolutions[trackStats.getType()].set(trackId, trackStats.getResolution());
framerates[trackStats.getType()].set(trackId, trackStats.getFramerate());
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();
}
report.bitrate = {
upload: bitrateUpload,
download: bitrateDownload
};
report.bitrate.audio = {
upload: audioBitrateUpload,
download: audioBitrateDownload
};
report.bitrate.video = {
upload: videoBitrateUpload,
download: videoBitrateDownload
};
report.packetLoss = {
total: ConnectionStatsReportBuilder.calculatePacketLoss(lostPackets.download + lostPackets.upload, totalPackets.download + totalPackets.upload),
download: ConnectionStatsReportBuilder.calculatePacketLoss(lostPackets.download, totalPackets.download),
upload: ConnectionStatsReportBuilder.calculatePacketLoss(lostPackets.upload, totalPackets.upload)
};
report.audioConcealment = audioConcealment;
report.totalAudioConcealment = {
concealedAudio: totalConcealedAudio,
totalAudioDuration
};
report.framerate = framerates;
report.resolution = resolutions;
report.codec = codecs;
report.jitter = jitter;
return report;
}
static calculatePacketLoss(lostPackets, totalPackets) {
if (!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0) {
return 0;
}
return Math.round(lostPackets / totalPackets * 100);
}
}
//# sourceMappingURL=connectionStatsReportBuilder.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
import { CallStatsReportGatherer } from "./callStatsReportGatherer.ts";
import { StatsReportEmitter } from "./statsReportEmitter.ts";
export declare class GroupCallStats {
private groupCallId;
private userId;
private interval;
private timer;
private readonly gatherers;
readonly reports: StatsReportEmitter;
private readonly summaryStatsReportGatherer;
constructor(groupCallId: string, userId: string, interval?: number);
start(): void;
stop(): void;
hasStatsReportGatherer(callId: string): boolean;
addStatsReportGatherer(callId: string, opponentMemberId: string, peerConnection: RTCPeerConnection): boolean;
removeStatsReportGatherer(callId: string): boolean;
getStatsReportGatherer(callId: string): CallStatsReportGatherer | undefined;
updateOpponentMember(callId: string, opponentMember: string): void;
private processStats;
setInterval(interval: number): void;
}
//# sourceMappingURL=groupCallStats.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"groupCallStats.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/groupCallStats.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAK7D,qBAAa,cAAc;IAOnB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;IARpB,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoF;IAC9G,SAAgB,OAAO,qBAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAgD;IAE3F,YACY,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,MAAc,EAChC;IAEG,KAAK,IAAI,IAAI,CAMnB;IAEM,IAAI,IAAI,IAAI,CAKlB;IAEM,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;IAEM,sBAAsB,CACzB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,cAAc,EAAE,iBAAiB,GAClC,OAAO,CAMT;IAEM,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD;IAEM,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAEjF;IAEM,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAExE;IAED,OAAO,CAAC,YAAY;IAab,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzC;CACJ"}

View File

@@ -0,0 +1,76 @@
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 { CallStatsReportGatherer } from "./callStatsReportGatherer.js";
import { StatsReportEmitter } from "./statsReportEmitter.js";
import { SummaryStatsReportGatherer } from "./summaryStatsReportGatherer.js";
import { logger } from "../../logger.js";
export class GroupCallStats {
constructor(groupCallId, userId, interval = 10000) {
_defineProperty(this, "timer", void 0);
_defineProperty(this, "gatherers", new Map());
_defineProperty(this, "reports", new StatsReportEmitter());
_defineProperty(this, "summaryStatsReportGatherer", new SummaryStatsReportGatherer(this.reports));
this.groupCallId = groupCallId;
this.userId = userId;
this.interval = interval;
}
start() {
if (this.timer === undefined && this.interval > 0) {
this.timer = setInterval(() => {
this.processStats();
}, this.interval);
}
}
stop() {
if (this.timer !== undefined) {
clearInterval(this.timer);
this.gatherers.forEach(c => c.stopProcessingStats());
}
}
hasStatsReportGatherer(callId) {
return this.gatherers.has(callId);
}
addStatsReportGatherer(callId, opponentMemberId, peerConnection) {
if (this.hasStatsReportGatherer(callId)) {
return false;
}
this.gatherers.set(callId, new CallStatsReportGatherer(callId, opponentMemberId, peerConnection, this.reports));
return true;
}
removeStatsReportGatherer(callId) {
return this.gatherers.delete(callId);
}
getStatsReportGatherer(callId) {
return this.hasStatsReportGatherer(callId) ? this.gatherers.get(callId) : undefined;
}
updateOpponentMember(callId, opponentMember) {
this.getStatsReportGatherer(callId)?.setOpponentMemberId(opponentMember);
}
processStats() {
const summary = [];
this.gatherers.forEach(c => {
summary.push(c.processStats(this.groupCallId, this.userId));
});
Promise.all(summary).then(s => this.summaryStatsReportGatherer.build(s)).catch(err => {
logger.error("Could not build summary stats report", err);
});
}
setInterval(interval) {
this.interval = interval;
}
}
//# sourceMappingURL=groupCallStats.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
export type Mid = string;
export type Ssrc = string;
export type MapType = "local" | "remote";
export declare class MediaSsrcHandler {
private readonly ssrcToMid;
findMidBySsrc(ssrc: Ssrc, type: "local" | "remote"): Mid | undefined;
parse(description: string, type: MapType): void;
getSsrcToMidMap(type: MapType): Map<Mid, Ssrc[]>;
}
//# sourceMappingURL=mediaSsrcHandler.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mediaSsrcHandler.d.ts","sourceRoot":"","sources":["../../../../src/webrtc/stats/media/mediaSsrcHandler.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC;AACzB,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC;AAC1B,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzC,qBAAa,gBAAgB;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqE;IAExF,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,GAAG,GAAG,SAAS,CAS1E;IAEM,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAerD;IAEM,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAEtD;CACJ"}

View File

@@ -0,0 +1,56 @@
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 { parse as parseSdp } from "sdp-transform";
export class MediaSsrcHandler {
constructor() {
_defineProperty(this, "ssrcToMid", {
local: new Map(),
remote: new Map()
});
}
findMidBySsrc(ssrc, type) {
let mid;
this.ssrcToMid[type].forEach((ssrcs, m) => {
if (ssrcs.find(s => s == ssrc)) {
mid = m;
return;
}
});
return mid;
}
parse(description, type) {
const sdp = parseSdp(description);
const ssrcToMid = new Map();
sdp.media.forEach(m => {
if (!!m.mid && m.type === "video" || m.type === "audio") {
const ssrcs = [];
m.ssrcs?.forEach(ssrc => {
if (ssrc.attribute === "cname") {
ssrcs.push(`${ssrc.id}`);
}
});
ssrcToMid.set(`${m.mid}`, ssrcs);
}
});
this.ssrcToMid[type] = ssrcToMid;
}
getSsrcToMidMap(type) {
return this.ssrcToMid[type];
}
}
//# sourceMappingURL=mediaSsrcHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mediaSsrcHandler.js","names":[],"sources":["../../../../src/webrtc/stats/media/mediaSsrcHandler.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { parse as parseSdp } from \"sdp-transform\";\n\nexport type Mid = string;\nexport type Ssrc = string;\nexport type MapType = \"local\" | \"remote\";\n\nexport class MediaSsrcHandler {\n private readonly ssrcToMid = { local: new Map<Mid, Ssrc[]>(), remote: new Map<Mid, Ssrc[]>() };\n\n public findMidBySsrc(ssrc: Ssrc, type: \"local\" | \"remote\"): Mid | undefined {\n let mid: Mid | undefined;\n this.ssrcToMid[type].forEach((ssrcs, m) => {\n if (ssrcs.find((s) => s == ssrc)) {\n mid = m;\n return;\n }\n });\n return mid;\n }\n\n public parse(description: string, type: MapType): void {\n const sdp = parseSdp(description);\n const ssrcToMid = new Map<Mid, Ssrc[]>();\n sdp.media.forEach((m) => {\n if ((!!m.mid && m.type === \"video\") || m.type === \"audio\") {\n const ssrcs: Ssrc[] = [];\n m.ssrcs?.forEach((ssrc) => {\n if (ssrc.attribute === \"cname\") {\n ssrcs.push(`${ssrc.id}`);\n }\n });\n ssrcToMid.set(`${m.mid}`, ssrcs);\n }\n });\n this.ssrcToMid[type] = ssrcToMid;\n }\n\n public getSsrcToMidMap(type: MapType): Map<Mid, Ssrc[]> {\n return this.ssrcToMid[type];\n }\n}\n"],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAAS,KAAK,IAAI,QAAQ,QAAQ,eAAe;AAMjD,OAAO,MAAM,gBAAgB,CAAC;EAAA;IAAA,mCACG;MAAE,KAAK,EAAE,IAAI,GAAG,CAAc,CAAC;MAAE,MAAM,EAAE,IAAI,GAAG,CAAc;IAAE,CAAC;EAAA;EAEvF,aAAa,CAAC,IAAU,EAAE,IAAwB,EAAmB;IACxE,IAAI,GAAoB;IACxB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK;MACvC,IAAI,KAAK,CAAC,IAAI,CAAE,CAAC,IAAK,CAAC,IAAI,IAAI,CAAC,EAAE;QAC9B,GAAG,GAAG,CAAC;QACP;MACJ;IACJ,CAAC,CAAC;IACF,OAAO,GAAG;EACd;EAEO,KAAK,CAAC,WAAmB,EAAE,IAAa,EAAQ;IACnD,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC;IACjC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAc,CAAC;IACxC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAE,CAAC,IAAK;MACrB,IAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,IAAK,CAAC,CAAC,IAAI,KAAK,OAAO,EAAE;QACvD,MAAM,KAAa,GAAG,EAAE;QACxB,CAAC,CAAC,KAAK,EAAE,OAAO,CAAE,IAAI,IAAK;UACvB,IAAI,IAAI,CAAC,SAAS,KAAK,OAAO,EAAE;YAC5B,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;UAC5B;QACJ,CAAC,CAAC;QACF,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC;MACpC;IACJ,CAAC,CAAC;IACF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,SAAS;EACpC;EAEO,eAAe,CAAC,IAAa,EAAoB;IACpD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;EAC/B;AACJ","ignoreList":[]}

View File

@@ -0,0 +1,12 @@
export type TrackId = string;
export declare class MediaTrackHandler {
private readonly pc;
constructor(pc: RTCPeerConnection);
getLocalTracks(kind: "audio" | "video"): MediaStreamTrack[];
getTackById(trackId: string): MediaStreamTrack | undefined;
getLocalTrackIdByMid(mid: string): string | undefined;
getRemoteTrackIdByMid(mid: string): string | undefined;
getActiveSimulcastStreams(): number;
getTransceiverByTrackId(trackId: TrackId): RTCRtpTransceiver | undefined;
}
//# sourceMappingURL=mediaTrackHandler.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mediaTrackHandler.d.ts","sourceRoot":"","sources":["../../../../src/webrtc/stats/media/mediaTrackHandler.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,qBAAa,iBAAiB;IACP,OAAO,CAAC,QAAQ,CAAC,EAAE;IAAtC,YAAoC,EAAE,EAAE,iBAAiB,EAAI;IAEtD,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,gBAAgB,EAAE,CAWjE;IAEM,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAahE;IAEM,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG3D;IAEM,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG5D;IAEM,yBAAyB,IAAI,MAAM,CAGzC;IAEM,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAI9E;CACJ"}

View File

@@ -0,0 +1,56 @@
/*
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.
*/
export class MediaTrackHandler {
constructor(pc) {
this.pc = pc;
}
getLocalTracks(kind) {
const isNotNullAndKind = track => {
return track !== null && track.kind === kind;
};
return this.pc.getTransceivers().filter(t => t.currentDirection === "sendonly" || t.currentDirection === "sendrecv").filter(t => t.sender !== null).map(t => t.sender).map(s => s.track).filter(isNotNullAndKind);
}
getTackById(trackId) {
return this.pc.getTransceivers().map(t => {
if (t?.sender.track !== null && t.sender.track.id === trackId) {
return t.sender.track;
}
if (t?.receiver.track !== null && t.receiver.track.id === trackId) {
return t.receiver.track;
}
return undefined;
}).find(t => t !== undefined);
}
getLocalTrackIdByMid(mid) {
const transceiver = this.pc.getTransceivers().find(t => t.mid === mid);
return transceiver?.sender?.track?.id;
}
getRemoteTrackIdByMid(mid) {
const transceiver = this.pc.getTransceivers().find(t => t.mid === mid);
return transceiver?.receiver?.track?.id;
}
getActiveSimulcastStreams() {
//@TODO implement this right.. Check how many layer configured
return 3;
}
getTransceiverByTrackId(trackId) {
return this.pc.getTransceivers().find(t => {
return t.receiver.track.id === trackId || t.sender.track !== null && t.sender.track.id === trackId;
});
}
}
//# sourceMappingURL=mediaTrackHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mediaTrackHandler.js","names":[],"sources":["../../../../src/webrtc/stats/media/mediaTrackHandler.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport type TrackId = string;\n\nexport class MediaTrackHandler {\n public constructor(private readonly pc: RTCPeerConnection) {}\n\n public getLocalTracks(kind: \"audio\" | \"video\"): MediaStreamTrack[] {\n const isNotNullAndKind = (track: MediaStreamTrack | null): boolean => {\n return track !== null && track.kind === kind;\n };\n return this.pc\n .getTransceivers()\n .filter((t) => t.currentDirection === \"sendonly\" || t.currentDirection === \"sendrecv\")\n .filter((t) => t.sender !== null)\n .map((t) => t.sender)\n .map((s) => s.track)\n .filter(isNotNullAndKind) as MediaStreamTrack[];\n }\n\n public getTackById(trackId: string): MediaStreamTrack | undefined {\n return this.pc\n .getTransceivers()\n .map((t) => {\n if (t?.sender.track !== null && t.sender.track.id === trackId) {\n return t.sender.track;\n }\n if (t?.receiver.track !== null && t.receiver.track.id === trackId) {\n return t.receiver.track;\n }\n return undefined;\n })\n .find((t) => t !== undefined);\n }\n\n public getLocalTrackIdByMid(mid: string): string | undefined {\n const transceiver = this.pc.getTransceivers().find((t) => t.mid === mid);\n return transceiver?.sender?.track?.id;\n }\n\n public getRemoteTrackIdByMid(mid: string): string | undefined {\n const transceiver = this.pc.getTransceivers().find((t) => t.mid === mid);\n return transceiver?.receiver?.track?.id;\n }\n\n public getActiveSimulcastStreams(): number {\n //@TODO implement this right.. Check how many layer configured\n return 3;\n }\n\n public getTransceiverByTrackId(trackId: TrackId): RTCRtpTransceiver | undefined {\n return this.pc.getTransceivers().find((t) => {\n return t.receiver.track.id === trackId || (t.sender.track !== null && t.sender.track.id === trackId);\n });\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA,OAAO,MAAM,iBAAiB,CAAC;EACpB,WAAW,CAAkB,EAAqB,EAAE;IAAA,KAAvB,EAAqB,GAArB,EAAqB;EAAG;EAErD,cAAc,CAAC,IAAuB,EAAsB;IAC/D,MAAM,gBAAgB,GAAI,KAA8B,IAAc;MAClE,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;IAChD,CAAC;IACD,OAAO,IAAI,CAAC,EAAE,CACT,eAAe,CAAC,CAAC,CACjB,MAAM,CAAE,CAAC,IAAK,CAAC,CAAC,gBAAgB,KAAK,UAAU,IAAI,CAAC,CAAC,gBAAgB,KAAK,UAAU,CAAC,CACrF,MAAM,CAAE,CAAC,IAAK,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,CAChC,GAAG,CAAE,CAAC,IAAK,CAAC,CAAC,MAAM,CAAC,CACpB,GAAG,CAAE,CAAC,IAAK,CAAC,CAAC,KAAK,CAAC,CACnB,MAAM,CAAC,gBAAgB,CAAC;EACjC;EAEO,WAAW,CAAC,OAAe,EAAgC;IAC9D,OAAO,IAAI,CAAC,EAAE,CACT,eAAe,CAAC,CAAC,CACjB,GAAG,CAAE,CAAC,IAAK;MACR,IAAI,CAAC,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE;QAC3D,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK;MACzB;MACA,IAAI,CAAC,EAAE,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE;QAC/D,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK;MAC3B;MACA,OAAO,SAAS;IACpB,CAAC,CAAC,CACD,IAAI,CAAE,CAAC,IAAK,CAAC,KAAK,SAAS,CAAC;EACrC;EAEO,oBAAoB,CAAC,GAAW,EAAsB;IACzD,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAE,CAAC,IAAK,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC;IACxE,OAAO,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;EACzC;EAEO,qBAAqB,CAAC,GAAW,EAAsB;IAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAE,CAAC,IAAK,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC;IACxE,OAAO,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;EAC3C;EAEO,yBAAyB,GAAW;IACvC;IACA,OAAO,CAAC;EACZ;EAEO,uBAAuB,CAAC,OAAgB,EAAiC;IAC5E,OAAO,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAE,CAAC,IAAK;MACzC,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,IAAK,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,OAAQ;IACxG,CAAC,CAAC;EACN;AACJ","ignoreList":[]}

View File

@@ -0,0 +1,86 @@
import { type AudioConcealment } from "../statsReport.ts";
import { type TrackId } from "./mediaTrackHandler.ts";
export interface PacketLoss {
packetsTotal: number;
packetsLost: number;
isDownloadStream: boolean;
}
export interface Bitrate {
/**
* bytes per second
*/
download: number;
/**
* bytes per second
*/
upload: number;
}
export interface ConcealedAudio {
/**
* duration in ms
*/
duration: number;
ratio: number;
}
export interface Resolution {
width: number;
height: number;
}
export type TrackStatsType = "local" | "remote";
export declare class MediaTrackStats {
readonly trackId: TrackId;
readonly type: TrackStatsType;
readonly kind: "audio" | "video";
private loss;
private bitrate;
private resolution;
private audioConcealment;
private framerate;
private jitter;
private codec;
private isAlive;
private isMuted;
private isEnabled;
constructor(trackId: TrackId, type: TrackStatsType, kind: "audio" | "video");
getType(): TrackStatsType;
setLoss(loss: PacketLoss): void;
getLoss(): PacketLoss;
setResolution(resolution: Resolution): void;
getResolution(): Resolution;
setFramerate(framerate: number): void;
getFramerate(): number;
setBitrate(bitrate: Bitrate): void;
getBitrate(): Bitrate;
setCodec(codecShortType: string): boolean;
getCodec(): string;
resetBitrate(): void;
set alive(isAlive: boolean);
/**
* A MediaTrackState is alive if the corresponding MediaStreamTrack track bound to a transceiver and the
* MediaStreamTrack is in state MediaStreamTrack.readyState === live
*/
get alive(): boolean;
set muted(isMuted: boolean);
/**
* A MediaTrackState.isMuted corresponding to MediaStreamTrack.muted.
* But these values only match if MediaTrackState.isAlive.
*/
get muted(): boolean;
set enabled(isEnabled: boolean);
/**
* A MediaTrackState.isEnabled corresponding to MediaStreamTrack.enabled.
* But these values only match if MediaTrackState.isAlive.
*/
get enabled(): boolean;
setJitter(jitter: number): void;
/**
* Jitter in milliseconds
*/
getJitter(): number;
/**
* Audio concealment ration (conceled duration / total duration)
*/
setAudioConcealment(concealedAudioDuration: number, totalAudioDuration: number): void;
getAudioConcealment(): AudioConcealment;
}
//# sourceMappingURL=mediaTrackStats.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mediaTrackStats.d.ts","sourceRoot":"","sources":["../../../../src/webrtc/stats/media/mediaTrackStats.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEtD,MAAM,WAAW,UAAU;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACpB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,cAAc;IAC3B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB,KAAK,EAAE,MAAM,CAAC;CACjB;AACD,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhD,qBAAa,eAAe;aAaJ,OAAO,EAAE,OAAO;aAChB,IAAI,EAAE,cAAc;aACpB,IAAI,EAAE,OAAO,GAAG,OAAO;IAd3C,OAAO,CAAC,IAAI,CAA4E;IACxF,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,UAAU,CAAyC;IAC3D,OAAO,CAAC,gBAAgB,CAAkE;IAC1F,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAQ;IAEzB,YACoB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,OAAO,GAAG,OAAO,EACvC;IAEG,OAAO,IAAI,cAAc,CAE/B;IAEM,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAErC;IAEM,OAAO,IAAI,UAAU,CAE3B;IAEM,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAEjD;IAEM,aAAa,IAAI,UAAU,CAEjC;IAEM,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3C;IAEM,YAAY,IAAI,MAAM,CAE5B;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExC;IAEM,UAAU,IAAI,OAAO,CAE3B;IAEM,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAG/C;IAEM,QAAQ,IAAI,MAAM,CAExB;IAEM,YAAY,IAAI,IAAI,CAE1B;IAED,IAAW,KAAK,CAAC,OAAO,EAAE,OAAO,EAEhC;IAED;;;OAGG;IACH,IAAW,KAAK,IAAI,OAAO,CAE1B;IAED,IAAW,KAAK,CAAC,OAAO,EAAE,OAAO,EAEhC;IAED;;;OAGG;IACH,IAAW,KAAK,IAAI,OAAO,CAE1B;IAED,IAAW,OAAO,CAAC,SAAS,EAAE,OAAO,EAEpC;IAED;;;OAGG;IACH,IAAW,OAAO,IAAI,OAAO,CAE5B;IAEM,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAErC;IAED;;OAEG;IACI,SAAS,IAAI,MAAM,CAEzB;IAED;;OAEG;IACI,mBAAmB,CAAC,sBAAsB,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAG3F;IAEM,mBAAmB,IAAI,gBAAgB,CAE7C;CACJ"}

View File

@@ -0,0 +1,142 @@
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.
*/
export class MediaTrackStats {
constructor(trackId, type, kind) {
_defineProperty(this, "loss", {
packetsTotal: 0,
packetsLost: 0,
isDownloadStream: false
});
_defineProperty(this, "bitrate", {
download: 0,
upload: 0
});
_defineProperty(this, "resolution", {
width: -1,
height: -1
});
_defineProperty(this, "audioConcealment", {
concealedAudio: 0,
totalAudioDuration: 0
});
_defineProperty(this, "framerate", 0);
_defineProperty(this, "jitter", 0);
_defineProperty(this, "codec", "");
_defineProperty(this, "isAlive", true);
_defineProperty(this, "isMuted", false);
_defineProperty(this, "isEnabled", true);
this.trackId = trackId;
this.type = type;
this.kind = kind;
}
getType() {
return this.type;
}
setLoss(loss) {
this.loss = loss;
}
getLoss() {
return this.loss;
}
setResolution(resolution) {
this.resolution = resolution;
}
getResolution() {
return this.resolution;
}
setFramerate(framerate) {
this.framerate = framerate;
}
getFramerate() {
return this.framerate;
}
setBitrate(bitrate) {
this.bitrate = bitrate;
}
getBitrate() {
return this.bitrate;
}
setCodec(codecShortType) {
this.codec = codecShortType;
return true;
}
getCodec() {
return this.codec;
}
resetBitrate() {
this.bitrate = {
download: 0,
upload: 0
};
}
set alive(isAlive) {
this.isAlive = isAlive;
}
/**
* A MediaTrackState is alive if the corresponding MediaStreamTrack track bound to a transceiver and the
* MediaStreamTrack is in state MediaStreamTrack.readyState === live
*/
get alive() {
return this.isAlive;
}
set muted(isMuted) {
this.isMuted = isMuted;
}
/**
* A MediaTrackState.isMuted corresponding to MediaStreamTrack.muted.
* But these values only match if MediaTrackState.isAlive.
*/
get muted() {
return this.isMuted;
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
}
/**
* A MediaTrackState.isEnabled corresponding to MediaStreamTrack.enabled.
* But these values only match if MediaTrackState.isAlive.
*/
get enabled() {
return this.isEnabled;
}
setJitter(jitter) {
this.jitter = jitter;
}
/**
* Jitter in milliseconds
*/
getJitter() {
return this.jitter;
}
/**
* Audio concealment ration (conceled duration / total duration)
*/
setAudioConcealment(concealedAudioDuration, totalAudioDuration) {
this.audioConcealment.concealedAudio = concealedAudioDuration;
this.audioConcealment.totalAudioDuration = totalAudioDuration;
}
getAudioConcealment() {
return this.audioConcealment;
}
}
//# sourceMappingURL=mediaTrackStats.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
import { type TrackID } from "../statsReport.ts";
import { MediaTrackStats } from "./mediaTrackStats.ts";
import { type MediaTrackHandler, type TrackId } from "./mediaTrackHandler.ts";
import { type MediaSsrcHandler } from "./mediaSsrcHandler.ts";
export declare class MediaTrackStatsHandler {
readonly mediaSsrcHandler: MediaSsrcHandler;
readonly mediaTrackHandler: MediaTrackHandler;
private readonly track2stats;
constructor(mediaSsrcHandler: MediaSsrcHandler, mediaTrackHandler: MediaTrackHandler);
/**
* Find tracks by rtc stats
* Argument report is any because the stats api is not consistent:
* For example `trackIdentifier`, `mid` not existing in every implementations
* https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats
* https://developer.mozilla.org/en-US/docs/Web/API/RTCInboundRtpStreamStats
*/
findTrack2Stats(report: any, type: "remote" | "local"): MediaTrackStats | undefined;
findLocalVideoTrackStats(report: any): MediaTrackStats | undefined;
getTrack2stats(): Map<TrackID, MediaTrackStats>;
findTransceiverByTrackId(trackID: TrackId): undefined | RTCRtpTransceiver;
}
//# sourceMappingURL=mediaTrackStatsHandler.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mediaTrackStatsHandler.d.ts","sourceRoot":"","sources":["../../../../src/webrtc/stats/media/mediaTrackStatsHandler.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,qBAAa,sBAAsB;aAIX,gBAAgB,EAAE,gBAAgB;aAClC,iBAAiB,EAAE,iBAAiB;IAJxD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuC;IAEnE,YACoB,gBAAgB,EAAE,gBAAgB,EAClC,iBAAiB,EAAE,iBAAiB,EACpD;IAEJ;;;;;;OAMG;IACI,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,eAAe,GAAG,SAAS,CAqCzF;IAEM,wBAAwB,CAAC,MAAM,EAAE,GAAG,GAAG,eAAe,GAAG,SAAS,CAMxE;IAEM,cAAc,IAAI,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,CAErD;IAEM,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,iBAAiB,CAE/E;CACJ"}

View File

@@ -0,0 +1,76 @@
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 { MediaTrackStats } from "./mediaTrackStats.js";
export class MediaTrackStatsHandler {
constructor(mediaSsrcHandler, mediaTrackHandler) {
_defineProperty(this, "track2stats", new Map());
this.mediaSsrcHandler = mediaSsrcHandler;
this.mediaTrackHandler = mediaTrackHandler;
}
/**
* Find tracks by rtc stats
* Argument report is any because the stats api is not consistent:
* For example `trackIdentifier`, `mid` not existing in every implementations
* https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats
* https://developer.mozilla.org/en-US/docs/Web/API/RTCInboundRtpStreamStats
*/
findTrack2Stats(report, type) {
let trackID;
if (report.trackIdentifier) {
trackID = report.trackIdentifier;
} else if (report.mid) {
trackID = type === "remote" ? this.mediaTrackHandler.getRemoteTrackIdByMid(report.mid) : this.mediaTrackHandler.getLocalTrackIdByMid(report.mid);
} else if (report.ssrc) {
const mid = this.mediaSsrcHandler.findMidBySsrc(report.ssrc, type);
if (!mid) {
return undefined;
}
trackID = type === "remote" ? this.mediaTrackHandler.getRemoteTrackIdByMid(report.mid) : this.mediaTrackHandler.getLocalTrackIdByMid(report.mid);
}
if (!trackID) {
return undefined;
}
let trackStats = this.track2stats.get(trackID);
if (!trackStats) {
const track = this.mediaTrackHandler.getTackById(trackID);
if (track !== undefined) {
const kind = track.kind === "audio" ? track.kind : "video";
trackStats = new MediaTrackStats(trackID, type, kind);
this.track2stats.set(trackID, trackStats);
} else {
return undefined;
}
}
return trackStats;
}
findLocalVideoTrackStats(report) {
const localVideoTracks = this.mediaTrackHandler.getLocalTracks("video");
if (localVideoTracks.length === 0) {
return undefined;
}
return this.findTrack2Stats(report, "local");
}
getTrack2stats() {
return this.track2stats;
}
findTransceiverByTrackId(trackID) {
return this.mediaTrackHandler.getTransceiverByTrackId(trackID);
}
}
//# sourceMappingURL=mediaTrackStatsHandler.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,99 @@
import { type ConnectionStatsBandwidth, type ConnectionStatsBitrate, type PacketLoss } from "./connectionStats.ts";
import { type TransportStats } from "./transportStats.ts";
import { type Resolution } from "./media/mediaTrackStats.ts";
export declare enum StatsReport {
CONNECTION_STATS = "StatsReport.connection_stats",
CALL_FEED_REPORT = "StatsReport.call_feed_report",
BYTE_SENT_STATS = "StatsReport.byte_sent_stats",
SUMMARY_STATS = "StatsReport.summary_stats"
}
export interface ByteSentStatsReport extends Map<TrackID, ByteSend> {
callId?: string;
opponentMemberId?: string;
}
export type TrackID = string;
export type ByteSend = number;
export interface ConnectionStatsReport {
callId?: string;
opponentMemberId?: string;
bandwidth: ConnectionStatsBandwidth;
bitrate: ConnectionStatsBitrate;
packetLoss: PacketLoss;
audioConcealment: Map<TrackID, AudioConcealment>;
totalAudioConcealment: AudioConcealment;
resolution: ResolutionMap;
framerate: FramerateMap;
codec: CodecMap;
jitter: Map<TrackID, number>;
transport: TransportStats[];
}
export interface AudioConcealment {
concealedAudio: number;
totalAudioDuration: number;
}
export interface ResolutionMap {
local: Map<TrackID, Resolution>;
remote: Map<TrackID, Resolution>;
}
export interface FramerateMap {
local: Map<TrackID, number>;
remote: Map<TrackID, number>;
}
export interface CodecMap {
local: Map<TrackID, string>;
remote: Map<TrackID, string>;
}
export interface SummaryStatsReport {
/**
* Aggregated the information for percentage of received media
*
* This measure whether the current user receive data from a call participants.
* As soon as a participant sends at least a byte media to this user, this counts as one measurement unit.
* The units of measure divided by the total number of participants is a value between 0 and 1.
*/
percentageReceivedMedia: number;
percentageReceivedAudioMedia: number;
percentageReceivedVideoMedia: number;
maxJitter: number;
maxPacketLoss: number;
percentageConcealedAudio: number;
peerConnections: number;
opponentUsersInCall?: number;
opponentDevicesInCall?: number;
diffDevicesToPeerConnections?: number;
ratioPeerConnectionToDevices?: number;
}
export interface CallFeedReport {
callId: string;
opponentMemberId: string;
transceiver: TransceiverStats[];
callFeeds: CallFeedStats[];
}
export interface CallFeedStats {
stream: string;
type: "remote" | "local";
audio: TrackStats | null;
video: TrackStats | null;
purpose: string;
prefix: string;
isVideoMuted: boolean;
isAudioMuted: boolean;
}
export interface TransceiverStats {
readonly mid: string;
readonly sender: TrackStats | null;
readonly receiver: TrackStats | null;
readonly direction: string;
readonly currentDirection: string;
}
export interface TrackStats {
readonly id: string;
readonly kind: "audio" | "video";
readonly settingDeviceId: string;
readonly constrainDeviceId: string;
readonly muted: boolean;
readonly enabled: boolean;
readonly readyState: "ended" | "live";
readonly label: string;
}
//# sourceMappingURL=statsReport.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"statsReport.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/statsReport.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,wBAAwB,EAAE,KAAK,sBAAsB,EAAE,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACnH,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE7D,oBAAY,WAAW;IACnB,gBAAgB,iCAAiC;IACjD,gBAAgB,iCAAiC;IACjD,eAAe,gCAAgC;IAC/C,aAAa,8BAA8B;CAC9C;AAGD,MAAM,WAAW,mBAAoB,SAAQ,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAE7B;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAG9B,MAAM,WAAW,qBAAqB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,wBAAwB,CAAC;IACpC,OAAO,EAAE,sBAAsB,CAAC;IAChC,UAAU,EAAE,UAAU,CAAC;IACvB,gBAAgB,EAAE,GAAG,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IACjD,qBAAqB,EAAE,gBAAgB,CAAC;IACxC,UAAU,EAAE,aAAa,CAAC;IAC1B,SAAS,EAAE,YAAY,CAAC;IACxB,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7B,SAAS,EAAE,cAAc,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;CAChC;AAGD,MAAM,WAAW,kBAAkB;IAC/B;;;;;;OAMG;IACH,uBAAuB,EAAE,MAAM,CAAC;IAChC,4BAA4B,EAAE,MAAM,CAAC;IACrC,4BAA4B,EAAE,MAAM,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,wBAAwB,EAAE,MAAM,CAAC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,4BAA4B,CAAC,EAAE,MAAM,CAAC;CAEzC;AAGD,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,SAAS,EAAE,aAAa,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B"}

View File

@@ -0,0 +1,32 @@
/*
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.
*/
export let StatsReport = /*#__PURE__*/function (StatsReport) {
StatsReport["CONNECTION_STATS"] = "StatsReport.connection_stats";
StatsReport["CALL_FEED_REPORT"] = "StatsReport.call_feed_report";
StatsReport["BYTE_SENT_STATS"] = "StatsReport.byte_sent_stats";
StatsReport["SUMMARY_STATS"] = "StatsReport.summary_stats";
return StatsReport;
}({});
/// ByteSentStatsReport ################################################################################################
/// ConnectionStatsReport ##############################################################################################
/// SummaryStatsReport #################################################################################################
/// CallFeedReport #####################################################################################################
//# sourceMappingURL=statsReport.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import { TypedEventEmitter } from "../../models/typed-event-emitter.ts";
import { type ByteSentStatsReport, type CallFeedReport, type ConnectionStatsReport, StatsReport, type SummaryStatsReport } from "./statsReport.ts";
export type StatsReportHandlerMap = {
[StatsReport.BYTE_SENT_STATS]: (report: ByteSentStatsReport) => void;
[StatsReport.CONNECTION_STATS]: (report: ConnectionStatsReport) => void;
[StatsReport.CALL_FEED_REPORT]: (report: CallFeedReport) => void;
[StatsReport.SUMMARY_STATS]: (report: SummaryStatsReport) => void;
};
export declare class StatsReportEmitter extends TypedEventEmitter<StatsReport, StatsReportHandlerMap> {
emitByteSendReport(byteSentStats: ByteSentStatsReport): void;
emitConnectionStatsReport(report: ConnectionStatsReport): void;
emitCallFeedReport(report: CallFeedReport): void;
emitSummaryStatsReport(report: SummaryStatsReport): void;
}
//# sourceMappingURL=statsReportEmitter.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"statsReportEmitter.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/statsReportEmitter.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EACH,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,WAAW,EACX,KAAK,kBAAkB,EAC1B,MAAM,kBAAkB,CAAC;AAE1B,MAAM,MAAM,qBAAqB,GAAG;IAChC,CAAC,WAAW,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACrE,CAAC,WAAW,CAAC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACxE,CAAC,WAAW,CAAC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACjE,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACrE,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,iBAAiB,CAAC,WAAW,EAAE,qBAAqB,CAAC;IAClF,kBAAkB,CAAC,aAAa,EAAE,mBAAmB,GAAG,IAAI,CAElE;IAEM,yBAAyB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAEpE;IAEM,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAEtD;IAEM,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAE9D;CACJ"}

View File

@@ -0,0 +1,33 @@
/*
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 { TypedEventEmitter } from "../../models/typed-event-emitter.js";
import { StatsReport } from "./statsReport.js";
export class StatsReportEmitter extends TypedEventEmitter {
emitByteSendReport(byteSentStats) {
this.emit(StatsReport.BYTE_SENT_STATS, byteSentStats);
}
emitConnectionStatsReport(report) {
this.emit(StatsReport.CONNECTION_STATS, report);
}
emitCallFeedReport(report) {
this.emit(StatsReport.CALL_FEED_REPORT, report);
}
emitSummaryStatsReport(report) {
this.emit(StatsReport.SUMMARY_STATS, report);
}
}
//# sourceMappingURL=statsReportEmitter.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"statsReportEmitter.js","names":[],"sources":["../../../src/webrtc/stats/statsReportEmitter.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { TypedEventEmitter } from \"../../models/typed-event-emitter.ts\";\nimport {\n type ByteSentStatsReport,\n type CallFeedReport,\n type ConnectionStatsReport,\n StatsReport,\n type SummaryStatsReport,\n} from \"./statsReport.ts\";\n\nexport type StatsReportHandlerMap = {\n [StatsReport.BYTE_SENT_STATS]: (report: ByteSentStatsReport) => void;\n [StatsReport.CONNECTION_STATS]: (report: ConnectionStatsReport) => void;\n [StatsReport.CALL_FEED_REPORT]: (report: CallFeedReport) => void;\n [StatsReport.SUMMARY_STATS]: (report: SummaryStatsReport) => void;\n};\n\nexport class StatsReportEmitter extends TypedEventEmitter<StatsReport, StatsReportHandlerMap> {\n public emitByteSendReport(byteSentStats: ByteSentStatsReport): void {\n this.emit(StatsReport.BYTE_SENT_STATS, byteSentStats);\n }\n\n public emitConnectionStatsReport(report: ConnectionStatsReport): void {\n this.emit(StatsReport.CONNECTION_STATS, report);\n }\n\n public emitCallFeedReport(report: CallFeedReport): void {\n this.emit(StatsReport.CALL_FEED_REPORT, report);\n }\n\n public emitSummaryStatsReport(report: SummaryStatsReport): void {\n this.emit(StatsReport.SUMMARY_STATS, report);\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAAS,iBAAiB,QAAQ,qCAAqC;AACvE,SAII,WAAW,QAER,kBAAkB;AASzB,OAAO,MAAM,kBAAkB,SAAS,iBAAiB,CAAqC;EACnF,kBAAkB,CAAC,aAAkC,EAAQ;IAChE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,eAAe,EAAE,aAAa,CAAC;EACzD;EAEO,yBAAyB,CAAC,MAA6B,EAAQ;IAClE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,gBAAgB,EAAE,MAAM,CAAC;EACnD;EAEO,kBAAkB,CAAC,MAAsB,EAAQ;IACpD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,gBAAgB,EAAE,MAAM,CAAC;EACnD;EAEO,sBAAsB,CAAC,MAA0B,EAAQ;IAC5D,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,MAAM,CAAC;EAChD;AACJ","ignoreList":[]}

View File

@@ -0,0 +1,16 @@
import { type StatsReportEmitter } from "./statsReportEmitter.ts";
import { type CallStatsReportSummary } from "./callStatsReportSummary.ts";
import { type SummaryStatsReport } from "./statsReport.ts";
import { type ParticipantState } from "../groupCall.ts";
import { type RoomMember } from "../../matrix.ts";
export declare class SummaryStatsReportGatherer {
private emitter;
constructor(emitter: StatsReportEmitter);
build(allSummary: CallStatsReportSummary[]): void;
static extendSummaryReport(report: SummaryStatsReport, callParticipants: Map<RoomMember, Map<string, ParticipantState>>): void;
private countTrackListReceivedMedia;
private buildMaxJitter;
private buildMaxPacketLoss;
private countConcealedAudio;
}
//# sourceMappingURL=summaryStatsReportGatherer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"summaryStatsReportGatherer.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/summaryStatsReportGatherer.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAUlD,qBAAa,0BAA0B;IAChB,OAAO,CAAC,OAAO;IAAlC,YAA2B,OAAO,EAAE,kBAAkB,EAAI;IAEnD,KAAK,CAAC,UAAU,EAAE,sBAAsB,EAAE,GAAG,IAAI,CA8CvD;IAED,OAAc,mBAAmB,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,gBAAgB,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GACjE,IAAI,CAiBN;IAED,OAAO,CAAC,2BAA2B;IAsBnC,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,mBAAmB;CAI9B"}

View File

@@ -0,0 +1,116 @@
/*
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.
*/
export class SummaryStatsReportGatherer {
constructor(emitter) {
this.emitter = emitter;
}
build(allSummary) {
// Filter all stats which collect the first time webrtc stats.
// Because stats based on time interval and the first collection of a summery stats has no previous
// webrtcStats as basement all the calculation are 0. We don't want track the 0 stats.
const summary = allSummary.filter(s => !s.isFirstCollection);
const summaryTotalCount = summary.length;
// For counting the peer connections we also want to consider the ignored summaries
const peerConnectionsCount = allSummary.length;
if (summaryTotalCount === 0) {
return;
}
const summaryCounter = {
receivedAudio: 0,
receivedVideo: 0,
receivedMedia: 0,
concealedAudio: 0,
totalAudio: 0
};
let maxJitter = 0;
let maxPacketLoss = 0;
summary.forEach(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: 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),
peerConnections: peerConnectionsCount
};
this.emitter.emitSummaryStatsReport(report);
}
static extendSummaryReport(report, callParticipants) {
// Calculate the actual number of devices based on the participants state event
// (this is used, to compare the expected participant count from the room state with the acutal peer connections)
// const devices = callParticipants.()
const devices = [];
const users = [];
for (const userEntry of callParticipants) {
users.push(userEntry);
for (const device of userEntry[1]) {
devices.push(device);
}
}
report.opponentDevicesInCall = Math.max(0, devices.length - 1);
report.opponentUsersInCall = Math.max(0, users.length - 1);
report.diffDevicesToPeerConnections = Math.max(0, devices.length - 1) - report.peerConnections;
report.ratioPeerConnectionToDevices = Math.max(0, devices.length - 1) == 0 ? 0 : report.peerConnections / (devices.length - 1);
}
countTrackListReceivedMedia(counter, stats) {
let hasReceivedAudio = false;
let hasReceivedVideo = false;
if (stats.receivedAudioMedia > 0 || stats.audioTrackSummary.count === 0) {
counter.receivedAudio++;
hasReceivedAudio = true;
}
if (stats.receivedVideoMedia > 0 || stats.videoTrackSummary.count === 0) {
counter.receivedVideo++;
hasReceivedVideo = true;
} else {
if (stats.videoTrackSummary.muted > 0 && stats.videoTrackSummary.muted === stats.videoTrackSummary.count) {
counter.receivedVideo++;
hasReceivedVideo = true;
}
}
if (hasReceivedVideo && hasReceivedAudio) {
counter.receivedMedia++;
}
}
buildMaxJitter(maxJitter, stats) {
if (maxJitter < stats.videoTrackSummary.maxJitter) {
maxJitter = stats.videoTrackSummary.maxJitter;
}
if (maxJitter < stats.audioTrackSummary.maxJitter) {
maxJitter = stats.audioTrackSummary.maxJitter;
}
return maxJitter;
}
buildMaxPacketLoss(maxPacketLoss, stats) {
if (maxPacketLoss < stats.videoTrackSummary.maxPacketLoss) {
maxPacketLoss = stats.videoTrackSummary.maxPacketLoss;
}
if (maxPacketLoss < stats.audioTrackSummary.maxPacketLoss) {
maxPacketLoss = stats.audioTrackSummary.maxPacketLoss;
}
return maxPacketLoss;
}
countConcealedAudio(summaryCounter, stats) {
summaryCounter.concealedAudio += stats.audioTrackSummary.concealedAudio;
summaryCounter.totalAudio += stats.audioTrackSummary.totalAudio;
}
}
//# sourceMappingURL=summaryStatsReportGatherer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
import { type MediaTrackStats } from "./media/mediaTrackStats.ts";
import { type TrackSummary } from "./callStatsReportSummary.ts";
export declare class TrackStatsBuilder {
static buildFramerateResolution(trackStats: MediaTrackStats, now: any): void;
static calculateSimulcastFramerate(trackStats: MediaTrackStats, now: any, before: any, layer: number): void;
static buildCodec(report: RTCStatsReport | undefined, trackStats: MediaTrackStats, now: any): void;
static buildBitrateReceived(trackStats: MediaTrackStats, now: any, before: any): void;
static buildBitrateSend(trackStats: MediaTrackStats, now: any, before: any): void;
static buildPacketsLost(trackStats: MediaTrackStats, now: any, before: any): void;
private static calculateBitrate;
static setTrackStatsState(trackStats: MediaTrackStats, transceiver: RTCRtpTransceiver | undefined): void;
static buildTrackSummary(trackStatsList: MediaTrackStats[]): {
audioTrackSummary: TrackSummary;
videoTrackSummary: TrackSummary;
};
static buildJitter(trackStats: MediaTrackStats, statsReport: any): void;
static buildAudioConcealment(trackStats: MediaTrackStats, statsReport: any): void;
}
//# sourceMappingURL=trackStatsBuilder.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"trackStatsBuilder.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/trackStatsBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,qBAAa,iBAAiB;IAC1B,OAAc,wBAAwB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAWlF;IAED,OAAc,2BAA2B,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAqBjH;IAED,OAAc,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAaxG;IAED,OAAc,oBAAoB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAU3F;IAED,OAAc,gBAAgB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAKvF;IAED,OAAc,gBAAgB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAoBvF;IAED,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAoB/B,OAAc,kBAAkB,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,CAmB9G;IAED,OAAc,iBAAiB,CAAC,cAAc,EAAE,eAAe,EAAE,GAAG;QAChE,iBAAiB,EAAE,YAAY,CAAC;QAChC,iBAAiB,EAAE,YAAY,CAAC;KACnC,CAwCA;IAED,OAAc,WAAW,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,GAAG,IAAI,CAY7E;IAED,OAAc,qBAAqB,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,GAAG,IAAI,CAQvF;CACJ"}

View File

@@ -0,0 +1,166 @@
import { ValueFormatter } from "./valueFormatter.js";
export class TrackStatsBuilder {
static buildFramerateResolution(trackStats, now) {
const resolution = {
height: now.frameHeight,
width: now.frameWidth
};
const frameRate = now.framesPerSecond;
if (resolution.height && resolution.width) {
trackStats.setResolution(resolution);
}
trackStats.setFramerate(Math.round(frameRate || 0));
}
static calculateSimulcastFramerate(trackStats, now, before, layer) {
let frameRate = trackStats.getFramerate();
if (!frameRate) {
if (before) {
const timeMs = now.timestamp - before.timestamp;
if (timeMs > 0 && now.framesSent) {
const numberOfFramesSinceBefore = now.framesSent - before.framesSent;
frameRate = numberOfFramesSinceBefore / timeMs * 1000;
}
}
if (!frameRate) {
return;
}
}
// Reset frame rate to 0 when video is suspended as a result of endpoint falling out of last-n.
frameRate = layer ? Math.round(frameRate / layer) : 0;
trackStats.setFramerate(frameRate);
}
static buildCodec(report, trackStats, now) {
const codec = report?.get(now.codecId);
if (codec) {
/**
* The mime type has the following form: video/VP8 or audio/ISAC,
* so we what to keep just the type after the '/', audio and video
* keys will be added on the processing side.
*/
const codecShortType = codec.mimeType.split("/")[1];
if (codecShortType) trackStats.setCodec(codecShortType);
}
}
static buildBitrateReceived(trackStats, now, before) {
trackStats.setBitrate({
download: TrackStatsBuilder.calculateBitrate(now.bytesReceived, before.bytesReceived, now.timestamp, before.timestamp),
upload: 0
});
}
static buildBitrateSend(trackStats, now, before) {
trackStats.setBitrate({
download: 0,
upload: this.calculateBitrate(now.bytesSent, before.bytesSent, now.timestamp, before.timestamp)
});
}
static buildPacketsLost(trackStats, now, before) {
const key = now.type === "outbound-rtp" ? "packetsSent" : "packetsReceived";
let packetsNow = now[key];
if (!packetsNow || packetsNow < 0) {
packetsNow = 0;
}
const packetsBefore = ValueFormatter.getNonNegativeValue(before[key]);
const packetsDiff = Math.max(0, packetsNow - packetsBefore);
const packetsLostNow = ValueFormatter.getNonNegativeValue(now.packetsLost);
const packetsLostBefore = ValueFormatter.getNonNegativeValue(before.packetsLost);
const packetsLostDiff = Math.max(0, packetsLostNow - packetsLostBefore);
trackStats.setLoss({
packetsTotal: packetsDiff + packetsLostDiff,
packetsLost: packetsLostDiff,
isDownloadStream: now.type !== "outbound-rtp"
});
}
static calculateBitrate(bytesNowAny, bytesBeforeAny, nowTimestamp, beforeTimestamp) {
const bytesNow = ValueFormatter.getNonNegativeValue(bytesNowAny);
const bytesBefore = ValueFormatter.getNonNegativeValue(bytesBeforeAny);
const bytesProcessed = Math.max(0, bytesNow - bytesBefore);
const timeMs = nowTimestamp - beforeTimestamp;
let bitrateKbps = 0;
if (timeMs > 0) {
bitrateKbps = Math.round(bytesProcessed * 8 / timeMs);
}
return bitrateKbps;
}
static setTrackStatsState(trackStats, transceiver) {
if (transceiver === undefined) {
trackStats.alive = false;
return;
}
const track = trackStats.getType() === "remote" ? transceiver.receiver.track : transceiver?.sender?.track;
if (track === undefined || track === null) {
trackStats.alive = false;
return;
}
if (track.readyState === "ended") {
trackStats.alive = false;
return;
}
trackStats.muted = track.muted;
trackStats.enabled = track.enabled;
trackStats.alive = true;
}
static buildTrackSummary(trackStatsList) {
const videoTrackSummary = {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0
};
const audioTrackSummary = {
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
};
}
static buildJitter(trackStats, statsReport) {
if (statsReport.type !== "inbound-rtp") {
return;
}
const jitterStr = statsReport?.jitter;
if (jitterStr !== undefined) {
const jitter = ValueFormatter.getNonNegativeValue(jitterStr);
trackStats.setJitter(Math.round(jitter * 1000));
} else {
trackStats.setJitter(-1);
}
}
static buildAudioConcealment(trackStats, statsReport) {
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);
}
}
//# sourceMappingURL=trackStatsBuilder.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
export interface TransportStats {
ip: string;
type: string;
localIp: string;
isFocus: boolean;
localCandidateType: string;
remoteCandidateType: string;
networkType: string;
rtt: number;
}
//# sourceMappingURL=transportStats.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"transportStats.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/transportStats.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACf"}

View File

@@ -0,0 +1,3 @@
export {};
export {};
//# sourceMappingURL=transportStats.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"transportStats.js","names":[],"sources":["../../../src/webrtc/stats/transportStats.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport interface TransportStats {\n ip: string;\n type: string;\n localIp: string;\n isFocus: boolean;\n localCandidateType: string;\n remoteCandidateType: string;\n networkType: string;\n rtt: number;\n}\n"],"mappings":"","ignoreList":[]}

View File

@@ -0,0 +1,5 @@
import { type TransportStats } from "./transportStats.ts";
export declare class TransportStatsBuilder {
static buildReport(report: RTCStatsReport | undefined, now: RTCIceCandidatePairStats, conferenceStatsTransport: TransportStats[], isFocus: boolean): TransportStats[];
}
//# sourceMappingURL=transportStatsBuilder.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"transportStatsBuilder.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/transportStatsBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,qBAAa,qBAAqB;IAC9B,OAAc,WAAW,CACrB,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,GAAG,EAAE,wBAAwB,EAC7B,wBAAwB,EAAE,cAAc,EAAE,EAC1C,OAAO,EAAE,OAAO,GACjB,cAAc,EAAE,CAsClB;CACJ"}

View File

@@ -0,0 +1,34 @@
export class TransportStatsBuilder {
static buildReport(report, now, conferenceStatsTransport, isFocus) {
const localUsedCandidate = report?.get(now.localCandidateId);
const remoteUsedCandidate = report?.get(now.remoteCandidateId);
// RTCIceCandidateStats
// https://w3c.github.io/webrtc-stats/#icecandidate-dict*
if (remoteUsedCandidate && localUsedCandidate) {
const remoteIpAddress = remoteUsedCandidate.ip !== undefined ? remoteUsedCandidate.ip : remoteUsedCandidate.address;
const remotePort = remoteUsedCandidate.port;
const ip = `${remoteIpAddress}:${remotePort}`;
const localIpAddress = localUsedCandidate.ip !== undefined ? localUsedCandidate.ip : localUsedCandidate.address;
const localPort = localUsedCandidate.port;
const localIp = `${localIpAddress}:${localPort}`;
const type = remoteUsedCandidate.protocol;
// Save the address unless it has been saved already.
if (!conferenceStatsTransport.some(t => t.ip === ip && t.type === type && t.localIp === localIp)) {
conferenceStatsTransport.push({
ip,
type,
localIp,
isFocus,
localCandidateType: localUsedCandidate.candidateType,
remoteCandidateType: remoteUsedCandidate.candidateType,
networkType: localUsedCandidate.networkType,
rtt: now.currentRoundTripTime ? now.currentRoundTripTime * 1000 : NaN
});
}
}
return conferenceStatsTransport;
}
}
//# sourceMappingURL=transportStatsBuilder.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"transportStatsBuilder.js","names":[],"sources":["../../../src/webrtc/stats/transportStatsBuilder.ts"],"sourcesContent":["import { type TransportStats } from \"./transportStats.ts\";\n\nexport class TransportStatsBuilder {\n public static buildReport(\n report: RTCStatsReport | undefined,\n now: RTCIceCandidatePairStats,\n conferenceStatsTransport: TransportStats[],\n isFocus: boolean,\n ): TransportStats[] {\n const localUsedCandidate = report?.get(now.localCandidateId);\n const remoteUsedCandidate = report?.get(now.remoteCandidateId);\n\n // RTCIceCandidateStats\n // https://w3c.github.io/webrtc-stats/#icecandidate-dict*\n if (remoteUsedCandidate && localUsedCandidate) {\n const remoteIpAddress =\n remoteUsedCandidate.ip !== undefined ? remoteUsedCandidate.ip : remoteUsedCandidate.address;\n const remotePort = remoteUsedCandidate.port;\n const ip = `${remoteIpAddress}:${remotePort}`;\n\n const localIpAddress =\n localUsedCandidate.ip !== undefined ? localUsedCandidate.ip : localUsedCandidate.address;\n const localPort = localUsedCandidate.port;\n const localIp = `${localIpAddress}:${localPort}`;\n\n const type = remoteUsedCandidate.protocol;\n\n // Save the address unless it has been saved already.\n if (\n !conferenceStatsTransport.some(\n (t: TransportStats) => t.ip === ip && t.type === type && t.localIp === localIp,\n )\n ) {\n conferenceStatsTransport.push({\n ip,\n type,\n localIp,\n isFocus,\n localCandidateType: localUsedCandidate.candidateType,\n remoteCandidateType: remoteUsedCandidate.candidateType,\n networkType: localUsedCandidate.networkType,\n rtt: now.currentRoundTripTime ? now.currentRoundTripTime * 1000 : NaN,\n });\n }\n }\n return conferenceStatsTransport;\n }\n}\n"],"mappings":"AAEA,OAAO,MAAM,qBAAqB,CAAC;EAC/B,OAAc,WAAW,CACrB,MAAkC,EAClC,GAA6B,EAC7B,wBAA0C,EAC1C,OAAgB,EACA;IAChB,MAAM,kBAAkB,GAAG,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC5D,MAAM,mBAAmB,GAAG,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC;;IAE9D;IACA;IACA,IAAI,mBAAmB,IAAI,kBAAkB,EAAE;MAC3C,MAAM,eAAe,GACjB,mBAAmB,CAAC,EAAE,KAAK,SAAS,GAAG,mBAAmB,CAAC,EAAE,GAAG,mBAAmB,CAAC,OAAO;MAC/F,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI;MAC3C,MAAM,EAAE,GAAG,GAAG,eAAe,IAAI,UAAU,EAAE;MAE7C,MAAM,cAAc,GAChB,kBAAkB,CAAC,EAAE,KAAK,SAAS,GAAG,kBAAkB,CAAC,EAAE,GAAG,kBAAkB,CAAC,OAAO;MAC5F,MAAM,SAAS,GAAG,kBAAkB,CAAC,IAAI;MACzC,MAAM,OAAO,GAAG,GAAG,cAAc,IAAI,SAAS,EAAE;MAEhD,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ;;MAEzC;MACA,IACI,CAAC,wBAAwB,CAAC,IAAI,CACzB,CAAiB,IAAK,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,KAAK,OAC3E,CAAC,EACH;QACE,wBAAwB,CAAC,IAAI,CAAC;UAC1B,EAAE;UACF,IAAI;UACJ,OAAO;UACP,OAAO;UACP,kBAAkB,EAAE,kBAAkB,CAAC,aAAa;UACpD,mBAAmB,EAAE,mBAAmB,CAAC,aAAa;UACtD,WAAW,EAAE,kBAAkB,CAAC,WAAW;UAC3C,GAAG,EAAE,GAAG,CAAC,oBAAoB,GAAG,GAAG,CAAC,oBAAoB,GAAG,IAAI,GAAG;QACtE,CAAC,CAAC;MACN;IACJ;IACA,OAAO,wBAAwB;EACnC;AACJ","ignoreList":[]}

View File

@@ -0,0 +1,4 @@
export declare class ValueFormatter {
static getNonNegativeValue(imput: any): number;
}
//# sourceMappingURL=valueFormatter.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"valueFormatter.d.ts","sourceRoot":"","sources":["../../../src/webrtc/stats/valueFormatter.ts"],"names":[],"mappings":"AAYA,qBAAa,cAAc;IACvB,OAAc,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAYpD;CACJ"}

View File

@@ -0,0 +1,25 @@
/*
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.
*/
export class ValueFormatter {
static getNonNegativeValue(imput) {
let value = imput;
if (typeof value !== "number") {
value = Number(value);
}
if (isNaN(value)) {
return 0;
}
return Math.max(0, value);
}
}
//# sourceMappingURL=valueFormatter.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"valueFormatter.js","names":[],"sources":["../../../src/webrtc/stats/valueFormatter.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\nexport class ValueFormatter {\n public static getNonNegativeValue(imput: any): number {\n let value = imput;\n\n if (typeof value !== \"number\") {\n value = Number(value);\n }\n\n if (isNaN(value)) {\n return 0;\n }\n\n return Math.max(0, value);\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,cAAc,CAAC;EACxB,OAAc,mBAAmB,CAAC,KAAU,EAAU;IAClD,IAAI,KAAK,GAAG,KAAK;IAEjB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;MAC3B,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IACzB;IAEA,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE;MACd,OAAO,CAAC;IACZ;IAEA,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC;EAC7B;AACJ","ignoreList":[]}