From d8b9568400eb9267a2dc5f3efdff09cfec770831 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 8 Dec 2025 23:33:41 -0500 Subject: [PATCH 1/4] Stop publisher in a less brittle way --- .../CallViewModel/localMember/LocalMembership.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalMembership.ts b/src/state/CallViewModel/localMember/LocalMembership.ts index 60ae79b8..71261d37 100644 --- a/src/state/CallViewModel/localMember/LocalMembership.ts +++ b/src/state/CallViewModel/localMember/LocalMembership.ts @@ -323,12 +323,14 @@ export const createLocalMembership$ = ({ // - overwrite current publisher scope.reconcile(localConnection$, async (connection) => { if (connection !== null) { - publisher$.next(createPublisherFactory(connection)); + const publisher = createPublisherFactory(connection); + publisher$.next(publisher); + // Clean-up callback + return Promise.resolve(async (): Promise => { + await publisher.stopPublishing(); + publisher.stopTracks(); + }); } - return Promise.resolve(async (): Promise => { - await publisher$?.value?.stopPublishing(); - publisher$?.value?.stopTracks(); - }); }); // Use reconcile here to not run concurrent createAndSetupTracks calls From 9481dc401c6c06c32085560431552247a56ddb16 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 8 Dec 2025 23:34:42 -0500 Subject: [PATCH 2/4] Remove extraneous 'scope running' check Semantically, behaviors are only meaningful for as long as their scope is running. Setting a behavior's value to an empty array once its scope ends is not guaranteed to work (as it depends on execution order of how the scope is ended), and subscribers should be robust enough to handle clean-up of all connections at the end of the scope either way. --- .../CallViewModel/remoteMembers/ConnectionManager.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 0b9f939c..09a8e79f 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -10,7 +10,7 @@ import { type LivekitTransport, type ParticipantId, } from "matrix-js-sdk/lib/matrixrtc"; -import { BehaviorSubject, combineLatest, map, of, switchMap, tap } from "rxjs"; +import { combineLatest, map, of, switchMap, tap } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; @@ -115,9 +115,6 @@ export function createConnectionManager$({ logger: parentLogger, }: Props): IConnectionManager { const logger = parentLogger.getChild("[ConnectionManager]"); - - const running$ = new BehaviorSubject(true); - scope.onEnd(() => running$.next(false)); // TODO logger: only construct one logger from the client and make it compatible via a EC specific sing /** @@ -129,10 +126,7 @@ export function createConnectionManager$({ * externally this is modified via `registerTransports()`. */ const transports$ = scope.behavior( - combineLatest([running$, inputTransports$]).pipe( - map(([running, transports]) => - transports.mapInner((transport) => (running ? transport : [])), - ), + inputTransports$.pipe( map((transports) => transports.mapInner(removeDuplicateTransports)), tap(({ value: transports }) => { logger.trace( From 2f3f9f95eb6ed5961ff7769c246b0a29a97d181c Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 8 Dec 2025 23:38:15 -0500 Subject: [PATCH 3/4] Use more compact optional chaining and coalescing notation --- src/state/CallViewModel/remoteMembers/ConnectionManager.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 09a8e79f..755ba3dd 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -60,11 +60,7 @@ export class ConnectionManagerData { transport: LivekitTransport, ): (LocalParticipant | RemoteParticipant)[] { const key = transport.livekit_service_url + "|" + transport.livekit_alias; - const existing = this.store.get(key); - if (existing) { - return existing[1]; - } - return []; + return this.store.get(key)?.[1] ?? []; } /** * Get all connections where the given participant is publishing. From 6ee3ef27954d1148ad4e3d7854d84431fb6c349b Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 8 Dec 2025 23:38:54 -0500 Subject: [PATCH 4/4] Edit a misleading log line The factory function is called once per item to construct the item. It is not called on future updates to the item's data. --- src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 2f152630..79ad933c 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -108,7 +108,7 @@ export function createMatrixLivekitMembers$({ // Each update where the key of the generator array do not change will result in updates to the `data$` observable in the factory. (scope, data$, participantId, userId) => { logger.debug( - `Updating data$ for participantId: ${participantId}, userId: ${userId}`, + `Generating member for participantId: ${participantId}, userId: ${userId}`, ); // will only get called once per `participantId, userId` pair. // updates to data$ and as a result to displayName$ and mxcAvatarUrl$ are more frequent.