2025-10-28 21:18:47 +01:00
|
|
|
/*
|
2025-10-29 15:20:06 +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.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-11-05 17:55:36 +01:00
|
|
|
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";
|
2025-10-30 01:13:06 +01:00
|
|
|
// eslint-disable-next-line rxjs/no-internal
|
2025-11-04 20:24:15 +01:00
|
|
|
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";
|
2025-10-30 01:13:06 +01:00
|
|
|
import { getRoomMemberFromRtcMember, memberDisplaynames$ } from "./displayname";
|
|
|
|
|
import { type Connection } from "./Connection";
|
2025-10-29 12:37:14 +01:00
|
|
|
|
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.
|
|
|
|
|
*/
|
2025-11-05 17:55:36 +01:00
|
|
|
export interface MatrixLivekitMember {
|
2025-10-28 21:58:10 +01:00
|
|
|
membership: CallMembership;
|
2025-11-05 17:55:36 +01:00
|
|
|
displayName$: Behavior<string>;
|
|
|
|
|
participant?: LocalLivekitParticipant | RemoteLivekitParticipant;
|
2025-10-30 01:13:06 +01:00
|
|
|
connection?: Connection;
|
|
|
|
|
/**
|
|
|
|
|
* TODO Try to remove this! Its waaay to much information.
|
|
|
|
|
* Just get the member's avatar
|
|
|
|
|
* @deprecated
|
|
|
|
|
*/
|
2025-11-05 17:55:36 +01:00
|
|
|
member: RoomMember;
|
2025-10-30 01:13:06 +01:00
|
|
|
mxcAvatarUrl?: string;
|
2025-11-05 17:55:36 +01:00
|
|
|
participantId: string;
|
2025-10-28 21:18:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Alternative structure idea:
|
2025-11-05 17:55:36 +01:00
|
|
|
// 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):
|
2025-11-05 17:55:36 +01:00
|
|
|
* - `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).
|
|
|
|
|
*/
|
2025-11-05 17:55:36 +01:00
|
|
|
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(
|
2025-10-30 01:13:06 +01:00
|
|
|
private scope: ObservableScope,
|
2025-11-04 20:24:15 +01:00
|
|
|
private membershipsWithTransport$: Behavior<
|
|
|
|
|
{ membership: CallMembership; transport?: LivekitTransport }[]
|
|
|
|
|
>,
|
2025-10-28 21:18:47 +01:00
|
|
|
private connectionManager: ConnectionManager,
|
2025-10-29 15:20:06 +01:00
|
|
|
// 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?
|
2025-10-30 01:13:06 +01:00
|
|
|
// Better with just `getMember`
|
2025-11-03 13:18:21 +01:00
|
|
|
private matrixRoom: Pick<MatrixRoom, "getMember"> & NodeStyleEventEmitter,
|
2025-10-30 01:13:06 +01:00
|
|
|
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");
|
|
|
|
|
|
2025-11-05 17:55:36 +01:00
|
|
|
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
|
|
|
|
|
// =======================================
|
2025-11-05 17:55:36 +01:00
|
|
|
private start$(): Observable<MatrixLivekitMember[]> {
|
2025-10-30 01:13:06 +01:00
|
|
|
const displaynameMap$ = memberDisplaynames$(
|
|
|
|
|
this.scope,
|
|
|
|
|
this.matrixRoom,
|
2025-11-04 20:24:15 +01:00
|
|
|
this.membershipsWithTransport$.pipe(
|
|
|
|
|
map((v) => v.map((v) => v.membership)),
|
|
|
|
|
),
|
2025-10-30 01:13:06 +01:00
|
|
|
this.userId,
|
|
|
|
|
this.deviceId,
|
|
|
|
|
);
|
2025-11-04 20:24:15 +01:00
|
|
|
const membershipsWithTransport$ = this.membershipsWithTransport$;
|
2025-10-29 18:31:58 +01:00
|
|
|
|
|
|
|
|
return combineLatest([
|
|
|
|
|
membershipsWithTransport$,
|
2025-11-03 13:18:21 +01:00
|
|
|
this.connectionManager.connectionManagerData$,
|
2025-10-29 18:31:58 +01:00
|
|
|
]).pipe(
|
2025-11-05 17:55:36 +01:00
|
|
|
map(([memberships, managerData]) => {
|
|
|
|
|
const items: MatrixLivekitMember[] = memberships.map(
|
2025-10-30 01:13:06 +01:00
|
|
|
({ membership, transport }) => {
|
2025-11-03 13:18:21 +01:00
|
|
|
// 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
|
|
|
);
|
2025-10-30 01:13:06 +01:00
|
|
|
const member = getRoomMemberFromRtcMember(
|
|
|
|
|
membership,
|
|
|
|
|
this.matrixRoom,
|
|
|
|
|
)?.member;
|
2025-11-03 13:18:21 +01:00
|
|
|
const connection = transport
|
|
|
|
|
? managerData.getConnectionForTransport(transport)
|
|
|
|
|
: undefined;
|
2025-11-05 17:55:36 +01:00
|
|
|
const displayName$ = this.scope.behavior(
|
|
|
|
|
displaynameMap$.pipe(
|
|
|
|
|
map(
|
|
|
|
|
(displayNameMap) =>
|
|
|
|
|
displayNameMap.get(membership.membershipID) ?? "---",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-10-30 01:13:06 +01:00
|
|
|
return {
|
2025-11-03 13:18:21 +01:00
|
|
|
participant,
|
2025-10-30 01:13:06 +01:00
|
|
|
membership,
|
2025-11-03 13:18:21 +01:00
|
|
|
connection,
|
2025-10-30 01:13:06 +01:00
|
|
|
// 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,
|
2025-11-05 17:55:36 +01:00
|
|
|
displayName$,
|
2025-10-30 01:13:06 +01:00
|
|
|
mxcAvatarUrl: member?.getMxcAvatarUrl(),
|
2025-11-05 17:55:36 +01:00
|
|
|
participantId,
|
2025-10-30 01:13:06 +01:00
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
);
|
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
|
|
|
}
|
2025-10-29 12:37:14 +01:00
|
|
|
|
2025-10-29 15:20:06 +01:00
|
|
|
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
|
|
|
|
|
|
2025-10-29 12:37:14 +01:00
|
|
|
// TODO add this to the JS-SDK
|
2025-10-29 18:31:58 +01:00
|
|
|
export function areLivekitTransportsEqual(
|
2025-10-29 12:37:14 +01:00
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|