Clarify which Matrix-LiveKit members are remote

It was rather confusing that matrixLivekitMembers$ gives you objects of type RemoteMatrixLivekitMembers and yet the *local* member would often be among these. I've attempted to clear this up. To my knowledge this wasn't creating any bugs.
This commit is contained in:
Robin
2026-06-18 09:19:24 +02:00
parent 256219c4bf
commit e77d143ce1
5 changed files with 66 additions and 64 deletions

View File

@@ -76,9 +76,9 @@ interface MatrixRTCSdk {
stop: () => void; stop: () => void;
data$: Observable<{ rtcBackendIdentity: string; data: string }>; data$: Observable<{ rtcBackendIdentity: string; data: string }>;
/** /**
* flattened list of members * flattened list of remote members
*/ */
members$: Behavior< remoteMembers$: Behavior<
{ {
connection: Connection | null; connection: Connection | null;
membership: CallMembership; membership: CallMembership;
@@ -86,7 +86,7 @@ interface MatrixRTCSdk {
}[] }[]
>; >;
/** /**
* flattened local members * flattened local member
*/ */
localMember$: Behavior<{ localMember$: Behavior<{
connection: Connection | null; connection: Connection | null;
@@ -338,8 +338,8 @@ export async function createMatrixRTCSdk(
), ),
), ),
connected$: callViewModel.connected$, connected$: callViewModel.connected$,
members$: scope.behavior( remoteMembers$: scope.behavior(
callViewModel.matrixLivekitMembers$.pipe( callViewModel.remoteMatrixLivekitMembers$.pipe(
switchMap((members) => { switchMap((members) => {
const listOfMemberObservables = members.map((member) => const listOfMemberObservables = members.map((member) =>
combineLatest([ combineLatest([

View File

@@ -127,10 +127,9 @@ import {
createConnectionManager$, createConnectionManager$,
} from "./remoteMembers/ConnectionManager.ts"; } from "./remoteMembers/ConnectionManager.ts";
import { import {
createMatrixLivekitMembers$, createRemoteMatrixLivekitMembers$,
type LocalMatrixLivekitMember, type LocalMatrixLivekitMember,
type RemoteMatrixLivekitMember, type RemoteMatrixLivekitMember,
type MatrixLivekitMember,
} from "./remoteMembers/MatrixLivekitMembers.ts"; } from "./remoteMembers/MatrixLivekitMembers.ts";
import { import {
type AutoLeaveReason, type AutoLeaveReason,
@@ -302,7 +301,7 @@ export interface CallViewModel {
/** Participants sorted by livekit room so they can be used in the audio rendering */ /** Participants sorted by livekit room so they can be used in the audio rendering */
livekitRoomItems$: Behavior<LivekitRoomItem[]>; livekitRoomItems$: Behavior<LivekitRoomItem[]>;
/** use the layout instead, this is just for the sdk export. */ /** use the layout instead, this is just for the sdk export. */
matrixLivekitMembers$: Behavior<RemoteMatrixLivekitMember[]>; remoteMatrixLivekitMembers$: Behavior<RemoteMatrixLivekitMember[]>;
localMatrixLivekitMember$: Behavior<LocalMatrixLivekitMember | null>; localMatrixLivekitMember$: Behavior<LocalMatrixLivekitMember | null>;
/** List of participants raising their hand */ /** List of participants raising their hand */
handsRaised$: Behavior<Record<string, RaisedHandInfo>>; handsRaised$: Behavior<Record<string, RaisedHandInfo>>;
@@ -529,13 +528,15 @@ export function createCallViewModel$(
ownMembershipIdentity, ownMembershipIdentity,
}); });
const matrixLivekitMembers$: Behavior<Epoch<RemoteMatrixLivekitMember[]>> = const remoteMatrixLivekitMembers$: Behavior<
createMatrixLivekitMembers$({ Epoch<RemoteMatrixLivekitMember[]>
scope: scope, > = createRemoteMatrixLivekitMembers$({
membershipsWithTransport$: scope: scope,
membershipsAndTransports.membershipsWithTransport$, membershipsWithTransport$:
connectionManager: connectionManager, membershipsAndTransports.membershipsWithTransport$,
}); connectionManager: connectionManager,
localUser: { userId, deviceId },
});
const connectOptions$ = scope.behavior( const connectOptions$ = scope.behavior(
matrixRTCMode$.pipe( matrixRTCMode$.pipe(
@@ -612,6 +613,13 @@ export function createCallViewModel$(
), ),
); );
const matrixLivekitMembers$ = scope.behavior(
combineLatest(
[localMatrixLivekitMember$, remoteMatrixLivekitMembers$],
(local, remote) => [...(local === null ? [] : [local]), ...remote.value],
),
);
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// matrixMemberMetadataStore // matrixMemberMetadataStore
@@ -641,7 +649,7 @@ export function createCallViewModel$(
connectionManager.connectionManagerData$.pipe(map((d) => d.value)), connectionManager.connectionManagerData$.pipe(map((d) => d.value)),
); );
const livekitRoomItems$ = scope.behavior( const livekitRoomItems$ = scope.behavior(
matrixLivekitMembers$.pipe( remoteMatrixLivekitMembers$.pipe(
switchMap((members) => { switchMap((members) => {
const a$ = combineLatest( const a$ = combineLatest(
members.value.map((member) => members.value.map((member) =>
@@ -707,43 +715,20 @@ export function createCallViewModel$(
* List of user media (camera feeds) that we want tiles for. * List of user media (camera feeds) that we want tiles for.
*/ */
const userMedia$ = scope.behavior<WrappedUserMediaViewModel[]>( const userMedia$ = scope.behavior<WrappedUserMediaViewModel[]>(
combineLatest([ combineLatest([matrixLivekitMembers$, duplicateTiles.value$]).pipe(
localMatrixLivekitMember$,
matrixLivekitMembers$,
duplicateTiles.value$,
]).pipe(
// Generate a collection of user media from the list of expected (whether // Generate a collection of user media from the list of expected (whether
// present or missing) LiveKit participants. // present or missing) LiveKit participants.
generateItems( generateItems(
"CallViewModel userMedia$", "CallViewModel userMedia$",
function* ([ function* ([members, duplicateTiles]) {
localMatrixLivekitMember, for (const {
matrixLivekitMembers, userId,
duplicateTiles, participant,
]) { connection$,
const computeMediaId = (m: MatrixLivekitMember): string => membership$,
`${m.userId}:${m.membership$.value.deviceId}`; } of members) {
const rtcId = membership$.value.rtcBackendIdentity;
const localUserMediaId = localMatrixLivekitMember const mediaId = `${userId}:${membership$.value.deviceId}`;
? computeMediaId(localMatrixLivekitMember)
: undefined;
const localAsArray = localMatrixLivekitMember
? [localMatrixLivekitMember]
: [];
const remoteWithoutLocal = matrixLivekitMembers.value.filter(
(m) => computeMediaId(m) !== localUserMediaId,
);
const allMatrixLivekitMembers = [
...localAsArray,
...remoteWithoutLocal,
];
for (const matrixLivekitMember of allMatrixLivekitMembers) {
const { userId, participant, connection$, membership$ } =
matrixLivekitMember;
const rtcId = membership$.value.rtcBackendIdentity; // rtcBackendIdentity
const mediaId = computeMediaId(matrixLivekitMember);
for (let dup = 0; dup < 1 + duplicateTiles; dup++) { for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
yield { yield {
keys: [dup, mediaId, userId, participant, connection$, rtcId], keys: [dup, mediaId, userId, participant, connection$, rtcId],
@@ -861,7 +846,7 @@ export function createCallViewModel$(
* multiple devices. * multiple devices.
*/ */
const participantCount$ = scope.behavior( const participantCount$ = scope.behavior(
matrixLivekitMembers$.pipe(map((ms) => ms.value.length)), matrixLivekitMembers$.pipe(map((ms) => ms.length)),
); );
const leaveSoundEffect$ = userMedia$.pipe( const leaveSoundEffect$ = userMedia$.pipe(
@@ -1770,8 +1755,8 @@ export function createCallViewModel$(
setGridMode: setGridMode, setGridMode: setGridMode,
layout$: layout$, layout$: layout$,
localMatrixLivekitMember$, localMatrixLivekitMember$,
matrixLivekitMembers$: scope.behavior( remoteMatrixLivekitMembers$: scope.behavior(
matrixLivekitMembers$.pipe( remoteMatrixLivekitMembers$.pipe(
map((members) => members.value), map((members) => members.value),
tap((v) => { tap((v) => {
const listForLogs = v const listForLogs = v

View File

@@ -15,7 +15,7 @@ import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs";
import { type IConnectionManager } from "./ConnectionManager.ts"; import { type IConnectionManager } from "./ConnectionManager.ts";
import { import {
type RemoteMatrixLivekitMember, type RemoteMatrixLivekitMember,
createMatrixLivekitMembers$, createRemoteMatrixLivekitMembers$,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { import {
Epoch, Epoch,
@@ -31,6 +31,7 @@ import {
} from "../../../utils/test.ts"; } from "../../../utils/test.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
import { constant } from "../../Behavior.ts"; import { constant } from "../../Behavior.ts";
import { localRtcMember } from "../../../utils/test-fixtures.ts";
let testScope: ObservableScope; let testScope: ObservableScope;
@@ -88,12 +89,13 @@ test("should signal participant not yet connected to livekit", async () => {
mockConnectionManagerData$, mockConnectionManagerData$,
); );
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createRemoteMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: { connectionManager: {
connectionManagerData$: connectionManagerData$, connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager, } as unknown as IConnectionManager,
localUser: localRtcMember,
}); });
await flushPromises(); await flushPromises();
@@ -157,12 +159,13 @@ test("should signal participant on a connection that is publishing", async () =>
constant(dataWithPublisher), constant(dataWithPublisher),
); );
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createRemoteMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: { connectionManager: {
connectionManagerData$: connectionManagerData$, connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager, } as unknown as IConnectionManager,
localUser: localRtcMember,
}); });
await flushPromises(); await flushPromises();
@@ -197,12 +200,13 @@ test("should signal participant on a connection that is not publishing", async (
constant(dataWithPublisher), constant(dataWithPublisher),
); );
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createRemoteMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: { connectionManager: {
connectionManagerData$: connectionManagerData$, connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager, } as unknown as IConnectionManager,
localUser: localRtcMember,
}); });
await flushPromises(); await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy( expect(matrixLivekitMember$.value.value).toSatisfy(
@@ -245,12 +249,13 @@ describe("Publication edge case", () => {
constant(connectionWithPublisher), constant(connectionWithPublisher),
); );
const matrixLivekitMembers$ = createMatrixLivekitMembers$({ const matrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: { connectionManager: {
connectionManagerData$: connectionManagerData$, connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager, } as unknown as IConnectionManager,
localUser: localRtcMember,
}); });
await flushPromises(); await flushPromises();
expect(matrixLivekitMembers$.value.value).toSatisfy( expect(matrixLivekitMembers$.value.value).toSatisfy(
@@ -303,12 +308,13 @@ test("bob is publishing in the wrong connection", async () => {
connectionsWithPublisher$, connectionsWithPublisher$,
); );
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createRemoteMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$), membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: { connectionManager: {
connectionManagerData$: connectionManagerData$, connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager, } as unknown as IConnectionManager,
localUser: localRtcMember,
}); });
await flushPromises(); await flushPromises();

View File

@@ -62,7 +62,9 @@ interface Props {
Epoch<{ membership: CallMembership; transport?: LivekitTransportConfig }[]> Epoch<{ membership: CallMembership; transport?: LivekitTransportConfig }[]>
>; >;
connectionManager: IConnectionManager; connectionManager: IConnectionManager;
localUser: { deviceId: string; userId: string };
} }
/** /**
* Combines MatrixRTC and Livekit worlds. * Combines MatrixRTC and Livekit worlds.
* *
@@ -73,13 +75,14 @@ interface Props {
* - out (via public Observable): * - out (via public Observable):
* - `remoteMatrixLivekitMember` an observable of MatrixLivekitMember[] to track the remote members and associated livekit data. * - `remoteMatrixLivekitMember` an observable of MatrixLivekitMember[] to track the remote members and associated livekit data.
*/ */
export function createMatrixLivekitMembers$({ export function createRemoteMatrixLivekitMembers$({
scope, scope,
membershipsWithTransport$, membershipsWithTransport$,
connectionManager, connectionManager,
localUser,
}: Props): Behavior<Epoch<RemoteMatrixLivekitMember[]>> { }: Props): Behavior<Epoch<RemoteMatrixLivekitMember[]>> {
/** /**
* Stream of all the call members and their associated livekit data (if available). * Behavior of all the remote call members and their associated livekit data (if available).
*/ */
return scope.behavior( return scope.behavior(
combineLatest([ combineLatest([
@@ -91,12 +94,19 @@ export function createMatrixLivekitMembers$({
), ),
map(([ms, data]) => new Epoch([ms.value, data.value] as const, ms.epoch)), map(([ms, data]) => new Epoch([ms.value, data.value] as const, ms.epoch)),
generateItemsWithEpoch( generateItemsWithEpoch(
"MatrixLivekitMembers", "RemoteMatrixLivekitMembers",
// Generator function. // Generator function.
// creates an array of `{key, data}[]` // creates an array of `{key, data}[]`
// Each change in the keys (new key) will result in a call to the factory function. // Each change in the keys (new key) will result in a call to the factory function.
function* ([membershipsWithTransport, managerData]) { function* ([membershipsWithTransport, managerData]) {
for (const { membership, transport } of membershipsWithTransport) { for (const { membership, transport } of membershipsWithTransport) {
// Exclude the local membership
if (
membership.userId === localUser.userId &&
membership.deviceId === localUser.deviceId
)
continue;
const participants = transport const participants = transport
? managerData.getParticipantsForTransport(transport) ? managerData.getParticipantsForTransport(transport)
: []; : [];

View File

@@ -29,13 +29,13 @@ import {
import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { import {
areLivekitTransportsEqual, areLivekitTransportsEqual,
createMatrixLivekitMembers$, createRemoteMatrixLivekitMembers$,
type RemoteMatrixLivekitMember, type RemoteMatrixLivekitMember,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { createConnectionManager$ } from "./ConnectionManager.ts"; import { createConnectionManager$ } from "./ConnectionManager.ts";
import { membershipsAndTransports$ } from "../../SessionBehaviors.ts"; import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
import { constant } from "../../Behavior.ts"; import { constant } from "../../Behavior.ts";
import { testJWTToken } from "../../../utils/test-fixtures.ts"; import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts";
// Test the integration of ConnectionManager and MatrixLivekitMerger // Test the integration of ConnectionManager and MatrixLivekitMerger
@@ -130,11 +130,12 @@ test("bob, carl, then bob joining no tracks yet", () => {
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
}); });
const matrixLivekitMembers$ = createMatrixLivekitMembers$({ const matrixLivekitMembers$ = createRemoteMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: membershipsWithTransport$:
membershipsAndTransports.membershipsWithTransport$, membershipsAndTransports.membershipsWithTransport$,
connectionManager, connectionManager,
localUser: localRtcMember,
}); });
expectObservable(matrixLivekitMembers$).toBe(vMarble, { expectObservable(matrixLivekitMembers$).toBe(vMarble, {