1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2026-01-03 23:22:30 +03:00

Expand webrtc stats with connection and call feed track information (#3421)

* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add call and opponent member id to call stats reports

* Update opponent member when we know them

* Add missing return type

* remove async in test

* add call feed webrtc report

* add logger for error case in stats gathering

* gather connection track report

* expand call feed stats with call feed

* formation code and fix lint issues

* clean up new track stats

 * set label for call feed stats and
 * remove stream in track stats
 * transceiver stats based on mid
 * call feed stats based on stream id
 * fix lint and test issues

* Fix merge issues

* Add test for expanding call feed stats in group call

* Fix export issue from prv PR

* explain test data and fixed some linter issues

* convert tests to snapshot tests
This commit is contained in:
Enrico Schwendig
2023-06-07 16:05:51 +02:00
committed by GitHub
parent 60c715d5df
commit 3cfad3cdeb
11 changed files with 1710 additions and 19 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -18,25 +18,25 @@ import { mocked } from "jest-mock";
import { EventType, GroupCallIntent, GroupCallType, MatrixCall, MatrixEvent, Room, RoomMember } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
import { GroupCall, GroupCallEvent, GroupCallState, GroupCallStatsReportEvent } from "../../../src/webrtc/groupCall";
import { IMyDevice, MatrixClient } from "../../../src/client";
import {
FAKE_CONF_ID,
FAKE_DEVICE_ID_1,
FAKE_DEVICE_ID_2,
FAKE_ROOM_ID,
FAKE_SESSION_ID_1,
FAKE_SESSION_ID_2,
FAKE_USER_ID_1,
FAKE_USER_ID_2,
FAKE_USER_ID_3,
installWebRTCMocks,
MockCallFeed,
MockCallMatrixClient,
MockMatrixCall,
MockMediaStream,
MockMediaStreamTrack,
MockRTCPeerConnection,
MockMatrixCall,
FAKE_ROOM_ID,
FAKE_USER_ID_1,
FAKE_CONF_ID,
FAKE_DEVICE_ID_2,
FAKE_SESSION_ID_2,
FAKE_USER_ID_2,
FAKE_DEVICE_ID_1,
FAKE_SESSION_ID_1,
FAKE_USER_ID_3,
} from "../../test-utils/webrtc";
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { sleep } from "../../../src/utils";
@@ -44,6 +44,9 @@ import { CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
import { CallFeed } from "../../../src/webrtc/callFeed";
import { CallEvent, CallState } from "../../../src/webrtc/call";
import { flushPromises } from "../../test-utils/flushPromises";
import { CallFeedReport } from "../../../src/webrtc/stats/statsReport";
import { CallFeedStatsReporter } from "../../../src/webrtc/stats/callFeedStatsReporter";
import { StatsReportEmitter } from "../../../src/webrtc/stats/statsReportEmitter";
const FAKE_STATE_EVENTS = [
{
@@ -1726,4 +1729,89 @@ describe("Group Call", function () {
expect(start).toHaveBeenCalled();
});
});
describe("as stats event listener and a CallFeedReport was triggered", () => {
let groupCall: GroupCall;
let reportEmitter: StatsReportEmitter;
const report: CallFeedReport = {} as CallFeedReport;
beforeEach(async () => {
CallFeedStatsReporter.expandCallFeedReport = jest.fn().mockReturnValue(report);
const typedMockClient = new MockCallMatrixClient(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1);
const mockClient = typedMockClient.typed();
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
groupCall = await createAndEnterGroupCall(mockClient, room);
reportEmitter = groupCall.getGroupCallStats().reports;
});
it("should not extends with feed stats if no call exists", async () => {
const testPromise = new Promise<void>((done) => {
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith({}, [], "from-call-feed");
done();
});
});
const report: CallFeedReport = {} as CallFeedReport;
reportEmitter.emitCallFeedReport(report);
await testPromise;
});
it("and a CallFeedReport was triggered then it should extends with local feed", async () => {
const localCallFeed = {} as CallFeed;
groupCall.localCallFeed = localCallFeed;
const testPromise = new Promise<void>((done) => {
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
[localCallFeed],
"from-local-feed",
);
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
[],
"from-call-feed",
);
done();
});
});
const report: CallFeedReport = {} as CallFeedReport;
reportEmitter.emitCallFeedReport(report);
await testPromise;
});
it("and a CallFeedReport was triggered then it should extends with remote feed", async () => {
const localCallFeed = {} as CallFeed;
groupCall.localCallFeed = localCallFeed;
// @ts-ignore Suppress error because access to private property
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
report.callId = call.callId;
const feeds = call.getFeeds();
const testPromise = new Promise<void>((done) => {
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
[localCallFeed],
"from-local-feed",
);
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
feeds,
"from-call-feed",
);
done();
});
});
reportEmitter.emitCallFeedReport(report);
await testPromise;
});
});
});

View File

@@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CallFeedStatsReporter should builds CallFeedReport 1`] = `
{
"callFeeds": [],
"callId": "CALL_ID",
"opponentMemberId": "USER_ID",
"transceiver": [
{
"currentDirection": "sendonly",
"direction": "sendrecv",
"mid": "0",
"receiver": {
"constrainDeviceId": "constrainDeviceId-receiver_audio_0",
"enabled": true,
"id": "receiver_audio_0",
"kind": "audio",
"label": "receiver",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-receiver_audio_0",
},
"sender": {
"constrainDeviceId": "constrainDeviceId-sender_audio_0",
"enabled": true,
"id": "sender_audio_0",
"kind": "audio",
"label": "sender",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-sender_audio_0",
},
},
{
"currentDirection": "sendrecv",
"direction": "recvonly",
"mid": "1",
"receiver": {
"constrainDeviceId": "constrainDeviceId-receiver_video_1",
"enabled": true,
"id": "receiver_video_1",
"kind": "video",
"label": "receiver",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-receiver_video_1",
},
"sender": {
"constrainDeviceId": "constrainDeviceId-sender_video_1",
"enabled": true,
"id": "sender_video_1",
"kind": "video",
"label": "sender",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-sender_video_1",
},
},
{
"currentDirection": "recvonly",
"direction": "recvonly",
"mid": "2",
"receiver": {
"constrainDeviceId": "constrainDeviceId-receiver_video_2",
"enabled": true,
"id": "receiver_video_2",
"kind": "video",
"label": "receiver",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-receiver_video_2",
},
"sender": null,
},
],
}
`;
exports[`CallFeedStatsReporter should extends CallFeedReport with call feeds 1`] = `
[
{
"audio": {
"constrainDeviceId": "constrainDeviceId-video-1",
"enabled": true,
"id": "video-1",
"kind": "video",
"label": "--",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-video-1",
},
"isAudioMuted": true,
"isVideoMuted": false,
"prefix": "unknown",
"purpose": undefined,
"stream": "stream-1",
"type": "local",
"video": {
"constrainDeviceId": "constrainDeviceId-audio-1",
"enabled": true,
"id": "audio-1",
"kind": "audio",
"label": "--",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-audio-1",
},
},
]
`;

View File

@@ -0,0 +1,117 @@
/*
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 { CallFeedStatsReporter } from "../../../../src/webrtc/stats/callFeedStatsReporter";
import { CallFeedReport } from "../../../../src/webrtc/stats/statsReport";
import { CallFeed } from "../../../../src/webrtc/callFeed";
const CALL_ID = "CALL_ID";
const USER_ID = "USER_ID";
describe("CallFeedStatsReporter", () => {
let rtcSpy: RTCPeerConnection;
beforeEach(() => {
rtcSpy = {} as RTCPeerConnection;
rtcSpy.getTransceivers = jest.fn().mockReturnValue(buildTransceiverMocks());
});
describe("should", () => {
it("builds CallFeedReport", async () => {
expect(CallFeedStatsReporter.buildCallFeedReport(CALL_ID, USER_ID, rtcSpy)).toMatchSnapshot();
});
it("extends CallFeedReport with call feeds", async () => {
const feed = buildCallFeedMock("1");
const callFeedList: CallFeed[] = [feed];
const report = {
callId: "callId",
opponentMemberId: "opponentMemberId",
transceiver: [],
callFeeds: [],
} as CallFeedReport;
expect(CallFeedStatsReporter.expandCallFeedReport(report, callFeedList).callFeeds).toMatchSnapshot();
});
});
const buildTransceiverMocks = (): RTCRtpTransceiver[] => {
const trans1 = {
mid: "0",
direction: "sendrecv",
currentDirection: "sendonly",
sender: buildSenderMock("sender_audio_0", "audio"),
receiver: buildReceiverMock("receiver_audio_0", "audio"),
} as RTCRtpTransceiver;
const trans2 = {
mid: "1",
direction: "recvonly",
currentDirection: "sendrecv",
sender: buildSenderMock("sender_video_1", "video"),
receiver: buildReceiverMock("receiver_video_1", "video"),
} as RTCRtpTransceiver;
const trans3 = {
mid: "2",
direction: "recvonly",
currentDirection: "recvonly",
sender: { track: null } as RTCRtpSender,
receiver: buildReceiverMock("receiver_video_2", "video"),
} as RTCRtpTransceiver;
return [trans1, trans2, trans3];
};
const buildSenderMock = (id: string, kind: "audio" | "video"): RTCRtpSender => {
const track = buildTrackMock(id, kind);
return {
track,
} as RTCRtpSender;
};
const buildReceiverMock = (id: string, kind: "audio" | "video"): RTCRtpReceiver => {
const track = buildTrackMock(id, kind);
return {
track,
} as RTCRtpReceiver;
};
const buildTrackMock = (id: string, kind: "audio" | "video"): MediaStreamTrack => {
return {
id,
kind,
enabled: true,
label: "--",
muted: false,
readyState: "live",
getSettings: () => ({ deviceId: `settingDeviceId-${id}` }),
getConstraints: () => ({ deviceId: `constrainDeviceId-${id}` }),
} as MediaStreamTrack;
};
const buildCallFeedMock = (id: string, isLocal = true): CallFeed => {
const stream = {
id: `stream-${id}`,
getAudioTracks(): MediaStreamTrack[] {
return [buildTrackMock(`video-${id}`, "video")];
},
getVideoTracks(): MediaStreamTrack[] {
return [buildTrackMock(`audio-${id}`, "audio")];
},
} as MediaStream;
return {
stream,
isLocal: () => isLocal,
isVideoMuted: () => false,
isAudioMuted: () => true,
} as CallFeed;
};
});

View File

@@ -17,6 +17,8 @@ limitations under the License.
import { CallStatsReportGatherer } from "../../../../src/webrtc/stats/callStatsReportGatherer";
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
import { MediaSsrcHandler } from "../../../../src/webrtc/stats/media/mediaSsrcHandler";
import { currentChromeReport, prevChromeReport } from "../../../test-utils/webrtcReports";
import { MockRTCPeerConnection } from "../../../test-utils/webrtc";
const CALL_ID = "CALL_ID";
const USER_ID = "USER_ID";
@@ -28,6 +30,7 @@ describe("CallStatsReportGatherer", () => {
beforeEach(() => {
rtcSpy = { getStats: () => new Promise<RTCStatsReport>(() => null) } as RTCPeerConnection;
rtcSpy.addEventListener = jest.fn();
rtcSpy.getTransceivers = jest.fn().mockReturnValue([]);
emitter = new StatsReportEmitter();
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
});
@@ -145,6 +148,70 @@ describe("CallStatsReportGatherer", () => {
});
expect(collector.getActive()).toBeTruthy();
});
describe("should not only produce a call summary stat but also", () => {
const wantedSummaryReport = {
isFirstCollection: false,
audioTrackSummary: {
concealedAudio: 0,
count: 0,
maxJitter: 0,
maxPacketLoss: 0,
muted: 0,
totalAudio: 0,
},
receivedAudioMedia: 0,
receivedMedia: 0,
receivedVideoMedia: 0,
videoTrackSummary: {
concealedAudio: 0,
count: 0,
maxJitter: 0,
maxPacketLoss: 0,
muted: 0,
totalAudio: 0,
},
};
beforeEach(() => {
rtcSpy = new MockRTCPeerConnection() as unknown as RTCPeerConnection;
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
const getStats = jest.spyOn(rtcSpy, "getStats");
const previous = prevChromeReport as unknown as RTCStatsReport;
previous.get = (id: string) => {
return prevChromeReport.find((data) => data.id === id);
};
// @ts-ignore
collector.previousStatsReport = previous;
const current = currentChromeReport as unknown as RTCStatsReport;
current.get = (id: string) => {
return currentChromeReport.find((data) => data.id === id);
};
// @ts-ignore
getStats.mockResolvedValue(current);
});
it("emit byteSentStatsReport", async () => {
const emitByteSendReport = jest.spyOn(emitter, "emitByteSendReport");
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual(wantedSummaryReport);
expect(emitByteSendReport).toHaveBeenCalled();
});
it("emit emitConnectionStatsReport", async () => {
const emitConnectionStatsReport = jest.spyOn(emitter, "emitConnectionStatsReport");
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual(wantedSummaryReport);
expect(emitConnectionStatsReport).toHaveBeenCalled();
});
it("emit callFeedStatsReport", async () => {
const emitCallFeedReport = jest.spyOn(emitter, "emitCallFeedReport");
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual(wantedSummaryReport);
expect(emitCallFeedReport).toHaveBeenCalled();
});
});
});
describe("on signal state change event", () => {

View File

@@ -16,6 +16,7 @@ limitations under the License.
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
import {
ByteSentStatsReport,
CallFeedReport,
ConnectionStatsReport,
StatsReport,
SummaryStatsReport,
@@ -62,4 +63,16 @@ describe("StatsReportEmitter", () => {
emitter.emitSummaryStatsReport(report);
});
});
it("should emit and receive CallFeedReports", async () => {
const report = {} as CallFeedReport;
return new Promise((resolve, _) => {
emitter.on(StatsReport.CALL_FEED_REPORT, (r) => {
expect(r).toBe(report);
resolve(null);
return;
});
emitter.emitCallFeedReport(report);
});
});
});

View File

@@ -25,8 +25,15 @@ import { GroupCallEventHandlerEvent } from "./groupCallEventHandler";
import { IScreensharingOpts } from "./mediaHandler";
import { mapsEqual } from "../utils";
import { GroupCallStats } from "./stats/groupCallStats";
import { ByteSentStatsReport, ConnectionStatsReport, StatsReport, SummaryStatsReport } from "./stats/statsReport";
import {
ByteSentStatsReport,
CallFeedReport,
ConnectionStatsReport,
StatsReport,
SummaryStatsReport,
} from "./stats/statsReport";
import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer";
import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter";
export enum GroupCallIntent {
Ring = "m.ring",
@@ -97,6 +104,7 @@ export enum GroupCallStatsReportEvent {
ConnectionStats = "GroupCall.connection_stats",
ByteSentStats = "GroupCall.byte_sent_stats",
SummaryStats = "GroupCall.summary_stats",
CallFeedStats = "GroupCall.call_feed_stats",
}
/**
@@ -106,6 +114,7 @@ export type GroupCallStatsReportEventHandlerMap = {
[GroupCallStatsReportEvent.ConnectionStats]: (report: GroupCallStatsReport<ConnectionStatsReport>) => void;
[GroupCallStatsReportEvent.ByteSentStats]: (report: GroupCallStatsReport<ByteSentStatsReport>) => void;
[GroupCallStatsReportEvent.SummaryStats]: (report: GroupCallStatsReport<SummaryStatsReport>) => void;
[GroupCallStatsReportEvent.CallFeedStats]: (report: GroupCallStatsReport<CallFeedReport>) => void;
};
export enum GroupCallErrorCode {
@@ -114,7 +123,9 @@ export enum GroupCallErrorCode {
PlaceCallFailed = "place_call_failed",
}
export interface GroupCallStatsReport<T extends ConnectionStatsReport | ByteSentStatsReport | SummaryStatsReport> {
export interface GroupCallStatsReport<
T extends ConnectionStatsReport | ByteSentStatsReport | SummaryStatsReport | CallFeedReport,
> {
report: T;
}
@@ -288,6 +299,22 @@ export class GroupCall extends TypedEventEmitter<
this.emit(GroupCallStatsReportEvent.SummaryStats, { report });
};
private onCallFeedReport = (report: CallFeedReport): void => {
if (this.localCallFeed) {
report = CallFeedStatsReporter.expandCallFeedReport(report, [this.localCallFeed], "from-local-feed");
}
const callFeeds: CallFeed[] = [];
this.forEachCall((call) => {
if (call.callId === report.callId) {
call.getFeeds().forEach((f) => callFeeds.push(f));
}
});
report = CallFeedStatsReporter.expandCallFeedReport(report, callFeeds, "from-call-feed");
this.emit(GroupCallStatsReportEvent.CallFeedStats, { report });
};
public async create(): Promise<GroupCall> {
this.creationTs = Date.now();
this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this);
@@ -1642,6 +1669,7 @@ export class GroupCall extends TypedEventEmitter<
this.stats.reports.on(StatsReport.CONNECTION_STATS, this.onConnectionStats);
this.stats.reports.on(StatsReport.BYTE_SENT_STATS, this.onByteSentStats);
this.stats.reports.on(StatsReport.SUMMARY_STATS, this.onSummaryStats);
this.stats.reports.on(StatsReport.CALL_FEED_REPORT, this.onCallFeedReport);
}
return this.stats;
}

View File

@@ -0,0 +1,94 @@
/*
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 { CallFeedReport, CallFeedStats, TrackStats, TransceiverStats } from "./statsReport";
import { CallFeed } from "../callFeed";
export class CallFeedStatsReporter {
public static buildCallFeedReport(callId: string, opponentMemberId: string, pc: RTCPeerConnection): CallFeedReport {
const rtpTransceivers = pc.getTransceivers();
const transceiver: TransceiverStats[] = [];
const callFeeds: CallFeedStats[] = [];
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,
};
}
private static buildTrackStats(track: MediaStreamTrack, label = "--"): TrackStats {
const settingDeviceId = track.getSettings()?.deviceId;
const constrainDeviceId = track.getConstraints()?.deviceId;
return {
id: track.id,
kind: track.kind,
settingDeviceId: settingDeviceId ? settingDeviceId : "unknown",
constrainDeviceId: constrainDeviceId ? constrainDeviceId : "unknown",
muted: track.muted,
enabled: track.enabled,
readyState: track.readyState,
label,
} as TrackStats;
}
public static expandCallFeedReport(
report: CallFeedReport,
callFeeds: CallFeed[],
prefix = "unknown",
): CallFeedReport {
if (!report.callFeeds) {
report.callFeeds = [];
}
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(),
} as CallFeedStats;
report.callFeeds.push(feedStats);
});
return report;
}
}

View File

@@ -26,6 +26,8 @@ import { TrackStatsBuilder } from "./trackStatsBuilder";
import { ConnectionStatsReportBuilder } from "./connectionStatsReportBuilder";
import { ValueFormatter } from "./valueFormatter";
import { CallStatsReportSummary } from "./callStatsReportSummary";
import { logger } from "../../logger";
import { CallFeedStatsReporter } from "./callFeedStatsReporter";
export class CallStatsReportGatherer {
private isActive = true;
@@ -62,10 +64,11 @@ export class CallStatsReportGatherer {
.then((report) => {
// @ts-ignore
this.currentStatsReport = typeof report?.result === "function" ? report.result() : report;
try {
this.processStatsReport(groupCallId, localUserId);
} catch (error) {
this.isActive = false;
this.handleError(error);
return summary;
}
@@ -161,6 +164,9 @@ export class CallStatsReportGatherer {
});
this.emitter.emitByteSendReport(byteSentStatsReport);
this.emitter.emitCallFeedReport(
CallFeedStatsReporter.buildCallFeedReport(this.callId, this.opponentMemberId, this.pc),
);
this.processAndEmitConnectionStatsReport();
}
@@ -172,8 +178,9 @@ export class CallStatsReportGatherer {
return this.isActive;
}
private handleError(_: any): void {
private handleError(error: any): void {
this.isActive = false;
logger.warn(`CallStatsReportGatherer ${this.callId} processStatsReport fails and set to inactive ${error}`);
}
private processAndEmitConnectionStatsReport(): void {

View File

@@ -20,19 +20,22 @@ import { Resolution } from "./media/mediaTrackStats";
export 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 type TrackID = string;
export type ByteSend = number;
/// ByteSentStatsReport ################################################################################################
export interface ByteSentStatsReport extends Map<TrackID, ByteSend> {
callId?: string;
opponentMemberId?: string;
// is a map: `local trackID` => byte send
}
export type TrackID = string;
export type ByteSend = number;
/// ConnectionStatsReport ##############################################################################################
export interface ConnectionStatsReport {
callId?: string;
opponentMemberId?: string;
@@ -68,6 +71,7 @@ export interface CodecMap {
remote: Map<TrackID, string>;
}
/// SummaryStatsReport #################################################################################################
export interface SummaryStatsReport {
/**
* Aggregated the information for percentage of received media
@@ -89,3 +93,41 @@ export interface SummaryStatsReport {
ratioPeerConnectionToDevices?: number;
// Todo: Decide if we want an index (or a timestamp) of this report in relation to the group call, to help differenciate when issues occur and ignore/track initial connection delays.
}
/// CallFeedReport #####################################################################################################
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;
}

View File

@@ -15,11 +15,18 @@ limitations under the License.
*/
import { TypedEventEmitter } from "../../models/typed-event-emitter";
import { ByteSentStatsReport, ConnectionStatsReport, StatsReport, SummaryStatsReport } from "./statsReport";
import {
ByteSentStatsReport,
CallFeedReport,
ConnectionStatsReport,
StatsReport,
SummaryStatsReport,
} from "./statsReport";
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;
};
@@ -32,6 +39,10 @@ export class StatsReportEmitter extends TypedEventEmitter<StatsReport, StatsRepo
this.emit(StatsReport.CONNECTION_STATS, report);
}
public emitCallFeedReport(report: CallFeedReport): void {
this.emit(StatsReport.CALL_FEED_REPORT, report);
}
public emitSummaryStatsReport(report: SummaryStatsReport): void {
this.emit(StatsReport.SUMMARY_STATS, report);
}