Refactor Matrix/LiveKit session merging

- Replace MatrixLivekitItem with MatrixLivekitMember, add displayName$
  and participantId, and use explicit LiveKit participant types
- Make sessionBehaviors$ accept a props object and return a typed
  RxRtcSession
- Update CallViewModel to use the new session behaviors, rebuild media
  items from matrixLivekitMembers, handle missing connections and use
  participantId-based keys
- Change localMembership/localTransport to accept Behavior-based
  options, read options.value for enterRTCSession, and fix advertised
  transport selection order
- Update tests and minor UI adjustments (settings modal livekitRooms
  stubbed) and fix JSON formatting in locales
This commit is contained in:
Timo K
2025-11-05 17:55:36 +01:00
parent 107ef16d94
commit 4d0de2fb71
10 changed files with 172 additions and 130 deletions

View File

@@ -74,16 +74,16 @@
"matrix_id": "Matrix ID: {{id}}", "matrix_id": "Matrix ID: {{id}}",
"matrixRTCMode": { "matrixRTCMode": {
"Comptibility": { "Comptibility": {
"label": "Compatibility: state events & multi SFU" "label": "Compatibility: state events & multi SFU",
"description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)", "description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)"
}, },
"Legacy": { "Legacy": {
"label": "Legacy: state events & oldest membership SFU" "label": "Legacy: state events & oldest membership SFU",
"description": "Compatible with old versions of EC that do not support multi SFU", "description": "Compatible with old versions of EC that do not support multi SFU"
}, },
"Matrix_2_0": { "Matrix_2_0": {
"label": "Matrix 2.0: sticky events & multi SFU" "label": "Matrix 2.0: sticky events & multi SFU",
"description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later", "description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later"
} }
}, },
"mute_all_audio": "Mute all audio (participants, reactions, join sounds)", "mute_all_audio": "Mute all audio (participants, reactions, join sounds)",

View File

@@ -138,7 +138,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
}, },
reactionsReader.raisedHands$, reactionsReader.raisedHands$,
reactionsReader.reactions$, reactionsReader.reactions$,
trackProcessorState$, scope.behavior(trackProcessorState$),
); );
setVm(vm); setVm(vm);
@@ -247,7 +247,7 @@ export const InCallView: FC<InCallViewProps> = ({
() => void toggleRaisedHand(), () => void toggleRaisedHand(),
); );
const allLivekitRooms = useBehavior(vm.allLivekitRooms$); // const allLivekitRooms = useBehavior(vm.allLivekitRooms$);
const audioParticipants = useBehavior(vm.audioParticipants$); const audioParticipants = useBehavior(vm.audioParticipants$);
const participantCount = useBehavior(vm.participantCount$); const participantCount = useBehavior(vm.participantCount$);
const reconnecting = useBehavior(vm.reconnecting$); const reconnecting = useBehavior(vm.reconnecting$);
@@ -841,7 +841,8 @@ export const InCallView: FC<InCallViewProps> = ({
onDismiss={closeSettings} onDismiss={closeSettings}
tab={settingsTab} tab={settingsTab}
onTabChange={setSettingsTab} onTabChange={setSettingsTab}
livekitRooms={allLivekitRooms} // TODO expose correct data to setttings modal
livekitRooms={[]}
/> />
</> </>
)} )}

View File

@@ -12,8 +12,9 @@ import EventEmitter from "events";
import { enterRTCSession } from "../src/rtcSessionHelpers"; import { enterRTCSession } from "../src/rtcSessionHelpers";
import { mockConfig } from "./utils/test"; import { mockConfig } from "./utils/test";
import { MatrixRTCMode } from "./settings/settings";
const USE_MUTI_SFU = false; const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("./UrlParams", () => ({ getUrlParams })); vi.mock("./UrlParams", () => ({ getUrlParams }));
@@ -94,8 +95,7 @@ test("It joins the correct Session", async () => {
}, },
{ {
encryptMedia: true, encryptMedia: true,
useMultiSfu: USE_MUTI_SFU, matrixRTCMode: MATRIX_RTC_MODE,
preferStickyEvents: false,
}, },
); );
@@ -153,8 +153,7 @@ test("It should not fail with configuration error if homeserver config has livek
}, },
{ {
encryptMedia: true, encryptMedia: true,
useMultiSfu: USE_MUTI_SFU, matrixRTCMode: MATRIX_RTC_MODE,
preferStickyEvents: false,
}, },
); );
}); });

View File

@@ -21,7 +21,6 @@ import {
RoomEvent, RoomEvent,
} from "matrix-js-sdk"; } from "matrix-js-sdk";
import { import {
BehaviorSubject,
combineLatest, combineLatest,
concat, concat,
distinctUntilChanged, distinctUntilChanged,
@@ -38,7 +37,6 @@ import {
of, of,
pairwise, pairwise,
race, race,
repeat,
scan, scan,
skip, skip,
skipWhile, skipWhile,
@@ -93,7 +91,6 @@ import {
import { shallowEquals } from "../utils/array"; import { shallowEquals } from "../utils/array";
import { type MediaDevices } from "./MediaDevices"; import { type MediaDevices } from "./MediaDevices";
import { type Behavior, constant } from "./Behavior"; import { type Behavior, constant } from "./Behavior";
import { enterRTCSession } from "../rtcSessionHelpers";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
import { type MuteStates } from "./MuteStates"; import { type MuteStates } from "./MuteStates";
@@ -112,12 +109,12 @@ import {
type SpotlightPortraitLayoutMedia, type SpotlightPortraitLayoutMedia,
} from "./layout-types.ts"; } from "./layout-types.ts";
import { type ElementCallError } from "../utils/errors.ts"; import { type ElementCallError } from "../utils/errors.ts";
import { ObservableScope } from "./ObservableScope.ts"; import { type ObservableScope } from "./ObservableScope.ts";
import { ConnectionManager } from "./remoteMembers/ConnectionManager.ts"; import { ConnectionManager } from "./remoteMembers/ConnectionManager.ts";
import { MatrixLivekitMerger } from "./remoteMembers/matrixLivekitMerger.ts"; import { MatrixLivekitMerger } from "./remoteMembers/matrixLivekitMerger.ts";
import { import {
localMembership$, localMembership$,
LocalMemberState, type LocalMemberState,
} from "./localMember/LocalMembership.ts"; } from "./localMember/LocalMembership.ts";
import { localTransport$ as computeLocalTransport$ } from "./localMember/LocalTransport.ts"; import { localTransport$ as computeLocalTransport$ } from "./localMember/LocalTransport.ts";
import { sessionBehaviors$ } from "./SessionBehaviors.ts"; import { sessionBehaviors$ } from "./SessionBehaviors.ts";
@@ -195,10 +192,10 @@ export class CallViewModel {
} }
: undefined; : undefined;
private sessionBehaviors = sessionBehaviors$( private sessionBehaviors = sessionBehaviors$({
this.scope, scope: this.scope,
this.matrixRTCSession, matrixRTCSession: this.matrixRTCSession,
); });
private memberships$ = this.sessionBehaviors.memberships$; private memberships$ = this.sessionBehaviors.memberships$;
private localTransport$ = computeLocalTransport$({ private localTransport$ = computeLocalTransport$({
@@ -211,6 +208,8 @@ export class CallViewModel {
), ),
}); });
// ------------------------------------------------------------------------
private connectionFactory = new ECConnectionFactory( private connectionFactory = new ECConnectionFactory(
this.matrixRoom.client, this.matrixRoom.client,
this.mediaDevices, this.mediaDevices,
@@ -219,10 +218,14 @@ export class CallViewModel {
getUrlParams().controlledAudioDevices, getUrlParams().controlledAudioDevices,
); );
// Can contain duplicates. The connection manager will take care of this.
private allTransports$ = this.scope.behavior( private allTransports$ = this.scope.behavior(
combineLatest( combineLatest(
[this.localTransport$, this.sessionBehaviors.transports$], [this.localTransport$, this.sessionBehaviors.transports$],
(l, t) => [...(l ? [l] : []), ...t], (localTransport, transports) => {
const localTransportAsArray = localTransport ? [localTransport] : [];
return [...localTransportAsArray, ...transports];
},
), ),
); );
@@ -232,6 +235,8 @@ export class CallViewModel {
this.allTransports$, this.allTransports$,
); );
// ------------------------------------------------------------------------
private matrixLivekitMerger = new MatrixLivekitMerger( private matrixLivekitMerger = new MatrixLivekitMerger(
this.scope, this.scope,
this.sessionBehaviors.membershipsWithTransport$, this.sessionBehaviors.membershipsWithTransport$,
@@ -240,7 +245,7 @@ export class CallViewModel {
this.userId, this.userId,
this.deviceId, this.deviceId,
); );
private matrixLivekitItems$ = this.matrixLivekitMerger.matrixLivekitItems$; private matrixLivekitMembers$ = this.matrixLivekitMerger.matrixLivekitMember$;
private localMembership = localMembership$({ private localMembership = localMembership$({
scope: this.scope, scope: this.scope,
@@ -297,12 +302,12 @@ export class CallViewModel {
// down, for example, and we want to avoid making people worry that the app is // down, for example, and we want to avoid making people worry that the app is
// in a split-brained state. // in a split-brained state.
// DISCUSSION own membership manager ALSO this probably can be simplifis // DISCUSSION own membership manager ALSO this probably can be simplifis
private readonly pretendToBeDisconnected$ = public reconnecting$ = this.localMembership.reconnecting$;
this.localMembership.reconnecting$; private readonly pretendToBeDisconnected$ = this.reconnecting$;
public readonly audioParticipants$ = this.scope.behavior( public readonly audioParticipants$ = this.scope.behavior(
this.matrixLivekitItems$.pipe( this.matrixLivekitMembers$.pipe(
map((items) => items.map((item) => item.participant)), map((members) => members.map((m) => m.participant)),
), ),
); );
@@ -330,72 +335,82 @@ export class CallViewModel {
// TODO KEEP THIS!! and adapt it to what our membershipManger returns // TODO KEEP THIS!! and adapt it to what our membershipManger returns
private readonly mediaItems$ = this.scope.behavior<MediaItem[]>( private readonly mediaItems$ = this.scope.behavior<MediaItem[]>(
generateKeyed$< generateKeyed$<
[typeof this.participantsByRoom$.value, number], [typeof this.matrixLivekitMembers$.value, number],
MediaItem, MediaItem,
MediaItem[] MediaItem[]
>( >(
// Generate a collection of MediaItems from the list of expected (whether // Generate a collection of MediaItems from the list of expected (whether
// present or missing) LiveKit participants. // present or missing) LiveKit participants.
combineLatest([this.participantsByRoom$, duplicateTiles.value$]), combineLatest([this.matrixLivekitMembers$, duplicateTiles.value$]),
([participantsByRoom, duplicateTiles], createOrGet) => { ([matrixLivekitMembers, duplicateTiles], createOrGet) => {
const items: MediaItem[] = []; const items: MediaItem[] = [];
for (const { livekitRoom, participants, url } of participantsByRoom) { for (const {
for (const { id, participant, member } of participants) { connection,
for (let i = 0; i < 1 + duplicateTiles; i++) { participant,
const mediaId = `${id}:${i}`; member,
const item = createOrGet( displayName$,
mediaId, participantId,
(scope) => } of matrixLivekitMembers) {
// We create UserMedia with or without a participant. if (connection === undefined) {
// This will be the initial value of a BehaviourSubject. logger.warn("connection is not yet initialised.");
// Once a participant appears we will update the BehaviourSubject. (see below) continue;
new UserMedia( }
scope, for (let i = 0; i < 1 + duplicateTiles; i++) {
mediaId, const mediaId = `${participantId}:${i}`;
member, const lkRoom = connection?.livekitRoom;
participant, const url = connection?.transport.livekit_service_url;
this.options.encryptionSystem, const dpName$ = displayName$.pipe(map((n) => n ?? "[👻]"));
livekitRoom, const item = createOrGet(
url, mediaId,
this.mediaDevices, (scope) =>
this.pretendToBeDisconnected$, // We create UserMedia with or without a participant.
this.memberDisplaynames$.pipe( // This will be the initial value of a BehaviourSubject.
map((m) => m.get(id) ?? "[👻]"), // Once a participant appears we will update the BehaviourSubject. (see below)
), new UserMedia(
this.handsRaised$.pipe(map((v) => v[id]?.time ?? null)), scope,
this.reactions$.pipe(map((v) => v[id] ?? undefined)), mediaId,
member,
participant,
this.options.encryptionSystem,
lkRoom,
url,
this.mediaDevices,
this.pretendToBeDisconnected$,
dpName$,
this.handsRaised$.pipe(
map((v) => v[participantId]?.time ?? null),
), ),
); this.reactions$.pipe(
items.push(item); map((v) => v[participantId] ?? undefined),
(item as UserMedia).updateParticipant(participant); ),
),
);
items.push(item);
(item as UserMedia).updateParticipant(participant);
if (participant?.isScreenShareEnabled) { if (participant?.isScreenShareEnabled) {
const screenShareId = `${mediaId}:screen-share`; const screenShareId = `${mediaId}:screen-share`;
items.push( items.push(
createOrGet( createOrGet(
screenShareId, screenShareId,
(scope) => (scope) =>
new ScreenShare( new ScreenShare(
scope, scope,
screenShareId, screenShareId,
member, member,
participant, participant,
this.options.encryptionSystem, this.options.encryptionSystem,
livekitRoom, lkRoom,
url, url,
this.pretendToBeDisconnected$, this.pretendToBeDisconnected$,
this.memberDisplaynames$.pipe( dpName$,
map((m) => m.get(id) ?? "[👻]"), ),
), ),
), );
),
);
}
} }
} }
} }
return items; return items;
}, },
), ),

View File

@@ -17,16 +17,29 @@ import { fromEvent, map } from "rxjs";
import { type ObservableScope } from "./ObservableScope"; import { type ObservableScope } from "./ObservableScope";
import { type Behavior } from "./Behavior"; import { type Behavior } from "./Behavior";
export const sessionBehaviors$ = ( interface Props {
scope: ObservableScope, scope: ObservableScope;
matrixRTCSession: MatrixRTCSession, matrixRTCSession: MatrixRTCSession;
): { }
/**
* Wraps behaviors that we extract from an matrixRTCSession.
*/
interface RxRtcSession {
/**
* some prop
*/
memberships$: Behavior<CallMembership[]>; memberships$: Behavior<CallMembership[]>;
membershipsWithTransport$: Behavior< membershipsWithTransport$: Behavior<
{ membership: CallMembership; transport?: LivekitTransport }[] { membership: CallMembership; transport?: LivekitTransport }[]
>; >;
transports$: Behavior<LivekitTransport[]>; transports$: Behavior<LivekitTransport[]>;
} => { }
export const sessionBehaviors$ = ({
scope,
matrixRTCSession,
}: Props): RxRtcSession => {
const memberships$ = scope.behavior( const memberships$ = scope.behavior(
fromEvent( fromEvent(
matrixRTCSession, matrixRTCSession,

View File

@@ -40,9 +40,8 @@ import {
enterRTCSession, enterRTCSession,
type EnterRTCSessionOptions, type EnterRTCSessionOptions,
} from "../../rtcSessionHelpers"; } from "../../rtcSessionHelpers";
import { ElementCallError } from "../../utils/errors"; import { type ElementCallError } from "../../utils/errors";
import { Widget } from "matrix-widget-api"; import { ElementWidgetActions, type WidgetHelpers } from "../../widget";
import { ElementWidgetActions, WidgetHelpers } from "../../widget";
enum LivekitState { enum LivekitState {
UNINITIALIZED = "uninitialized", UNINITIALIZED = "uninitialized",
@@ -87,6 +86,7 @@ export interface LocalMemberState {
* - send join state/sticky event * - send join state/sticky event
*/ */
interface Props { interface Props {
options: Behavior<EnterRTCSessionOptions>;
scope: ObservableScope; scope: ObservableScope;
mediaDevices: MediaDevices; mediaDevices: MediaDevices;
muteStates: MuteStates; muteStates: MuteStates;
@@ -113,6 +113,7 @@ interface Props {
*/ */
export const localMembership$ = ({ export const localMembership$ = ({
scope, scope,
options,
muteStates, muteStates,
mediaDevices, mediaDevices,
connectionManager, connectionManager,
@@ -124,7 +125,7 @@ export const localMembership$ = ({
widget, widget,
}: Props): { }: Props): {
// publisher: Publisher // publisher: Publisher
requestConnect: (options: EnterRTCSessionOptions) => LocalMemberState; requestConnect: () => LocalMemberState;
startTracks: () => Behavior<LocalTrack[]>; startTracks: () => Behavior<LocalTrack[]>;
requestDisconnect: () => Observable<LocalMemberLivekitState> | null; requestDisconnect: () => Observable<LocalMemberLivekitState> | null;
state: LocalMemberState; // TODO this is probably superseeded by joinState$ state: LocalMemberState; // TODO this is probably superseeded by joinState$
@@ -268,9 +269,7 @@ export const localMembership$ = ({
return tracks$; return tracks$;
}; };
const requestConnect = ( const requestConnect = (): LocalMemberState => {
options: EnterRTCSessionOptions,
): LocalMemberState => {
if (state.livekit$.value === null) { if (state.livekit$.value === null) {
startTracks(); startTracks();
state.livekit$.next({ state: LivekitState.CONNECTING }); state.livekit$.next({ state: LivekitState.CONNECTING });
@@ -290,7 +289,7 @@ export const localMembership$ = ({
localTransport$.pipe( localTransport$.pipe(
tap((transport) => { tap((transport) => {
if (transport !== undefined) { if (transport !== undefined) {
enterRTCSession(matrixRTCSession, transport, options).catch( enterRTCSession(matrixRTCSession, transport, options.value).catch(
(error) => { (error) => {
logger.error(error); logger.error(error);
}, },
@@ -379,7 +378,7 @@ export const localMembership$ = ({
if (advertised !== null && advertised !== undefined) { if (advertised !== null && advertised !== undefined) {
try { try {
configError$.next(null); configError$.next(null);
await enterRTCSession(matrixRTCSession, advertised, options); await enterRTCSession(matrixRTCSession, advertised, options.value);
} catch (e) { } catch (e) {
logger.error("Error entering RTC session", e); logger.error("Error entering RTC session", e);
} }

View File

@@ -77,13 +77,12 @@ export const localTransport$ = ({
scope.behavior(from(makeTransport(client, roomId)), undefined); scope.behavior(from(makeTransport(client, roomId)), undefined);
/** /**
* The transport we should advertise in our MatrixRTC membership (plus whether * The transport we should advertise in our MatrixRTC membership.
* it is a multi-SFU transport and whether we should use sticky events).
*/ */
const advertisedTransport$ = scope.behavior( const advertisedTransport$ = scope.behavior(
combineLatest( combineLatest(
[useOldestMember$, preferredTransport$, oldestMemberTransport$], [useOldestMember$, oldestMemberTransport$, preferredTransport$],
(useOldestMember, preferredTransport, oldestMemberTransport) => (useOldestMember, oldestMemberTransport, preferredTransport) =>
useOldestMember ? oldestMemberTransport : preferredTransport, useOldestMember ? oldestMemberTransport : preferredTransport,
).pipe<LivekitTransport>(distinctUntilChanged(deepCompare)), ).pipe<LivekitTransport>(distinctUntilChanged(deepCompare)),
undefined, undefined,

View File

@@ -107,7 +107,7 @@ export class ConnectionManager {
private readonly connectionFactory: ConnectionFactory, private readonly connectionFactory: ConnectionFactory,
private readonly inputTransports$: Behavior<LivekitTransport[]>, private readonly inputTransports$: Behavior<LivekitTransport[]>,
) { ) {
// TODO logger: only construct one logger from the client and make it compatible via a EC specific singleton. // TODO logger: only construct one logger from the client and make it compatible via a EC specific sing
this.logger = logger.getChild("ConnectionManager"); this.logger = logger.getChild("ConnectionManager");
scope.onEnd(() => this.running$.next(false)); scope.onEnd(() => this.running$.next(false));
} }

View File

@@ -23,7 +23,7 @@ import { type Room as MatrixRoom } from "matrix-js-sdk";
import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils"; import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils";
import { import {
type MatrixLivekitItem, type MatrixLivekitMember,
MatrixLivekitMerger, MatrixLivekitMerger,
} from "./matrixLivekitMerger"; } from "./matrixLivekitMerger";
import { ObservableScope } from "../ObservableScope"; import { ObservableScope } from "../ObservableScope";
@@ -79,10 +79,12 @@ afterEach(() => {
test("should signal participant not yet connected to livekit", () => { test("should signal participant not yet connected to livekit", () => {
fakeMemberships$.next([aliceRtcMember]); fakeMemberships$.next([aliceRtcMember]);
let items: MatrixLivekitItem[] = []; let items: MatrixLivekitMember[] = [];
matrixLivekitMerger.matrixLivekitItems$.pipe(take(1)).subscribe((emitted) => { matrixLivekitMerger.matrixLivekitMember$
items = emitted; .pipe(take(1))
}); .subscribe((emitted) => {
items = emitted;
});
expect(items).toHaveLength(1); expect(items).toHaveLength(1);
const item = items[0]; const item = items[0];
@@ -112,10 +114,12 @@ test("should signal participant on a connection that is publishing", () => {
]); ]);
fakeManagerData$.next(managerData); fakeManagerData$.next(managerData);
let items: MatrixLivekitItem[] = []; let items: MatrixLivekitMember[] = [];
matrixLivekitMerger.matrixLivekitItems$.pipe(take(1)).subscribe((emitted) => { matrixLivekitMerger.matrixLivekitMember$
items = emitted; .pipe(take(1))
}); .subscribe((emitted) => {
items = emitted;
});
expect(items).toHaveLength(1); expect(items).toHaveLength(1);
const item = items[0]; const item = items[0];
@@ -136,7 +140,7 @@ test("should signal participant on a connection that is not publishing", () => {
managerData.add(fakeConnection, []); managerData.add(fakeConnection, []);
fakeManagerData$.next(managerData); fakeManagerData$.next(managerData);
matrixLivekitMerger.matrixLivekitItems$.pipe(take(1)).subscribe((items) => { matrixLivekitMerger.matrixLivekitMember$.pipe(take(1)).subscribe((items) => {
expect(items).toHaveLength(1); expect(items).toHaveLength(1);
const item = items[0]; const item = items[0];
@@ -177,8 +181,8 @@ describe("Publication edge case", () => {
); );
test("bob is publishing in several connections", () => { test("bob is publishing in several connections", () => {
let lastMatrixLkItems: MatrixLivekitItem[] = []; let lastMatrixLkItems: MatrixLivekitMember[] = [];
matrixLivekitMerger.matrixLivekitItems$.subscribe((items) => { matrixLivekitMerger.matrixLivekitMember$.subscribe((items) => {
lastMatrixLkItems = items; lastMatrixLkItems = items;
}); });
@@ -218,8 +222,8 @@ describe("Publication edge case", () => {
}); });
test("bob is publishing in the wrong connection", () => { test("bob is publishing in the wrong connection", () => {
let lastMatrixLkItems: MatrixLivekitItem[] = []; let lastMatrixLkItems: MatrixLivekitMember[] = [];
matrixLivekitMerger.matrixLivekitItems$.subscribe((items) => { matrixLivekitMerger.matrixLivekitMember$.subscribe((items) => {
lastMatrixLkItems = items; lastMatrixLkItems = items;
}); });

View File

@@ -5,7 +5,10 @@ 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.
*/ */
import { type Participant as LivekitParticipant } from "livekit-client"; import {
type LocalParticipant as LocalLivekitParticipant,
type RemoteParticipant as RemoteLivekitParticipant,
} from "livekit-client";
import { import {
type LivekitTransport, type LivekitTransport,
type CallMembership, type CallMembership,
@@ -27,22 +30,23 @@ import { type Connection } from "./Connection";
* `livekitParticipant` can be undefined if the member is not yet connected to the livekit room * `livekitParticipant` can be undefined if the member is not yet connected to the livekit room
* or if it has no livekit transport at all. * or if it has no livekit transport at all.
*/ */
export interface MatrixLivekitItem { export interface MatrixLivekitMember {
membership: CallMembership; membership: CallMembership;
displayName: string; displayName$: Behavior<string>;
participant?: LivekitParticipant; participant?: LocalLivekitParticipant | RemoteLivekitParticipant;
connection?: Connection; connection?: Connection;
/** /**
* TODO Try to remove this! Its waaay to much information. * TODO Try to remove this! Its waaay to much information.
* Just get the member's avatar * Just get the member's avatar
* @deprecated * @deprecated
*/ */
member?: RoomMember; member: RoomMember;
mxcAvatarUrl?: string; mxcAvatarUrl?: string;
participantId: string;
} }
// Alternative structure idea: // Alternative structure idea:
// const livekitMatrixItems$ = (callMemberships$,connectionManager,scope): Observable<MatrixLivekitItem[]> => { // const livekitMatrixMember$ = (callMemberships$,connectionManager,scope): Observable<MatrixLivekitMember[]> => {
/** /**
* Combines MatrixRtc and Livekit worlds. * Combines MatrixRtc and Livekit worlds.
@@ -52,13 +56,13 @@ export interface MatrixLivekitItem {
* - an observable of CallMembership[] to track the call members (The matrix side) * - an observable of CallMembership[] to track the call members (The matrix side)
* - a `ConnectionManager` for the lk rooms (The livekit side) * - a `ConnectionManager` for the lk rooms (The livekit side)
* - out (via public Observable): * - out (via public Observable):
* - `remoteMatrixLivekitItems` an observable of MatrixLivekitItem[] to track the remote members and associated livekit data. * - `remoteMatrixLivekitMember` an observable of MatrixLivekitMember[] to track the remote members and associated livekit data.
*/ */
export class MatrixLivekitMerger { export class MatrixLivekitMerger {
/** /**
* Stream of all the call members and their associated livekit data (if available). * Stream of all the call members and their associated livekit data (if available).
*/ */
public matrixLivekitItems$: Behavior<MatrixLivekitItem[]>; public matrixLivekitMember$: Behavior<MatrixLivekitMember[]>;
// private readonly logger: Logger; // private readonly logger: Logger;
@@ -79,7 +83,7 @@ export class MatrixLivekitMerger {
) { ) {
// this.logger = parentLogger.getChild("MatrixLivekitMerger"); // this.logger = parentLogger.getChild("MatrixLivekitMerger");
this.matrixLivekitItems$ = this.scope.behavior( this.matrixLivekitMember$ = this.scope.behavior(
this.start$().pipe(startWith([])), this.start$().pipe(startWith([])),
); );
} }
@@ -87,7 +91,7 @@ export class MatrixLivekitMerger {
// ======================================= // =======================================
/// PRIVATES /// PRIVATES
// ======================================= // =======================================
private start$(): Observable<MatrixLivekitItem[]> { private start$(): Observable<MatrixLivekitMember[]> {
const displaynameMap$ = memberDisplaynames$( const displaynameMap$ = memberDisplaynames$(
this.scope, this.scope,
this.matrixRoom, this.matrixRoom,
@@ -102,10 +106,9 @@ export class MatrixLivekitMerger {
return combineLatest([ return combineLatest([
membershipsWithTransport$, membershipsWithTransport$,
this.connectionManager.connectionManagerData$, this.connectionManager.connectionManagerData$,
displaynameMap$,
]).pipe( ]).pipe(
map(([memberships, managerData, displayNameMap]) => { map(([memberships, managerData]) => {
const items: MatrixLivekitItem[] = memberships.map( const items: MatrixLivekitMember[] = memberships.map(
({ membership, transport }) => { ({ membership, transport }) => {
// TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to // TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to
const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`; const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`;
@@ -123,14 +126,23 @@ export class MatrixLivekitMerger {
const connection = transport const connection = transport
? managerData.getConnectionForTransport(transport) ? managerData.getConnectionForTransport(transport)
: undefined; : undefined;
const displayName$ = this.scope.behavior(
displaynameMap$.pipe(
map(
(displayNameMap) =>
displayNameMap.get(membership.membershipID) ?? "---",
),
),
);
return { return {
participant, participant,
membership, membership,
connection, 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) // 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,
displayName: displayNameMap.get(membership.membershipID) ?? "---", displayName$,
mxcAvatarUrl: member?.getMxcAvatarUrl(), mxcAvatarUrl: member?.getMxcAvatarUrl(),
participantId,
}; };
}, },
); );