30
src/state/remoteMembers/MatrixLivekitMerger.test.ts
Normal file
30
src/state/remoteMembers/MatrixLivekitMerger.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
test,
|
||||||
|
vi,
|
||||||
|
onTestFinished,
|
||||||
|
it,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
|
import { MatrixLivekitMerger } from "./matrixLivekitMerger";
|
||||||
|
import { ObservableScope } from "../ObservableScope";
|
||||||
|
|
||||||
|
let testScope: ObservableScope;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testScope = new ObservableScope();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testScope.end();
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2025 Element c.
|
Copyright 2025 Element Creations Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
fromEvent,
|
|
||||||
map,
|
map,
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
@@ -36,6 +35,7 @@ import { Behavior, constant } from "../Behavior";
|
|||||||
import { Room as MatrixRoom, RoomMember } from "matrix-js-sdk";
|
import { Room as MatrixRoom, RoomMember } from "matrix-js-sdk";
|
||||||
import { getRoomMemberFromRtcMember } from "./displayname";
|
import { getRoomMemberFromRtcMember } from "./displayname";
|
||||||
import { pauseWhen } from "../../utils/observable";
|
import { pauseWhen } from "../../utils/observable";
|
||||||
|
import { Logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
// TODOs:
|
// TODOs:
|
||||||
// - make ConnectionManager its own actual class
|
// - make ConnectionManager its own actual class
|
||||||
@@ -44,16 +44,17 @@ class ConnectionManager {
|
|||||||
public setTansports(transports$: Behavior<Transport[]>): void {}
|
public setTansports(transports$: Behavior<Transport[]>): void {}
|
||||||
public readonly connections$: Observable<Connection[]> = constant([]);
|
public readonly connections$: Observable<Connection[]> = constant([]);
|
||||||
// connection is used to find the transport (to find matching callmembership) & for the livekitRoom
|
// connection is used to find the transport (to find matching callmembership) & for the livekitRoom
|
||||||
public readonly participantsByMemberId$: Behavior<
|
public readonly participantsByMemberId$: Behavior<ParticipantByMemberIdMap> =
|
||||||
Map<
|
constant(new Map());
|
||||||
ParticipantId,
|
|
||||||
// It can be an array because a bad behaving client could be publishingParticipants$
|
|
||||||
// multiple times to several livekit rooms.
|
|
||||||
{ participant: LivekitParticipant; connection: Connection }[]
|
|
||||||
>
|
|
||||||
> = constant(new Map());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ParticipantByMemberIdMap = Map<
|
||||||
|
ParticipantId,
|
||||||
|
// It can be an array because a bad behaving client could be publishingParticipants$
|
||||||
|
// multiple times to several livekit rooms.
|
||||||
|
{ participant: LivekitParticipant; connection: Connection }[]
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents participant publishing or expected to publish on the connection.
|
* Represents participant publishing or expected to publish on the connection.
|
||||||
* It is paired with its associated rtc membership.
|
* It is paired with its associated rtc membership.
|
||||||
@@ -111,27 +112,20 @@ interface LivekitRoomWithParticipants {
|
|||||||
* - `remoteMatrixLivekitItems` an observable of MatrixLivekitItem[] to track the remote members and associated livekit data.
|
* - `remoteMatrixLivekitItems` an observable of MatrixLivekitItem[] to track the remote members and associated livekit data.
|
||||||
*/
|
*/
|
||||||
export class MatrixLivekitMerger {
|
export class MatrixLivekitMerger {
|
||||||
/**
|
private readonly logger: Logger;
|
||||||
* The MatrixRTC session participants.
|
|
||||||
*/
|
|
||||||
// Note that MatrixRTCSession already filters the call memberships by users
|
|
||||||
// that are joined to the room; we don't need to perform extra filtering here.
|
|
||||||
public readonly memberships$ = this.scope.behavior(
|
|
||||||
fromEvent(
|
|
||||||
this.matrixRTCSession,
|
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
|
||||||
).pipe(
|
|
||||||
startWith(null),
|
|
||||||
map(() => this.matrixRTCSession.memberships),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private matrixRTCSession: MatrixRTCSession,
|
private memberships$: Observable<CallMembership[]>,
|
||||||
private connectionManager: ConnectionManager,
|
private connectionManager: ConnectionManager,
|
||||||
private scope: ObservableScope,
|
private scope: ObservableScope,
|
||||||
|
// TODO this is too much information for that class,
|
||||||
|
// apparently needed to get a room member to later get the Avatar
|
||||||
|
// => Extract an AvatarService instead?
|
||||||
private matrixRoom: MatrixRoom,
|
private matrixRoom: MatrixRoom,
|
||||||
|
parentLogger: Logger,
|
||||||
) {
|
) {
|
||||||
|
this.logger = parentLogger.createChildLogger("MatrixLivekitMerger");
|
||||||
connectionManager.setTansports(this.transports$);
|
connectionManager.setTansports(this.transports$);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +140,9 @@ export class MatrixLivekitMerger {
|
|||||||
private readonly membershipsWithTransport$ = this.scope.behavior(
|
private readonly membershipsWithTransport$ = this.scope.behavior(
|
||||||
this.memberships$.pipe(
|
this.memberships$.pipe(
|
||||||
map((memberships) => {
|
map((memberships) => {
|
||||||
const oldestMembership = this.matrixRTCSession.getOldestMembership();
|
|
||||||
return memberships.map((membership) => {
|
return memberships.map((membership) => {
|
||||||
const transport = membership.getTransport(
|
const oldestMembership = memberships[0] ?? membership;
|
||||||
oldestMembership ?? membership,
|
const transport = membership.getTransport(oldestMembership);
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
membership,
|
membership,
|
||||||
transport: isLivekitTransport(transport) ? transport : undefined,
|
transport: isLivekitTransport(transport) ? transport : undefined,
|
||||||
@@ -205,45 +197,65 @@ export class MatrixLivekitMerger {
|
|||||||
// Filters the livekit partic
|
// Filters the livekit partic
|
||||||
private participantsByMemberId$ = this.participantsWithConnection$.pipe(
|
private participantsByMemberId$ = this.participantsWithConnection$.pipe(
|
||||||
map((participantsWithConnections) => {
|
map((participantsWithConnections) => {
|
||||||
const participantsByMemberId = new Map<string, Participant[]>();
|
const participantsByMemberId = participantsWithConnections.reduce(
|
||||||
participantsWithConnections.forEach(({ participant, connection }) => {
|
(acc, test) => {
|
||||||
if (participant.getTrackPublications().length > 0) {
|
const { participant, connection } = test;
|
||||||
const currentVal = participantsByMemberId.get(participant.identity);
|
if (participant.getTrackPublications().length > 0) {
|
||||||
participantsByMemberId.set(participant.identity, {
|
const currentVal = acc.get(participant.identity);
|
||||||
connection,
|
if (!currentVal) {
|
||||||
participants:
|
acc.set(participant.identity, [{ connection, participant }]);
|
||||||
currentVal === undefined
|
} else {
|
||||||
? [participant]
|
// already known
|
||||||
: ([...currentVal, participant] as Participant[]),
|
// This is user is publishing on several SFUs
|
||||||
});
|
currentVal.push({ connection, participant });
|
||||||
}
|
this.logger.info(
|
||||||
});
|
`Participant ${participant.identity} is publishing on several SFUs ${currentVal.join()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
new Map() as ParticipantByMemberIdMap,
|
||||||
|
);
|
||||||
|
|
||||||
return participantsByMemberId;
|
return participantsByMemberId;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly matrixLivekitItems$ = this.scope
|
public readonly matrixLivekitItems$ = this.scope
|
||||||
.behavior<MatrixLivekitItem[]>(
|
.behavior<MatrixLivekitItem[]>(
|
||||||
this.allPublishingParticipants$.pipe(
|
combineLatest([
|
||||||
map((participants) => {
|
this.membershipsWithTransport$,
|
||||||
const matrixLivekitItems: MatrixLivekitItem[] = participants.map(
|
this.participantsByMemberId$,
|
||||||
({ participant, membership }) => ({
|
]).pipe(
|
||||||
participant,
|
map(([memberships, participantsByMemberId]) => {
|
||||||
|
const items = memberships.map(({ membership, transport }) => {
|
||||||
|
const participantsWithConnection = participantsByMemberId.get(
|
||||||
|
membership.membershipID,
|
||||||
|
);
|
||||||
|
const participant =
|
||||||
|
transport &&
|
||||||
|
participantsWithConnection?.find((p) =>
|
||||||
|
areLivekitTransportsEqual(p.connection.transport, transport),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
livekitParticipant: participant,
|
||||||
membership,
|
membership,
|
||||||
id: `${membership.userId}:${membership.deviceId}`,
|
|
||||||
// This makes sense to add the the js-sdk callMembership (we only need the avatar so probably the call memberhsip just should aquire the avatar)
|
// This makes sense to add the the js-sdk callMembership (we only need the avatar so probably the call memberhsip just should aquire the avatar)
|
||||||
member:
|
member:
|
||||||
getRoomMemberFromRtcMember(membership, this.matrixRoom)
|
// Why a member error? if we have a call membership there is a room member
|
||||||
?.member ?? memberError(),
|
getRoomMemberFromRtcMember(membership, this.matrixRoom)?.member,
|
||||||
}),
|
} as MatrixLivekitItem;
|
||||||
);
|
});
|
||||||
return matrixLivekitItems;
|
return items;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(startWith([]), pauseWhen(this.pretendToBeDisconnected$));
|
.pipe(startWith([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
|
||||||
|
|
||||||
// TODO add this to the JS-SDK
|
// TODO add this to the JS-SDK
|
||||||
function areLivekitTransportsEqual(
|
function areLivekitTransportsEqual(
|
||||||
t1: LivekitTransport,
|
t1: LivekitTransport,
|
||||||
|
|||||||
Reference in New Issue
Block a user