mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
use the matrixRtcSdk
This commit is contained in:
+198
-135
@@ -4,7 +4,14 @@ Copyright 2023, 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 { map, type Observable, of, type SchedulerLike } from "rxjs";
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
NEVER,
|
||||
type Observable,
|
||||
of,
|
||||
type SchedulerLike,
|
||||
} from "rxjs";
|
||||
import { type RunHelpers, TestScheduler } from "rxjs/testing";
|
||||
import {
|
||||
expect,
|
||||
@@ -36,23 +43,13 @@ import {
|
||||
type Transport,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type MembershipManagerEventHandlerMap } from "matrix-js-sdk/lib/matrixrtc/IMembershipManager";
|
||||
import {
|
||||
type LocalParticipant,
|
||||
type LocalTrackPublication,
|
||||
type Participant,
|
||||
type RemoteParticipant,
|
||||
type RemoteTrackPublication,
|
||||
type Room as LivekitRoom,
|
||||
Track,
|
||||
} from "livekit-client";
|
||||
import { type RemoteParticipant } from "livekit-client";
|
||||
import { randomUUID } from "crypto";
|
||||
import { type TrackReference } from "@livekit/components-core";
|
||||
import EventEmitter from "events";
|
||||
import {
|
||||
type KeyTransportEvents,
|
||||
type KeyTransportEventsHandlerMap,
|
||||
} from "matrix-js-sdk/lib/matrixrtc/IKeyTransport";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import {
|
||||
@@ -61,7 +58,20 @@ import {
|
||||
} from "../config/ConfigOptions";
|
||||
import { Config } from "../config/Config";
|
||||
import { type MediaDevices } from "../state/MediaDevices";
|
||||
import { type Behavior, constant } from "../state/Behavior";
|
||||
import {
|
||||
type AudioMediaTrack,
|
||||
type Behavior,
|
||||
constant,
|
||||
type DisconnectReason,
|
||||
type LocalMemberMedia,
|
||||
type LocalRTCMember,
|
||||
type MatrixRTCClient,
|
||||
type MatrixRTCError,
|
||||
type MemberMedia,
|
||||
type RemoteRTCMember,
|
||||
type TransportMetadata,
|
||||
type VideoMediaTrack,
|
||||
} from "@element-hq/matrixrtc-sdk";
|
||||
import { ObservableScope } from "../state/ObservableScope";
|
||||
import { MuteStates } from "../state/MuteStates";
|
||||
import { nullHostBridge } from "../HostBridge";
|
||||
@@ -77,8 +87,6 @@ import {
|
||||
createRemoteScreenShare,
|
||||
type RemoteScreenShareViewModel,
|
||||
} from "../state/media/RemoteScreenShareViewModel";
|
||||
import { Connection } from "../state/CallViewModel/remoteMembers/Connection";
|
||||
import { type TransportLocator } from "../livekit/auth";
|
||||
|
||||
export function withFakeTimers(continuation: () => void): void {
|
||||
vi.useFakeTimers();
|
||||
@@ -93,13 +101,6 @@ export async function flushPromises(): Promise<void> {
|
||||
await new Promise<void>((resolve) => window.setTimeout(resolve));
|
||||
}
|
||||
|
||||
export type NodeEventHandler = (...args: unknown[]) => void;
|
||||
|
||||
export interface NodeStyleEventEmitter {
|
||||
addListener(eventName: string | symbol, handler: NodeEventHandler): this;
|
||||
removeListener(eventName: string | symbol, handler: NodeEventHandler): this;
|
||||
}
|
||||
|
||||
export interface OurRunHelpers extends RunHelpers {
|
||||
/**
|
||||
* Schedules a sequence of actions to happen, as described by a marble
|
||||
@@ -214,11 +215,6 @@ export const exampleTransport: UnstableLivekitTransport = {
|
||||
livekit_service_url: "https://lk.example.org",
|
||||
};
|
||||
|
||||
export const exampleTransportLocator: TransportLocator = {
|
||||
transport: exampleTransport,
|
||||
serverName: "example.org",
|
||||
};
|
||||
|
||||
export function mockRtcMembership(
|
||||
user: string | RoomMember,
|
||||
deviceId: string,
|
||||
@@ -273,11 +269,6 @@ export function mockRtcMembership(
|
||||
return cms;
|
||||
}
|
||||
|
||||
export const ownMemberMock: CallMembershipIdentityParts = {
|
||||
userId: "@alice:example.org",
|
||||
deviceId: "DEVICE",
|
||||
memberId: "@alice:example.org:DEVICE",
|
||||
};
|
||||
// Maybe it'd be good to move this to matrix-js-sdk? Our testing needs are
|
||||
// rather simple, but if one util to mock a member is good enough for us, maybe
|
||||
// it's useful for matrix-js-sdk consumers in general.
|
||||
@@ -300,54 +291,76 @@ export function mockMatrixRoom(room: Partial<MatrixRoom>): MatrixRoom {
|
||||
return { ...mockEmitter(), ...room } as Partial<MatrixRoom> as MatrixRoom;
|
||||
}
|
||||
|
||||
export function mockLivekitRoom(
|
||||
room: Partial<LivekitRoom>,
|
||||
{
|
||||
remoteParticipants$,
|
||||
}: { remoteParticipants$?: Observable<RemoteParticipant[]> } = {},
|
||||
): LivekitRoom {
|
||||
const livekitRoom = {
|
||||
options: {},
|
||||
setE2EEEnabled: vi.fn(),
|
||||
/** A track as the SDK hands it out, with spies where a test may look. */
|
||||
export type MockMediaTrack = Omit<AudioMediaTrack, "kind"> &
|
||||
Omit<VideoMediaTrack, "kind"> & { kind: "audio" | "video" };
|
||||
|
||||
...mockEmitter(),
|
||||
...room,
|
||||
} as Partial<LivekitRoom> as LivekitRoom;
|
||||
if (remoteParticipants$) {
|
||||
livekitRoom.remoteParticipants = new Map();
|
||||
remoteParticipants$.subscribe((newRemoteParticipants) => {
|
||||
livekitRoom.remoteParticipants.clear();
|
||||
newRemoteParticipants.forEach((p) => {
|
||||
livekitRoom.remoteParticipants.set(p.identity, p);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return livekitRoom;
|
||||
export function mockMediaTrack(
|
||||
track: Partial<MockMediaTrack> = {},
|
||||
): MockMediaTrack {
|
||||
return {
|
||||
source: "camera",
|
||||
kind: "video",
|
||||
id: "track",
|
||||
muted$: constant(false),
|
||||
encrypted$: constant(true),
|
||||
stats$: constant(undefined),
|
||||
attach: vi.fn(),
|
||||
detach: vi.fn(),
|
||||
setAudioContext: vi.fn(),
|
||||
setVolume: vi.fn(),
|
||||
facingMode$: constant(undefined),
|
||||
...track,
|
||||
};
|
||||
}
|
||||
|
||||
export function mockLocalParticipant(
|
||||
participant: Partial<LocalParticipant>,
|
||||
): LocalParticipant {
|
||||
export const mockVideoTrack = (
|
||||
track: Partial<MockMediaTrack> = {},
|
||||
): VideoMediaTrack =>
|
||||
mockMediaTrack({ kind: "video", ...track }) as VideoMediaTrack;
|
||||
|
||||
export const mockAudioTrack = (
|
||||
track: Partial<MockMediaTrack> = {},
|
||||
): AudioMediaTrack =>
|
||||
mockMediaTrack({
|
||||
kind: "audio",
|
||||
source: "microphone",
|
||||
...track,
|
||||
}) as AudioMediaTrack;
|
||||
|
||||
/** A member's media as the SDK hands it out, with nothing published unless given. */
|
||||
export function mockMemberMedia(
|
||||
media: Partial<Omit<LocalMemberMedia, "local">> & { local?: boolean } = {},
|
||||
): LocalMemberMedia {
|
||||
return {
|
||||
isLocal: true,
|
||||
trackPublications: new Map(),
|
||||
publishTrack: vi.fn(),
|
||||
unpublishTracks: vi.fn().mockResolvedValue([]),
|
||||
createTracks: vi.fn(),
|
||||
setMicrophoneEnabled: vi.fn(),
|
||||
setCameraEnabled: vi.fn(),
|
||||
getTrackPublication: () =>
|
||||
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||
local: false,
|
||||
speaking$: constant(false),
|
||||
screenShareEnabled$: constant(false),
|
||||
microphone$: constant(undefined),
|
||||
camera$: constant(undefined),
|
||||
screenShare$: constant(undefined),
|
||||
screenShareAudio$: constant(undefined),
|
||||
encryptionError$: NEVER,
|
||||
switchCamera: vi.fn().mockResolvedValue(undefined),
|
||||
...media,
|
||||
} as LocalMemberMedia;
|
||||
}
|
||||
|
||||
/** Whoever publishes media, known by identity only. */
|
||||
export function mockRemoteParticipant(
|
||||
participant: Partial<RemoteParticipant>,
|
||||
): RemoteParticipant {
|
||||
return {
|
||||
isLocal: false,
|
||||
...mockEmitter(),
|
||||
...participant,
|
||||
} as Partial<LocalParticipant> as LocalParticipant;
|
||||
} as RemoteParticipant;
|
||||
}
|
||||
|
||||
export function mockLocalMedia(
|
||||
rtcMember: CallMembership,
|
||||
roomMember: Partial<RoomMember>,
|
||||
localParticipant: LocalParticipant,
|
||||
media: LocalMemberMedia | null,
|
||||
mediaDevices: MediaDevices,
|
||||
): LocalUserMediaViewModel {
|
||||
const member = mockMatrixRoomMember(rtcMember, roomMember);
|
||||
@@ -355,10 +368,9 @@ export function mockLocalMedia(
|
||||
id: "local",
|
||||
userId: member.userId,
|
||||
rtcBackendIdentity: rtcMember.rtcBackendIdentity,
|
||||
participant$: constant(localParticipant),
|
||||
media$: constant(media),
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
livekitRoom$: constant(mockLivekitRoom({ localParticipant })),
|
||||
transport$: constant(exampleTransport),
|
||||
focusUrl$: constant("https://rtc-example.org"),
|
||||
mediaDevices,
|
||||
displayName$: constant(member.rawDisplayName ?? "nodisplayname"),
|
||||
mxcAvatarUrl$: constant(member.getMxcAvatarUrl()),
|
||||
@@ -367,41 +379,20 @@ export function mockLocalMedia(
|
||||
});
|
||||
}
|
||||
|
||||
export function mockRemoteParticipant(
|
||||
participant: Partial<RemoteParticipant>,
|
||||
): RemoteParticipant {
|
||||
return {
|
||||
isLocal: false,
|
||||
setVolume() {},
|
||||
getTrackPublication: () =>
|
||||
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
||||
// this will only get used for `getTrackPublications().length`
|
||||
getTrackPublications: () => [0],
|
||||
...mockEmitter(),
|
||||
...participant,
|
||||
} as RemoteParticipant;
|
||||
}
|
||||
|
||||
export function mockRemoteMedia(
|
||||
rtcMember: CallMembership,
|
||||
roomMember: Partial<RoomMember>,
|
||||
participant: RemoteParticipant | null,
|
||||
livekitRoom: LivekitRoom | undefined = mockLivekitRoom(
|
||||
{},
|
||||
{
|
||||
remoteParticipants$: of(participant ? [participant] : []),
|
||||
},
|
||||
),
|
||||
media: MemberMedia | null,
|
||||
{ focusUrl }: { focusUrl?: string } = { focusUrl: "https://rtc-example.org" },
|
||||
): RemoteUserMediaViewModel {
|
||||
const member = mockMatrixRoomMember(rtcMember, roomMember);
|
||||
return createRemoteUserMedia(testScope(), {
|
||||
id: "remote",
|
||||
userId: member.userId,
|
||||
rtcBackendIdentity: rtcMember.rtcBackendIdentity,
|
||||
participant$: constant(participant),
|
||||
media$: constant(media),
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
livekitRoom$: constant(livekitRoom),
|
||||
transport$: constant(exampleTransport),
|
||||
focusUrl$: constant(focusUrl),
|
||||
pretendToBeDisconnected$: constant(false),
|
||||
displayName$: constant(member.rawDisplayName ?? "nodisplayname"),
|
||||
mxcAvatarUrl$: constant(member.getMxcAvatarUrl()),
|
||||
@@ -413,28 +404,128 @@ export function mockRemoteMedia(
|
||||
export function mockRemoteScreenShare(
|
||||
rtcMember: CallMembership,
|
||||
roomMember: Partial<RoomMember>,
|
||||
participant: RemoteParticipant | null,
|
||||
livekitRoom: LivekitRoom | undefined = mockLivekitRoom(
|
||||
{},
|
||||
{
|
||||
remoteParticipants$: of(participant ? [participant] : []),
|
||||
},
|
||||
),
|
||||
media: MemberMedia | null,
|
||||
): RemoteScreenShareViewModel {
|
||||
const member = mockMatrixRoomMember(rtcMember, roomMember);
|
||||
return createRemoteScreenShare(testScope(), {
|
||||
id: "screenshare",
|
||||
userId: member.userId,
|
||||
participant$: constant(participant),
|
||||
media$: constant(media),
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
livekitRoom$: constant(livekitRoom),
|
||||
transport$: constant(exampleTransport),
|
||||
focusUrl$: constant("https://rtc-example.org"),
|
||||
pretendToBeDisconnected$: constant(false),
|
||||
displayName$: constant(member.rawDisplayName ?? "nodisplayname"),
|
||||
mxcAvatarUrl$: constant(member.getMxcAvatarUrl()),
|
||||
});
|
||||
}
|
||||
|
||||
export interface MockMemberInputs {
|
||||
membership: CallMembership;
|
||||
roomMember?: Partial<RoomMember>;
|
||||
media$?: Behavior<LocalMemberMedia | null>;
|
||||
transportUrl?: string | undefined;
|
||||
}
|
||||
|
||||
/** A member as the client hands it out. */
|
||||
export function mockRTCMember(
|
||||
local: true,
|
||||
inputs: MockMemberInputs,
|
||||
): LocalRTCMember;
|
||||
export function mockRTCMember(
|
||||
local: false,
|
||||
inputs: MockMemberInputs,
|
||||
): RemoteRTCMember;
|
||||
export function mockRTCMember(
|
||||
local: boolean,
|
||||
{ membership, roomMember, media$, transportUrl }: MockMemberInputs,
|
||||
): LocalRTCMember | RemoteRTCMember {
|
||||
const member = mockMatrixRoomMember(membership, roomMember);
|
||||
const transport: TransportMetadata | undefined =
|
||||
transportUrl === undefined
|
||||
? undefined
|
||||
: {
|
||||
type: "livekit",
|
||||
id: transportUrl,
|
||||
raw: { type: "livekit", livekit_service_url: transportUrl },
|
||||
resolved$: constant(undefined),
|
||||
};
|
||||
const base = {
|
||||
id: membership.rtcBackendIdentity,
|
||||
userId: membership.userId,
|
||||
deviceId: membership.deviceId,
|
||||
membership$: constant(membership),
|
||||
displayName$: constant(member.rawDisplayName ?? membership.userId),
|
||||
avatarUrl$: constant(member.getMxcAvatarUrl()),
|
||||
transport$: constant(transport),
|
||||
media$: media$ ?? constant(null),
|
||||
};
|
||||
if (!local) return { ...base, local: false };
|
||||
return {
|
||||
...base,
|
||||
local: true,
|
||||
sharingScreen$: constant(false),
|
||||
toggleScreenSharing: null,
|
||||
screenShareError$: constant(null),
|
||||
dismissScreenShareError: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export interface MockClientInputs {
|
||||
localMember$?: Behavior<LocalRTCMember | null>;
|
||||
remoteMembers$?: Behavior<RemoteRTCMember[]>;
|
||||
connected$?: Behavior<boolean>;
|
||||
reconnecting$?: Behavior<boolean>;
|
||||
disconnectReason$?: Behavior<DisconnectReason | null>;
|
||||
fatalError$?: Behavior<MatrixRTCError | null>;
|
||||
keyRotationSuppressed$?: Behavior<boolean>;
|
||||
connectedTransports$?: Behavior<TransportMetadata[]>;
|
||||
}
|
||||
|
||||
/** A client that does nothing but hold the state a test hands it. */
|
||||
export function mockMatrixRTCClient(
|
||||
scope: ObservableScope,
|
||||
{
|
||||
localMember$ = constant(null),
|
||||
remoteMembers$ = constant([]),
|
||||
connected$ = constant(true),
|
||||
reconnecting$ = constant(false),
|
||||
disconnectReason$ = constant(null),
|
||||
fatalError$ = constant(null),
|
||||
keyRotationSuppressed$ = constant(false),
|
||||
connectedTransports$ = constant([]),
|
||||
}: MockClientInputs = {},
|
||||
): MatrixRTCClient {
|
||||
return {
|
||||
join: vi.fn(),
|
||||
leave: vi.fn(),
|
||||
status$: scope.behavior(
|
||||
connected$.pipe(map((c) => (c ? "connected" : "connecting"))),
|
||||
),
|
||||
connected$,
|
||||
reconnecting$,
|
||||
disconnectReason$,
|
||||
setMicrophoneEnabled: vi.fn(async (enabled: boolean) =>
|
||||
Promise.resolve(enabled),
|
||||
),
|
||||
setCameraEnabled: vi.fn(async (enabled: boolean) =>
|
||||
Promise.resolve(enabled),
|
||||
),
|
||||
fatalError$,
|
||||
localMember$,
|
||||
remoteMembers$,
|
||||
memberCount$: scope.behavior(
|
||||
combineLatest(
|
||||
[localMember$, remoteMembers$],
|
||||
(local, remote) => remote.length + (local === null ? 0 : 1),
|
||||
),
|
||||
),
|
||||
keyRotationSuppressed$,
|
||||
connectedTransports$,
|
||||
sendData: vi.fn(async () => Promise.resolve()),
|
||||
data$: NEVER,
|
||||
};
|
||||
}
|
||||
|
||||
export function mockConfig(
|
||||
config: Partial<ResolvedConfigOptions> = {},
|
||||
): MockInstance<() => ResolvedConfigOptions> {
|
||||
@@ -537,29 +628,6 @@ export class MockRTCSession extends TypedEventEmitter<
|
||||
}
|
||||
}
|
||||
|
||||
export const mockTrack = (
|
||||
participant: Participant,
|
||||
kind?: Track.Kind,
|
||||
source?: Track.Source,
|
||||
): TrackReference =>
|
||||
({
|
||||
participant,
|
||||
publication: {
|
||||
kind: kind ?? Track.Kind.Audio,
|
||||
source: source ?? Track.Source.Microphone,
|
||||
trackSid: `123##${participant.identity}`,
|
||||
track: {
|
||||
attach: vi.fn(),
|
||||
detach: vi.fn(),
|
||||
setAudioContext: vi.fn(),
|
||||
setWebAudioPlugins: vi.fn(),
|
||||
setVolume: vi.fn(),
|
||||
},
|
||||
},
|
||||
track: {},
|
||||
source: {},
|
||||
}) as unknown as TrackReference;
|
||||
|
||||
export const deviceStub = {
|
||||
available$: of(new Map<never, never>()),
|
||||
selected$: of(undefined),
|
||||
@@ -587,11 +655,6 @@ export function mockMuteStates(
|
||||
);
|
||||
}
|
||||
|
||||
export class MockConnection extends Connection {
|
||||
public async start(): Promise<void> {}
|
||||
public async stop(): Promise<void> {}
|
||||
}
|
||||
|
||||
export interface StubbedCapture {
|
||||
getUserMedia: Mock;
|
||||
/** Grants the microphone, as the browser does once permission is given. */
|
||||
|
||||
Reference in New Issue
Block a user