diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 9014e60b..78d08f60 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -8,10 +8,14 @@ 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"; -import { ConnectionState, RemoteParticipant, Room } from "livekit-client"; -import { of } from "rxjs"; +import { ConnectionState, Room } from "livekit-client"; +import { BehaviorSubject, of } from "rxjs"; import { afterEach } from "node:test"; import { act } from "react"; +import { + CallMembership, + type MatrixRTCSession, +} from "matrix-js-sdk/src/matrixrtc"; import { soundEffectVolumeSetting } from "../settings/settings"; import { @@ -22,6 +26,8 @@ import { mockMatrixRoomMember, mockMediaPlay, mockRemoteParticipant, + mockRtcMembership, + MockRTCSession, } from "../utils/test"; import { E2eeType } from "../e2ee/e2eeType"; import { CallViewModel } from "../state/CallViewModel"; @@ -30,11 +36,15 @@ import { MAX_PARTICIPANT_COUNT_FOR_SOUND, } from "./CallEventAudioRenderer"; -const alice = mockMatrixRoomMember({ userId: "@alice:example.org" }); -const bob = mockMatrixRoomMember({ userId: "@bob:example.org" }); -const aliceId = `${alice.userId}:AAAA`; -const bobId = `${bob.userId}:BBBB`; +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); const localParticipant = mockLocalParticipant({ identity: "" }); +const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`; +const bobId = `${bob.userId}:${bobRtcMember.deviceId}`; const aliceParticipant = mockRemoteParticipant({ identity: aliceId }); const bobParticipant = mockRemoteParticipant({ identity: bobId }); @@ -53,20 +63,28 @@ afterEach(() => { test("plays a sound when entering a call", () => { const audioIsPlaying: string[] = mockMediaPlay(); - const members = new Map([alice, bob].map((p) => [p.userId, p])); + const matrixRoomMembers = new Map( + [local, alice, bob].map((p) => [p.userId, p]), + ); const remoteParticipants = of([aliceParticipant]); const liveKitRoom = mockLivekitRoom( { localParticipant }, { remoteParticipants }, ); + const matrixRoom = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + } as Partial as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const session = new MockRTCSession(matrixRoom, localRtcMember, [ + aliceRtcMember, + ]) as unknown as MatrixRTCSession; const vm = new CallViewModel( - mockMatrixRoom({ - client: { - getUserId: () => "@carol:example.org", - } as Partial as MatrixClient, - getMember: (userId) => members.get(userId) ?? null, - }), + session, liveKitRoom, { kind: E2eeType.PER_PARTICIPANT, @@ -84,20 +102,29 @@ test("plays a sound when entering a call", () => { test("plays no sound when muted", () => { soundEffectVolumeSetting.setValue(0); const audioIsPlaying: string[] = mockMediaPlay(); - const members = new Map([alice, bob].map((p) => [p.userId, p])); + const matrixRoomMembers = new Map( + [local, alice, bob].map((p) => [p.userId, p]), + ); const remoteParticipants = of([aliceParticipant, bobParticipant]); const liveKitRoom = mockLivekitRoom( { localParticipant }, { remoteParticipants }, ); + const matrixRoom = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + } as Partial as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const session = new MockRTCSession(matrixRoom, localRtcMember, [ + aliceRtcMember, + ]) as unknown as MatrixRTCSession; + const vm = new CallViewModel( - mockMatrixRoom({ - client: { - getUserId: () => "@carol:example.org", - } as Partial as MatrixClient, - getMember: (userId) => members.get(userId) ?? null, - }), + session, liveKitRoom, { kind: E2eeType.PER_PARTICIPANT, @@ -112,7 +139,7 @@ test("plays no sound when muted", () => { test("plays a sound when a user joins", () => { const audioIsPlaying: string[] = mockMediaPlay(); - const members = new Map([alice].map((p) => [p.userId, p])); + const matrixRoomMembers = new Map([local, alice].map((p) => [p.userId, p])); const remoteParticipants = new Map( [aliceParticipant].map((p) => [p.identity, p]), ); @@ -121,13 +148,27 @@ test("plays a sound when a user joins", () => { remoteParticipants, }); + const matrixRoom = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + } as Partial as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const remoteRtcMemberships = new BehaviorSubject([ + 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; + const vm = new CallViewModel( - mockMatrixRoom({ - client: { - getUserId: () => "@carol:example.org", - } as Partial as MatrixClient, - getMember: (userId) => members.get(userId) ?? null, - }), + session, liveKitRoom as unknown as Room, { kind: E2eeType.PER_PARTICIPANT, @@ -137,20 +178,20 @@ test("plays a sound when a user joins", () => { render(); act(() => { - liveKitRoom.addParticipant(bobParticipant); + remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); }); // Play a sound when joining a call. expect(audioIsPlaying).toEqual([ // Joining the call enterSound, - // Bob leaves + // Bob joins enterSound, ]); }); test("plays a sound when a user leaves", () => { const audioIsPlaying: string[] = mockMediaPlay(); - const members = new Map([alice].map((p) => [p.userId, p])); + const matrixRoomMembers = new Map([local, alice].map((p) => [p.userId, p])); const remoteParticipants = new Map( [aliceParticipant].map((p) => [p.identity, p]), ); @@ -159,13 +200,25 @@ test("plays a sound when a user leaves", () => { remoteParticipants, }); + const matrixRoom = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + } as Partial as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const remoteRtcMemberships = new BehaviorSubject([ + aliceRtcMember, + ]); + + const session = new MockRTCSession( + matrixRoom, + localRtcMember, + ).withMemberships(remoteRtcMemberships) as unknown as MatrixRTCSession; + const vm = new CallViewModel( - mockMatrixRoom({ - client: { - getUserId: () => "@carol:example.org", - } as Partial as MatrixClient, - getMember: (userId) => members.get(userId) ?? null, - }), + session, liveKitRoom as unknown as Room, { kind: E2eeType.PER_PARTICIPANT, @@ -175,7 +228,7 @@ test("plays a sound when a user leaves", () => { render(); act(() => { - liveKitRoom.removeParticipant(aliceParticipant); + remoteRtcMemberships.next([]); }); expect(audioIsPlaying).toEqual([ // Joining the call @@ -185,30 +238,45 @@ test("plays a sound when a user leaves", () => { ]); }); -test("plays no sound when the participant list", () => { +test("plays no sound when the session member count is larger than the max, until decreased", () => { const audioIsPlaying: string[] = mockMediaPlay(); - const members = new Map([alice].map((p) => [p.userId, p])); - const remoteParticipants = new Map([ - [aliceParticipant.identity, aliceParticipant], - ...Array.from({ length: MAX_PARTICIPANT_COUNT_FOR_SOUND - 1 }).map< - [string, RemoteParticipant] - >((_, index) => { - const p = mockRemoteParticipant({ identity: `user${index}` }); - return [p.identity, p]; - }), - ]); + 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( + mockRtcMemberships, + ); + const liveKitRoom = new EmittableMockLivekitRoom({ localParticipant, remoteParticipants, }); + const matrixRoom = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + } as Partial as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const session = new MockRTCSession( + matrixRoom, + localRtcMember, + ).withMemberships(remoteRtcMemberships) as unknown as MatrixRTCSession; + const vm = new CallViewModel( - mockMatrixRoom({ - client: { - getUserId: () => "@carol:example.org", - } as Partial as MatrixClient, - getMember: (userId) => members.get(userId) ?? null, - }), + session, liveKitRoom as unknown as Room, { kind: E2eeType.PER_PARTICIPANT, @@ -217,9 +285,11 @@ test("plays no sound when the participant list", () => { ); render(); expect(audioIsPlaying).toEqual([]); - // When the count drops + // When the count drops to the max we should play the leave sound act(() => { - liveKitRoom.removeParticipant(aliceParticipant); + remoteRtcMemberships.next( + mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), + ); }); expect(audioIsPlaying).toEqual([leaveSound]); });