Skip to content

Commit

Permalink
Expand webrtc stats with connection and call feed track information (m…
Browse files Browse the repository at this point in the history
…atrix-org#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
  • Loading branch information
Enrico Schwendig authored Jun 7, 2023
1 parent 60c715d commit 3cfad3c
Show file tree
Hide file tree
Showing 11 changed files with 1,710 additions and 19 deletions.
1,114 changes: 1,114 additions & 0 deletions spec/test-utils/webrtcReports.ts

Large diffs are not rendered by default.

110 changes: 99 additions & 11 deletions spec/unit/webrtc/groupCall.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,35 @@ 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";
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 = [
{
Expand Down Expand Up @@ -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;
});
});
});
Original file line number Diff line number Diff line change
@@ -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",
},
},
]
`;
117 changes: 117 additions & 0 deletions spec/unit/webrtc/stats/callFeedStatsReporter.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
};
});
Loading

0 comments on commit 3cfad3c

Please sign in to comment.