Files
element-call-Github/src/state/CallViewModel/CallViewModelTestUtils.ts
T
Timo K. e726c36f67 Drop the auth modules main's PR 4331 added under src
The SDK replaced `src/livekit` and the remote-member modules before that PR
landed on `main`, so its `src/livekit/auth/*` and `remoteMembers/ConnectionMap.ts`
have no consumer here: the same code lives under `sdk/src/media-backend/livekit`.
The imports of them that `main` added to the view model, the developer
settings tab and the test utils go with them.
2026-10-14 10:23:29 +02:00

291 lines
8.3 KiB
TypeScript

/*
Copyright 2025 Element Corp.
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
type Behavior,
constant,
E2eeType,
type LocalRTCMember,
type ParticipationState,
type RTCParticipation,
type MatrixRTCMode,
type ObservableScope,
type RemoteRTCMember,
} from "@element-hq/matrixrtc-sdk";
import { SyncState } from "matrix-js-sdk/lib/sync";
import { BehaviorSubject, map, of, switchMap } from "rxjs";
import { ClientEvent, type RoomMember, type MatrixClient } from "matrix-js-sdk";
import EventEmitter from "events";
import type { CallMembership } from "matrix-js-sdk/lib/matrixrtc";
import { type RaisedHandInfo, type ReactionInfo } from "../../reactions";
import {
type CallViewModel,
createCallViewModel$,
type CallViewModelOptions,
} from "./CallViewModel";
import { createSentCallNotification$ } from "./CallNotificationLifecycle";
import {
mockConfig,
mockMatrixRoom,
mockMatrixRoomMember,
mockRTCParticipation,
mockAudioTrack,
mockMediaDevices,
mockVideoTrack,
type MockTracks,
mockMuteStates,
MockRTCSession,
mockRTCMember,
testScope,
} from "../../utils/test";
import {
alice,
aliceDoppelganger,
bob,
bobZeroWidthSpace,
daveRTL,
daveRTLRtcMember,
local,
localRtcMember,
} from "../../utils/test-fixtures";
import { type MediaDevices } from "../MediaDevices";
mockConfig({
livekit: { livekit_service_url: "http://my-default-service-url.com" },
});
const carol = local;
const dave = mockMatrixRoomMember(daveRTLRtcMember, { rawDisplayName: "Dave" });
/** Whoever publishes media: a stand-in for a LiveKit participant, by identity. */
export interface Participant {
identity: string;
}
export interface CallViewModelInputs {
/** The members whose media has arrived. The local member's always has. */
remoteParticipants$: Behavior<Participant[]>;
rtcMembers$: Behavior<Partial<CallMembership>[]>;
roomMembers: RoomMember[];
/** Whether the local transport is connected. */
connected$: Behavior<boolean>;
speaking: Map<Participant, Behavior<boolean>>;
videoEnabled: Map<Participant, Behavior<boolean>>;
sharingScreen: Map<Participant, Behavior<boolean>>;
mediaDevices: MediaDevices;
initialSyncState: SyncState;
windowSize$: Behavior<{ width: number; height: number }>;
}
export const localParticipant: Participant = { identity: "" };
export function withCallViewModel(mode: MatrixRTCMode) {
return (
{
remoteParticipants$ = constant([]),
rtcMembers$ = constant([localRtcMember]),
roomMembers = [
alice,
aliceDoppelganger,
bob,
bobZeroWidthSpace,
carol,
dave,
daveRTL,
],
connected$ = constant(true),
speaking = new Map(),
videoEnabled = new Map(),
sharingScreen = new Map(),
mediaDevices = mockMediaDevices({}),
initialSyncState = SyncState.Syncing,
windowSize$ = constant({ width: 1000, height: 800 }),
}: Partial<CallViewModelInputs> = {},
continuation: (
vm: CallViewModel,
rtcSession: MockRTCSession,
subjects: {
raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>>;
/** The participation the view model was built on, with spies for its calls. */
rtcParticipation: RTCParticipation;
},
setSyncState: (value: SyncState) => void,
) => void,
options: Partial<CallViewModelOptions> = {},
): void => {
let syncState = initialSyncState;
const setSyncState = (value: SyncState): void => {
const prev = syncState;
syncState = value;
room.client.emit(ClientEvent.Sync, value, prev);
};
const room = mockMatrixRoom({
client: new (class extends EventEmitter {
public getUserId(): string | undefined {
return localRtcMember.userId;
}
public getDeviceId(): string {
return localRtcMember.deviceId;
}
public getDomain(): string {
return "example.com";
}
public getSyncState(): SyncState {
return syncState;
}
})() as Partial<MatrixClient> as MatrixClient,
getMembers: () => roomMembers,
getMembersWithMembership: () => roomMembers,
});
const rtcSession = new MockRTCSession(room, []).withMemberships(
rtcMembers$,
);
const scope = testScope();
const tracksOf = (
scope: ObservableScope,
participant: Participant,
): Behavior<MockTracks> => {
// A microphone and a camera are always published; the screen share
// comes and goes
const microphone = mockAudioTrack({
isActive$: speaking.get(participant) ?? constant(false),
});
const camera = mockVideoTrack({
source: "camera",
muted$: scope.behavior(
(videoEnabled.get(participant) ?? constant(false)).pipe(
map((enabled) => !enabled),
),
),
});
const screenShare = mockVideoTrack({ source: "screenShare" });
return scope.behavior(
(sharingScreen.get(participant) ?? constant(false)).pipe(
map((sharing) =>
sharing ? [microphone, camera, screenShare] : [microphone, camera],
),
),
);
};
const participantOf = (
scope: ObservableScope,
membership: CallMembership,
): Behavior<Participant | undefined> =>
scope.behavior(
remoteParticipants$.pipe(
map((participants) =>
participants.find(
(p) => p.identity === membership.rtcBackendIdentity,
),
),
),
);
const memberOf = (
scope: ObservableScope,
membership: CallMembership,
): RemoteRTCMember =>
mockRTCMember(false, {
membership,
roomMember: roomMembers.find((m) => m.userId === membership.userId),
transportUrl: "http://my-default-service-url.com",
tracks$: scope.behavior(
participantOf(scope, membership).pipe(
switchMap((participant) =>
participant === undefined
? of(null)
: tracksOf(scope, participant),
),
),
),
});
const remoteMembers$ = scope.behavior<RemoteRTCMember[]>(
rtcMembers$.pipe(
map((memberships) =>
(memberships as CallMembership[])
.filter(
(m) =>
m.userId !== localRtcMember.userId ||
m.deviceId !== localRtcMember.deviceId,
)
.map((membership) => memberOf(scope, membership)),
),
),
);
const localMember$ = scope.behavior<LocalRTCMember | null>(
rtcMembers$.pipe(
map((memberships) =>
(memberships as CallMembership[]).some(
(m) =>
m.userId === localRtcMember.userId &&
m.deviceId === localRtcMember.deviceId,
)
? mockRTCMember(true, {
membership: localRtcMember,
roomMember: carol,
transportUrl: "http://my-default-service-url.com",
tracks$: tracksOf(scope, localParticipant),
})
: null,
),
),
);
const rtcParticipation = mockRTCParticipation(scope, {
localMember$,
remoteMembers$,
state$: scope.behavior(
connected$.pipe(
map((connected): ParticipationState =>
connected
? { kind: "connected" }
: { kind: "reconnecting", reason: "media" },
),
),
),
});
const muteStates = mockMuteStates();
const raisedHands$ = new BehaviorSubject<Record<string, RaisedHandInfo>>(
{},
);
const reactions$ = new BehaviorSubject<Record<string, ReactionInfo>>({});
const vm = createCallViewModel$(
scope,
rtcParticipation,
room,
mediaDevices,
muteStates,
{
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
autoLeaveWhenOthersLeft: false,
sentCallNotification$: createSentCallNotification$(
scope,
rtcSession.asMockedSession(),
),
windowSize$,
...options,
},
raisedHands$,
reactions$,
);
void mode;
continuation(
vm,
rtcSession,
{ raisedHands$, rtcParticipation },
setSyncState,
);
};
}