summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats')
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportGatherer.js194
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportSummary.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStats.js34
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsBuilder.js33
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsReportBuilder.js127
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/groupCallStats.js80
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaSsrcHandler.js62
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackHandler.js69
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStats.js150
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStatsHandler.js82
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReport.js28
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReportEmitter.js36
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/summaryStatsReportGatherer.js103
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/trackStatsBuilder.js172
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStats.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStatsBuilder.js40
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/valueFormatter.js31
17 files changed, 1251 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportGatherer.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportGatherer.js
new file mode 100644
index 0000000000..b830e469a1
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportGatherer.js
@@ -0,0 +1,194 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.CallStatsReportGatherer = void 0;
+var _connectionStats = require("./connectionStats");
+var _connectionStatsBuilder = require("./connectionStatsBuilder");
+var _transportStatsBuilder = require("./transportStatsBuilder");
+var _mediaSsrcHandler = require("./media/mediaSsrcHandler");
+var _mediaTrackHandler = require("./media/mediaTrackHandler");
+var _mediaTrackStatsHandler = require("./media/mediaTrackStatsHandler");
+var _trackStatsBuilder = require("./trackStatsBuilder");
+var _connectionStatsReportBuilder = require("./connectionStatsReportBuilder");
+var _valueFormatter = require("./valueFormatter");
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+class CallStatsReportGatherer {
+ constructor(callId, opponentMemberId, pc, emitter, isFocus = true) {
+ this.callId = callId;
+ this.opponentMemberId = opponentMemberId;
+ this.pc = pc;
+ this.emitter = emitter;
+ this.isFocus = isFocus;
+ _defineProperty(this, "isActive", true);
+ _defineProperty(this, "previousStatsReport", void 0);
+ _defineProperty(this, "currentStatsReport", void 0);
+ _defineProperty(this, "connectionStats", new _connectionStats.ConnectionStats());
+ _defineProperty(this, "trackStats", void 0);
+ pc.addEventListener("signalingstatechange", this.onSignalStateChange.bind(this));
+ this.trackStats = new _mediaTrackStatsHandler.MediaTrackStatsHandler(new _mediaSsrcHandler.MediaSsrcHandler(), new _mediaTrackHandler.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.isActive = false;
+ 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.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.ConnectionStatsBuilder.buildBandwidthReport(now);
+ this.connectionStats.transport = _transportStatsBuilder.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.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.TrackStatsBuilder.buildFramerateResolution(trackStats, now);
+ if (before) {
+ _trackStatsBuilder.TrackStatsBuilder.buildBitrateReceived(trackStats, now, before);
+ }
+ const ts = this.trackStats.findTransceiverByTrackId(trackStats.trackId);
+ _trackStatsBuilder.TrackStatsBuilder.setTrackStatsState(trackStats, ts);
+ _trackStatsBuilder.TrackStatsBuilder.buildJitter(trackStats, now);
+ _trackStatsBuilder.TrackStatsBuilder.buildAudioConcealment(trackStats, now);
+ } else if (before) {
+ byteSentStatsReport.set(trackStats.trackId, _valueFormatter.ValueFormatter.getNonNegativeValue(now.bytesSent));
+ _trackStatsBuilder.TrackStatsBuilder.buildBitrateSend(trackStats, now, before);
+ }
+ _trackStatsBuilder.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.TrackStatsBuilder.buildFramerateResolution(trackStats, now);
+ _trackStatsBuilder.TrackStatsBuilder.calculateSimulcastFramerate(trackStats, now, before, this.trackStats.mediaTrackHandler.getActiveSimulcastStreams());
+ }
+ });
+ this.emitter.emitByteSendReport(byteSentStatsReport);
+ this.processAndEmitConnectionStatsReport();
+ }
+ setActive(isActive) {
+ this.isActive = isActive;
+ }
+ getActive() {
+ return this.isActive;
+ }
+ handleError(_) {
+ this.isActive = false;
+ }
+ processAndEmitConnectionStatsReport() {
+ const report = _connectionStatsReportBuilder.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;
+ }
+}
+exports.CallStatsReportGatherer = CallStatsReportGatherer; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportSummary.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportSummary.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/callStatsReportSummary.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStats.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStats.js
new file mode 100644
index 0000000000..16374812d0
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStats.js
@@ -0,0 +1,34 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ConnectionStats = void 0;
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+/*
+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.
+*/
+
+class ConnectionStats {
+ constructor() {
+ _defineProperty(this, "bandwidth", {});
+ _defineProperty(this, "bitrate", {});
+ _defineProperty(this, "packetLoss", {});
+ _defineProperty(this, "transport", []);
+ }
+}
+exports.ConnectionStats = ConnectionStats; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsBuilder.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsBuilder.js
new file mode 100644
index 0000000000..64bf4082ff
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsBuilder.js
@@ -0,0 +1,33 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ConnectionStatsBuilder = void 0;
+/*
+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.
+*/
+
+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
+ };
+ }
+}
+exports.ConnectionStatsBuilder = ConnectionStatsBuilder; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsReportBuilder.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsReportBuilder.js
new file mode 100644
index 0000000000..7178b5411e
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/connectionStatsReportBuilder.js
@@ -0,0 +1,127 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ConnectionStatsReportBuilder = void 0;
+/*
+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.
+*/
+
+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);
+ }
+}
+exports.ConnectionStatsReportBuilder = ConnectionStatsReportBuilder; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/groupCallStats.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/groupCallStats.js
new file mode 100644
index 0000000000..4ed8a1062f
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/groupCallStats.js
@@ -0,0 +1,80 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.GroupCallStats = void 0;
+var _callStatsReportGatherer = require("./callStatsReportGatherer");
+var _statsReportEmitter = require("./statsReportEmitter");
+var _summaryStatsReportGatherer = require("./summaryStatsReportGatherer");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+class GroupCallStats {
+ constructor(groupCallId, userId, interval = 10000) {
+ this.groupCallId = groupCallId;
+ this.userId = userId;
+ this.interval = interval;
+ _defineProperty(this, "timer", void 0);
+ _defineProperty(this, "gatherers", new Map());
+ _defineProperty(this, "reports", new _statsReportEmitter.StatsReportEmitter());
+ _defineProperty(this, "summaryStatsReportGatherer", new _summaryStatsReportGatherer.SummaryStatsReportGatherer(this.reports));
+ }
+ 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.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));
+ }
+ setInterval(interval) {
+ this.interval = interval;
+ }
+}
+exports.GroupCallStats = GroupCallStats; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaSsrcHandler.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaSsrcHandler.js
new file mode 100644
index 0000000000..5e43415558
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaSsrcHandler.js
@@ -0,0 +1,62 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MediaSsrcHandler = void 0;
+var _sdpTransform = require("sdp-transform");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+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 = (0, _sdpTransform.parse)(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];
+ }
+}
+exports.MediaSsrcHandler = MediaSsrcHandler; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackHandler.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackHandler.js
new file mode 100644
index 0000000000..c4252a9cbd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackHandler.js
@@ -0,0 +1,69 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MediaTrackHandler = void 0;
+/*
+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.
+*/
+
+class MediaTrackHandler {
+ constructor(pc) {
+ this.pc = pc;
+ }
+ getLocalTracks(kind) {
+ const isNotNullAndKind = track => {
+ return track !== null && track.kind === kind;
+ };
+ // @ts-ignore The linter don't get it
+ 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);
+ if (transceiver !== undefined && !!transceiver.sender && !!transceiver.sender.track) {
+ return transceiver.sender.track.id;
+ }
+ return undefined;
+ }
+ getRemoteTrackIdByMid(mid) {
+ const transceiver = this.pc.getTransceivers().find(t => t.mid === mid);
+ if (transceiver !== undefined && !!transceiver.receiver && !!transceiver.receiver.track) {
+ return transceiver.receiver.track.id;
+ }
+ return undefined;
+ }
+ 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;
+ });
+ }
+}
+exports.MediaTrackHandler = MediaTrackHandler; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStats.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStats.js
new file mode 100644
index 0000000000..d5a7963c23
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStats.js
@@ -0,0 +1,150 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MediaTrackStats = void 0;
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+/*
+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.
+*/
+
+class MediaTrackStats {
+ constructor(trackId, type, kind) {
+ this.trackId = trackId;
+ this.type = type;
+ this.kind = 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);
+ }
+ 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;
+ }
+}
+exports.MediaTrackStats = MediaTrackStats; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStatsHandler.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStatsHandler.js
new file mode 100644
index 0000000000..f72f644cb3
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/media/mediaTrackStatsHandler.js
@@ -0,0 +1,82 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MediaTrackStatsHandler = void 0;
+var _mediaTrackStats = require("./mediaTrackStats");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+class MediaTrackStatsHandler {
+ constructor(mediaSsrcHandler, mediaTrackHandler) {
+ this.mediaSsrcHandler = mediaSsrcHandler;
+ this.mediaTrackHandler = mediaTrackHandler;
+ _defineProperty(this, "track2stats", new Map());
+ }
+
+ /**
+ * 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.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);
+ }
+}
+exports.MediaTrackStatsHandler = MediaTrackStatsHandler; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReport.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReport.js
new file mode 100644
index 0000000000..d020a9e7f9
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReport.js
@@ -0,0 +1,28 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.StatsReport = void 0;
+/*
+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.
+*/
+let StatsReport = /*#__PURE__*/function (StatsReport) {
+ StatsReport["CONNECTION_STATS"] = "StatsReport.connection_stats";
+ StatsReport["BYTE_SENT_STATS"] = "StatsReport.byte_sent_stats";
+ StatsReport["SUMMARY_STATS"] = "StatsReport.summary_stats";
+ return StatsReport;
+}({});
+exports.StatsReport = StatsReport; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReportEmitter.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReportEmitter.js
new file mode 100644
index 0000000000..c25da81743
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/statsReportEmitter.js
@@ -0,0 +1,36 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.StatsReportEmitter = void 0;
+var _typedEventEmitter = require("../../models/typed-event-emitter");
+var _statsReport = require("./statsReport");
+/*
+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.
+*/
+
+class StatsReportEmitter extends _typedEventEmitter.TypedEventEmitter {
+ emitByteSendReport(byteSentStats) {
+ this.emit(_statsReport.StatsReport.BYTE_SENT_STATS, byteSentStats);
+ }
+ emitConnectionStatsReport(report) {
+ this.emit(_statsReport.StatsReport.CONNECTION_STATS, report);
+ }
+ emitSummaryStatsReport(report) {
+ this.emit(_statsReport.StatsReport.SUMMARY_STATS, report);
+ }
+}
+exports.StatsReportEmitter = StatsReportEmitter; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/summaryStatsReportGatherer.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/summaryStatsReportGatherer.js
new file mode 100644
index 0000000000..fb78690e64
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/summaryStatsReportGatherer.js
@@ -0,0 +1,103 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SummaryStatsReportGatherer = void 0;
+/*
+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.
+*/
+
+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;
+ 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: summaryTotalCount
+ };
+ this.emitter.emitSummaryStatsReport(report);
+ }
+ 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;
+ }
+}
+exports.SummaryStatsReportGatherer = SummaryStatsReportGatherer; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/trackStatsBuilder.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/trackStatsBuilder.js
new file mode 100644
index 0000000000..563a14b784
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/trackStatsBuilder.js
@@ -0,0 +1,172 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.TrackStatsBuilder = void 0;
+var _valueFormatter = require("./valueFormatter");
+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];
+ 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.ValueFormatter.getNonNegativeValue(before[key]);
+ const packetsDiff = Math.max(0, packetsNow - packetsBefore);
+ const packetsLostNow = _valueFormatter.ValueFormatter.getNonNegativeValue(now.packetsLost);
+ const packetsLostBefore = _valueFormatter.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.ValueFormatter.getNonNegativeValue(bytesNowAny);
+ const bytesBefore = _valueFormatter.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.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);
+ }
+}
+exports.TrackStatsBuilder = TrackStatsBuilder; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStats.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStats.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStats.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStatsBuilder.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStatsBuilder.js
new file mode 100644
index 0000000000..d65aa28dba
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/transportStatsBuilder.js
@@ -0,0 +1,40 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.TransportStatsBuilder = void 0;
+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;
+ }
+}
+exports.TransportStatsBuilder = TransportStatsBuilder; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/valueFormatter.js b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/valueFormatter.js
new file mode 100644
index 0000000000..17050d260e
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/stats/valueFormatter.js
@@ -0,0 +1,31 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ValueFormatter = void 0;
+/*
+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.
+*/
+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);
+ }
+}
+exports.ValueFormatter = ValueFormatter; \ No newline at end of file