2024-12-02 15:16:58 +00:00
|
|
|
/*
|
|
|
|
|
Copyright 2024 New Vector Ltd.
|
|
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
Please see LICENSE in the repository root for full details.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { render } from "@testing-library/react";
|
|
|
|
|
import { beforeEach, expect, test } from "vitest";
|
|
|
|
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
2024-12-06 12:28:37 +01:00
|
|
|
import { ConnectionState, Room } from "livekit-client";
|
|
|
|
|
import { BehaviorSubject, of } from "rxjs";
|
2024-12-02 15:16:58 +00:00
|
|
|
import { afterEach } from "node:test";
|
|
|
|
|
import { act } from "react";
|
2024-12-06 12:28:37 +01:00
|
|
|
import {
|
|
|
|
|
CallMembership,
|
|
|
|
|
type MatrixRTCSession,
|
|
|
|
|
} from "matrix-js-sdk/src/matrixrtc";
|
2024-12-02 15:16:58 +00:00
|
|
|
|
|
|
|
|
import { soundEffectVolumeSetting } from "../settings/settings";
|
|
|
|
|
import {
|
|
|
|
|
EmittableMockLivekitRoom,
|
|
|
|
|
mockLivekitRoom,
|
|
|
|
|
mockLocalParticipant,
|
|
|
|
|
mockMatrixRoom,
|
|
|
|
|
mockMatrixRoomMember,
|
|
|
|
|
mockMediaPlay,
|
|
|
|
|
mockRemoteParticipant,
|
2024-12-06 12:28:37 +01:00
|
|
|
mockRtcMembership,
|
|
|
|
|
MockRTCSession,
|
2024-12-02 15:16:58 +00:00
|
|
|
} from "../utils/test";
|
|
|
|
|
import { E2eeType } from "../e2ee/e2eeType";
|
|
|
|
|
import { CallViewModel } from "../state/CallViewModel";
|
|
|
|
|
import {
|
|
|
|
|
CallEventAudioRenderer,
|
|
|
|
|
MAX_PARTICIPANT_COUNT_FOR_SOUND,
|
|
|
|
|
} from "./CallEventAudioRenderer";
|
|
|
|
|
|
2024-12-06 12:28:37 +01:00
|
|
|
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
|
|
|
|
const local = mockMatrixRoomMember(localRtcMember);
|
|
|
|
|
const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA");
|
|
|
|
|
const alice = mockMatrixRoomMember(aliceRtcMember);
|
|
|
|
|
const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB");
|
|
|
|
|
const bob = mockMatrixRoomMember(bobRtcMember);
|
2024-12-02 15:16:58 +00:00
|
|
|
const localParticipant = mockLocalParticipant({ identity: "" });
|
2024-12-06 12:28:37 +01:00
|
|
|
const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`;
|
|
|
|
|
const bobId = `${bob.userId}:${bobRtcMember.deviceId}`;
|
2024-12-02 15:16:58 +00:00
|
|
|
const aliceParticipant = mockRemoteParticipant({ identity: aliceId });
|
|
|
|
|
const bobParticipant = mockRemoteParticipant({ identity: bobId });
|
|
|
|
|
|
|
|
|
|
const originalPlayFn = window.HTMLMediaElement.prototype.play;
|
|
|
|
|
|
|
|
|
|
const enterSound = "http://localhost:3000/src/sound/join_call.ogg";
|
|
|
|
|
const leaveSound = "http://localhost:3000/src/sound/left_call.ogg";
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
window.HTMLMediaElement.prototype.play = originalPlayFn;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("plays a sound when entering a call", () => {
|
|
|
|
|
const audioIsPlaying: string[] = mockMediaPlay();
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoomMembers = new Map(
|
|
|
|
|
[local, alice, bob].map((p) => [p.userId, p]),
|
|
|
|
|
);
|
2024-12-02 15:16:58 +00:00
|
|
|
const remoteParticipants = of([aliceParticipant]);
|
|
|
|
|
const liveKitRoom = mockLivekitRoom(
|
|
|
|
|
{ localParticipant },
|
|
|
|
|
{ remoteParticipants },
|
|
|
|
|
);
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoom = mockMatrixRoom({
|
|
|
|
|
client: {
|
|
|
|
|
getUserId: () => localRtcMember.sender,
|
|
|
|
|
getDeviceId: () => localRtcMember.deviceId,
|
|
|
|
|
} as Partial<MatrixClient> as MatrixClient,
|
|
|
|
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const session = new MockRTCSession(matrixRoom, localRtcMember, [
|
|
|
|
|
aliceRtcMember,
|
|
|
|
|
]) as unknown as MatrixRTCSession;
|
2024-12-02 15:16:58 +00:00
|
|
|
|
|
|
|
|
const vm = new CallViewModel(
|
2024-12-06 12:28:37 +01:00
|
|
|
session,
|
2024-12-02 15:16:58 +00:00
|
|
|
liveKitRoom,
|
|
|
|
|
{
|
|
|
|
|
kind: E2eeType.PER_PARTICIPANT,
|
|
|
|
|
},
|
|
|
|
|
of(ConnectionState.Connected),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<CallEventAudioRenderer vm={vm} />);
|
|
|
|
|
expect(audioIsPlaying).toEqual([
|
|
|
|
|
// Joining the call
|
|
|
|
|
enterSound,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("plays no sound when muted", () => {
|
|
|
|
|
soundEffectVolumeSetting.setValue(0);
|
|
|
|
|
const audioIsPlaying: string[] = mockMediaPlay();
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoomMembers = new Map(
|
|
|
|
|
[local, alice, bob].map((p) => [p.userId, p]),
|
|
|
|
|
);
|
2024-12-02 15:16:58 +00:00
|
|
|
const remoteParticipants = of([aliceParticipant, bobParticipant]);
|
|
|
|
|
const liveKitRoom = mockLivekitRoom(
|
|
|
|
|
{ localParticipant },
|
|
|
|
|
{ remoteParticipants },
|
|
|
|
|
);
|
|
|
|
|
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoom = mockMatrixRoom({
|
|
|
|
|
client: {
|
|
|
|
|
getUserId: () => localRtcMember.sender,
|
|
|
|
|
getDeviceId: () => localRtcMember.deviceId,
|
|
|
|
|
} as Partial<MatrixClient> as MatrixClient,
|
|
|
|
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const session = new MockRTCSession(matrixRoom, localRtcMember, [
|
|
|
|
|
aliceRtcMember,
|
|
|
|
|
]) as unknown as MatrixRTCSession;
|
|
|
|
|
|
2024-12-02 15:16:58 +00:00
|
|
|
const vm = new CallViewModel(
|
2024-12-06 12:28:37 +01:00
|
|
|
session,
|
2024-12-02 15:16:58 +00:00
|
|
|
liveKitRoom,
|
|
|
|
|
{
|
|
|
|
|
kind: E2eeType.PER_PARTICIPANT,
|
|
|
|
|
},
|
|
|
|
|
of(ConnectionState.Connected),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<CallEventAudioRenderer vm={vm} />);
|
|
|
|
|
// Play a sound when joining a call.
|
|
|
|
|
expect(audioIsPlaying).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("plays a sound when a user joins", () => {
|
|
|
|
|
const audioIsPlaying: string[] = mockMediaPlay();
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoomMembers = new Map([local, alice].map((p) => [p.userId, p]));
|
2024-12-02 15:16:58 +00:00
|
|
|
const remoteParticipants = new Map(
|
|
|
|
|
[aliceParticipant].map((p) => [p.identity, p]),
|
|
|
|
|
);
|
|
|
|
|
const liveKitRoom = new EmittableMockLivekitRoom({
|
|
|
|
|
localParticipant,
|
|
|
|
|
remoteParticipants,
|
|
|
|
|
});
|
|
|
|
|
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoom = mockMatrixRoom({
|
|
|
|
|
client: {
|
|
|
|
|
getUserId: () => localRtcMember.sender,
|
|
|
|
|
getDeviceId: () => localRtcMember.deviceId,
|
|
|
|
|
} as Partial<MatrixClient> as MatrixClient,
|
|
|
|
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>([
|
|
|
|
|
aliceRtcMember,
|
|
|
|
|
]);
|
|
|
|
|
// we give Bob an RTC session now, but no participant yet
|
|
|
|
|
const session = new MockRTCSession(
|
|
|
|
|
matrixRoom,
|
|
|
|
|
localRtcMember,
|
|
|
|
|
).withMemberships(
|
|
|
|
|
remoteRtcMemberships.asObservable(),
|
|
|
|
|
) as unknown as MatrixRTCSession;
|
|
|
|
|
|
2024-12-02 15:16:58 +00:00
|
|
|
const vm = new CallViewModel(
|
2024-12-06 12:28:37 +01:00
|
|
|
session,
|
2024-12-02 15:16:58 +00:00
|
|
|
liveKitRoom as unknown as Room,
|
|
|
|
|
{
|
|
|
|
|
kind: E2eeType.PER_PARTICIPANT,
|
|
|
|
|
},
|
|
|
|
|
of(ConnectionState.Connected),
|
|
|
|
|
);
|
|
|
|
|
render(<CallEventAudioRenderer vm={vm} />);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
2024-12-06 12:28:37 +01:00
|
|
|
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
2024-12-02 15:16:58 +00:00
|
|
|
});
|
|
|
|
|
// Play a sound when joining a call.
|
|
|
|
|
expect(audioIsPlaying).toEqual([
|
|
|
|
|
// Joining the call
|
|
|
|
|
enterSound,
|
2024-12-06 12:28:37 +01:00
|
|
|
// Bob joins
|
2024-12-02 15:16:58 +00:00
|
|
|
enterSound,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("plays a sound when a user leaves", () => {
|
|
|
|
|
const audioIsPlaying: string[] = mockMediaPlay();
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoomMembers = new Map([local, alice].map((p) => [p.userId, p]));
|
2024-12-02 15:16:58 +00:00
|
|
|
const remoteParticipants = new Map(
|
|
|
|
|
[aliceParticipant].map((p) => [p.identity, p]),
|
|
|
|
|
);
|
|
|
|
|
const liveKitRoom = new EmittableMockLivekitRoom({
|
|
|
|
|
localParticipant,
|
|
|
|
|
remoteParticipants,
|
|
|
|
|
});
|
|
|
|
|
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoom = mockMatrixRoom({
|
|
|
|
|
client: {
|
|
|
|
|
getUserId: () => localRtcMember.sender,
|
|
|
|
|
getDeviceId: () => localRtcMember.deviceId,
|
|
|
|
|
} as Partial<MatrixClient> as MatrixClient,
|
|
|
|
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>([
|
|
|
|
|
aliceRtcMember,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const session = new MockRTCSession(
|
|
|
|
|
matrixRoom,
|
|
|
|
|
localRtcMember,
|
|
|
|
|
).withMemberships(remoteRtcMemberships) as unknown as MatrixRTCSession;
|
|
|
|
|
|
2024-12-02 15:16:58 +00:00
|
|
|
const vm = new CallViewModel(
|
2024-12-06 12:28:37 +01:00
|
|
|
session,
|
2024-12-02 15:16:58 +00:00
|
|
|
liveKitRoom as unknown as Room,
|
|
|
|
|
{
|
|
|
|
|
kind: E2eeType.PER_PARTICIPANT,
|
|
|
|
|
},
|
|
|
|
|
of(ConnectionState.Connected),
|
|
|
|
|
);
|
|
|
|
|
render(<CallEventAudioRenderer vm={vm} />);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
2024-12-06 12:28:37 +01:00
|
|
|
remoteRtcMemberships.next([]);
|
2024-12-02 15:16:58 +00:00
|
|
|
});
|
|
|
|
|
expect(audioIsPlaying).toEqual([
|
|
|
|
|
// Joining the call
|
|
|
|
|
enterSound,
|
|
|
|
|
// Alice leaves
|
|
|
|
|
leaveSound,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2024-12-06 12:28:37 +01:00
|
|
|
test("plays no sound when the session member count is larger than the max, until decreased", () => {
|
2024-12-02 15:16:58 +00:00
|
|
|
const audioIsPlaying: string[] = mockMediaPlay();
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoomMembers = new Map([local, alice].map((p) => [p.userId, p]));
|
|
|
|
|
const remoteParticipants = new Map(
|
|
|
|
|
[aliceParticipant].map((p) => [p.identity, p]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mockRtcMemberships: CallMembership[] = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) {
|
|
|
|
|
mockRtcMemberships.push(
|
|
|
|
|
mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>(
|
|
|
|
|
mockRtcMemberships,
|
|
|
|
|
);
|
|
|
|
|
|
2024-12-02 15:16:58 +00:00
|
|
|
const liveKitRoom = new EmittableMockLivekitRoom({
|
|
|
|
|
localParticipant,
|
|
|
|
|
remoteParticipants,
|
|
|
|
|
});
|
|
|
|
|
|
2024-12-06 12:28:37 +01:00
|
|
|
const matrixRoom = mockMatrixRoom({
|
|
|
|
|
client: {
|
|
|
|
|
getUserId: () => localRtcMember.sender,
|
|
|
|
|
getDeviceId: () => localRtcMember.deviceId,
|
|
|
|
|
} as Partial<MatrixClient> as MatrixClient,
|
|
|
|
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const session = new MockRTCSession(
|
|
|
|
|
matrixRoom,
|
|
|
|
|
localRtcMember,
|
|
|
|
|
).withMemberships(remoteRtcMemberships) as unknown as MatrixRTCSession;
|
|
|
|
|
|
2024-12-02 15:16:58 +00:00
|
|
|
const vm = new CallViewModel(
|
2024-12-06 12:28:37 +01:00
|
|
|
session,
|
2024-12-02 15:16:58 +00:00
|
|
|
liveKitRoom as unknown as Room,
|
|
|
|
|
{
|
|
|
|
|
kind: E2eeType.PER_PARTICIPANT,
|
|
|
|
|
},
|
|
|
|
|
of(ConnectionState.Connected),
|
|
|
|
|
);
|
|
|
|
|
render(<CallEventAudioRenderer vm={vm} />);
|
|
|
|
|
expect(audioIsPlaying).toEqual([]);
|
2024-12-06 12:28:37 +01:00
|
|
|
// When the count drops to the max we should play the leave sound
|
2024-12-02 15:16:58 +00:00
|
|
|
act(() => {
|
2024-12-06 12:28:37 +01:00
|
|
|
remoteRtcMemberships.next(
|
|
|
|
|
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1),
|
|
|
|
|
);
|
2024-12-02 15:16:58 +00:00
|
|
|
});
|
|
|
|
|
expect(audioIsPlaying).toEqual([leaveSound]);
|
|
|
|
|
});
|