From f260755f46880f1bc8d06bd266b7c7c529895cbe Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 12 Oct 2026 10:11:54 +0200 Subject: [PATCH] Group livekitRoomItems$ by connection The old reducer keyed items by service URL and mutated the accumulator's entries in place, so a transport without a service URL could not be represented and items were not safe to compare. Group members by their Connection object instead and carry the whole transport on the item, which the audio renderer now receives in place of a URL. --- sdk/main.ts | 2 +- src/livekit/MatrixAudioRenderer.test.tsx | 7 +-- src/livekit/MatrixAudioRenderer.tsx | 10 ++-- src/room/InCallView.tsx | 6 +-- src/state/CallViewModel/CallViewModel.ts | 66 +++++++++++------------- 5 files changed, 42 insertions(+), 49 deletions(-) diff --git a/sdk/main.ts b/sdk/main.ts index 6cd37032d..3932d1190 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -231,7 +231,7 @@ export async function createMatrixRTCSdk( complete: () => { logger.info("Livekit room items subscription completed"); for (const item of callViewModel.livekitRoomItems$.value) { - logger.info("unregistering room item from room", item.url); + logger.info("unregistering room item from room", item.transport); item.livekitRoom.unregisterTextStreamHandler(TEXT_LK_TOPIC); } }, diff --git a/src/livekit/MatrixAudioRenderer.test.tsx b/src/livekit/MatrixAudioRenderer.test.tsx index e4573cda9..21183f866 100644 --- a/src/livekit/MatrixAudioRenderer.test.tsx +++ b/src/livekit/MatrixAudioRenderer.test.tsx @@ -24,6 +24,7 @@ import { testAudioContext } from "../useAudioContext.test"; import * as MediaDevicesContext from "../MediaDevicesContext"; import { LivekitRoomAudioRenderer } from "./MatrixAudioRenderer"; import { + exampleTransport, mockLocalParticipant, mockMediaDevices, mockRemoteParticipant, @@ -118,7 +119,7 @@ function renderTestComponent( p.identity)} livekitRoom={livekitRoom} - url={""} + transport={exampleTransport} /> , ); @@ -150,7 +151,7 @@ it("should not render or warn for the local participant", () => { , ); @@ -177,7 +178,7 @@ it("should warn only once per unexpected participant", () => { , ); diff --git a/src/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx index 95d68c056..c9c4bbb36 100644 --- a/src/livekit/MatrixAudioRenderer.tsx +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -19,12 +19,10 @@ import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { useEarpieceAudioConfig } from "../MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; import * as controls from "../controls"; +import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; export interface MatrixAudioRendererProps { - /** - * The service URL of the LiveKit room. - */ - url: string; + transport: LivekitTransport; livekitRoom: LivekitRoom; /** * The list of participant identities to render audio for. @@ -54,7 +52,7 @@ export interface MatrixAudioRendererProps { * @public */ export function LivekitRoomAudioRenderer({ - url, + transport, livekitRoom, validIdentities, muted, @@ -88,7 +86,7 @@ export function LivekitRoomAudioRenderer({ warnedIdentities.current.add(identity); // Log that there is an invalid identity, that means that someone is publishing audio that is not expected to be in the call. logger.warn( - `Audio track ${identity} from ${url} has no matching matrix call member`, + `Audio track ${identity} from ${JSON.stringify(transport)} has no matching matrix call member`, `current members: ${validIdentities.join()}`, `track will not get rendered`, ); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b8d7ab89d..51c916f92 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -665,10 +665,10 @@ export const InCallView: FC = ({ onPointerOut={onPointerOut} > {header} - {audioParticipants.map(({ livekitRoom, url, participants }) => ( + {audioParticipants.map(({ livekitRoom, transport, participants }) => ( d.value)), ); - const livekitRoomItems$ = scope.behavior( - remoteMatrixLivekitMembers$.pipe( - switchMap((members) => { - const a$ = combineLatest( - members.value.map((member) => - combineLatest([member.connection$, member.participant.value$]).pipe( - map(([connection, participant]) => { - // do not render audio for local participant - if (!connection || !participant || participant.isLocal) - return null; - const livekitRoom = connection.livekitRoom; - const url = connection.transport.livekit_service_url; - return { - url, - livekitRoom, - participant: participant.identity, - }; - }), + const livekitRoomItems$ = scope.behavior( + remoteMatrixLivekitMembers$.pipe( + // Flatten the members to a snapshot of their current connections and + // participant objects + switchMap(({ value: members }) => { + if (members.length === 0) return of([]); + return combineLatest( + members.map((m) => + combineLatest( + [m.connection$, m.participant.value$], + (connection, participant) => ({ connection, participant }), ), ), ); - return a$; }), + // Keep only those with a defined connection and participant map((members) => - members.reduce((acc, curr) => { - if (!curr) return acc; - - const existing = acc.find((item) => item.url === curr.url); - if (existing) { - existing.participants.push(curr.participant); - } else { - acc.push({ - livekitRoom: curr.livekitRoom, - participants: [curr.participant], - url: curr.url, - }); - } - return acc; - }, []), + members.filter( + ( + m, + ): m is { connection: Connection; participant: RemoteParticipant } => + m.connection !== null && m.participant !== null, + ), + ), + // Group members by connection + map((members) => + [...Map.groupBy(members, (m) => m.connection)].map( + ([connection, members]) => ({ + transport: connection.transport, + livekitRoom: connection.livekitRoom, + participants: members.map((m) => m.participant.identity), + }), + ), ), ), - [], ); const handsRaised$ = scope.behavior(