Files
element-call/src/state/remoteMembers/matrixLivekitMerger.ts

169 lines
5.9 KiB
TypeScript
Raw Normal View History

2025-10-28 21:18:47 +01:00
/*
Copyright 2025 Element Creations Ltd.
2025-10-28 21:18:47 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
type LocalParticipant as LocalLivekitParticipant,
type RemoteParticipant as RemoteLivekitParticipant,
} from "livekit-client";
2025-10-28 21:18:47 +01:00
import {
2025-10-29 18:31:58 +01:00
type LivekitTransport,
type CallMembership,
2025-10-28 21:18:47 +01:00
} from "matrix-js-sdk/lib/matrixrtc";
2025-10-29 18:31:58 +01:00
import { combineLatest, map, startWith, type Observable } from "rxjs";
// eslint-disable-next-line rxjs/no-internal
import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
2025-10-28 21:18:47 +01:00
2025-10-29 18:31:58 +01:00
import type { Room as MatrixRoom, RoomMember } from "matrix-js-sdk";
// import type { Logger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../Behavior";
2025-10-28 21:18:47 +01:00
import { type ObservableScope } from "../ObservableScope";
2025-10-29 18:31:58 +01:00
import { type ConnectionManager } from "./ConnectionManager";
import { getRoomMemberFromRtcMember, memberDisplaynames$ } from "./displayname";
import { type Connection } from "./Connection";
2025-10-28 21:18:47 +01:00
/**
* Represent a matrix call member and his associated livekit participation.
* `livekitParticipant` can be undefined if the member is not yet connected to the livekit room
* or if it has no livekit transport at all.
*/
export interface MatrixLivekitMember {
2025-10-28 21:58:10 +01:00
membership: CallMembership;
displayName$: Behavior<string>;
participant?: LocalLivekitParticipant | RemoteLivekitParticipant;
connection?: Connection;
/**
* TODO Try to remove this! Its waaay to much information.
* Just get the member's avatar
* @deprecated
*/
member: RoomMember;
mxcAvatarUrl?: string;
participantId: string;
2025-10-28 21:18:47 +01:00
}
// Alternative structure idea:
// const livekitMatrixMember$ = (callMemberships$,connectionManager,scope): Observable<MatrixLivekitMember[]> => {
2025-10-28 21:18:47 +01:00
/**
* Combines MatrixRtc and Livekit worlds.
*
* It has a small public interface:
* - in (via constructor):
* - an observable of CallMembership[] to track the call members (The matrix side)
* - a `ConnectionManager` for the lk rooms (The livekit side)
* - out (via public Observable):
* - `remoteMatrixLivekitMember` an observable of MatrixLivekitMember[] to track the remote members and associated livekit data.
2025-10-28 21:18:47 +01:00
*/
export class MatrixLivekitMerger {
2025-10-29 18:31:58 +01:00
/**
* Stream of all the call members and their associated livekit data (if available).
*/
public matrixLivekitMember$: Behavior<MatrixLivekitMember[]>;
2025-10-29 18:31:58 +01:00
// private readonly logger: Logger;
2025-10-28 21:18:47 +01:00
public constructor(
private scope: ObservableScope,
private membershipsWithTransport$: Behavior<
{ membership: CallMembership; transport?: LivekitTransport }[]
>,
2025-10-28 21:18:47 +01:00
private connectionManager: ConnectionManager,
// 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?
// Better with just `getMember`
private matrixRoom: Pick<MatrixRoom, "getMember"> & NodeStyleEventEmitter,
private userId: string,
private deviceId: string,
2025-10-29 18:31:58 +01:00
// parentLogger: Logger,
2025-10-28 21:18:47 +01:00
) {
2025-10-29 18:31:58 +01:00
// this.logger = parentLogger.getChild("MatrixLivekitMerger");
this.matrixLivekitMember$ = this.scope.behavior(
2025-10-29 18:31:58 +01:00
this.start$().pipe(startWith([])),
);
2025-10-28 21:18:47 +01:00
}
2025-10-29 18:31:58 +01:00
// =======================================
/// PRIVATES
// =======================================
private start$(): Observable<MatrixLivekitMember[]> {
const displaynameMap$ = memberDisplaynames$(
this.scope,
this.matrixRoom,
this.membershipsWithTransport$.pipe(
map((v) => v.map((v) => v.membership)),
),
this.userId,
this.deviceId,
);
const membershipsWithTransport$ = this.membershipsWithTransport$;
2025-10-29 18:31:58 +01:00
return combineLatest([
membershipsWithTransport$,
this.connectionManager.connectionManagerData$,
2025-10-29 18:31:58 +01:00
]).pipe(
map(([memberships, managerData]) => {
const items: MatrixLivekitMember[] = memberships.map(
({ membership, transport }) => {
// TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to
const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`;
const participants = transport
? managerData.getParticipantForTransport(transport)
: [];
const participant = participants.find(
(p) => p.identity == participantId,
2025-10-29 18:31:58 +01:00
);
const member = getRoomMemberFromRtcMember(
membership,
this.matrixRoom,
)?.member;
const connection = transport
? managerData.getConnectionForTransport(transport)
: undefined;
const displayName$ = this.scope.behavior(
displaynameMap$.pipe(
map(
(displayNameMap) =>
displayNameMap.get(membership.membershipID) ?? "---",
),
),
);
return {
participant,
membership,
connection,
// 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,
displayName$,
mxcAvatarUrl: member?.getMxcAvatarUrl(),
participantId,
};
},
);
2025-10-29 18:31:58 +01:00
return items;
2025-10-28 21:18:47 +01:00
}),
2025-10-29 18:31:58 +01:00
);
}
2025-10-28 21:18:47 +01:00
}
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
// TODO add this to the JS-SDK
2025-10-29 18:31:58 +01:00
export function areLivekitTransportsEqual(
t1: LivekitTransport,
t2: LivekitTransport,
): boolean {
return (
t1.livekit_service_url === t2.livekit_service_url &&
// In case we have different lk rooms in the same SFU (depends on the livekit authorization service)
// It is only needed in case the livekit authorization service is not behaving as expected (or custom implementation)
t1.livekit_alias === t2.livekit_alias
);
}