Fix resource leaks when we stop using a connection

The execution of certain Observables related to a local or remote connection would continue even after we stopped caring about said connection because we were failing to give these state holders a proper ObservableScope of their own, separate from the CallViewModel's longer-lived scope. With this commit they now have scopes managed by generateKeyed$.
This commit is contained in:
Robin
2025-10-16 15:52:56 -04:00
parent 717c7420f9
commit d5efba285b
3 changed files with 66 additions and 70 deletions

View File

@@ -116,11 +116,7 @@ import {
} from "../rtcSessionHelpers"; } from "../rtcSessionHelpers";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
import { import { type Connection, RemoteConnection } from "./Connection";
type Connection,
type ConnectionOpts,
RemoteConnection,
} from "./Connection";
import { type MuteStates } from "./MuteStates"; import { type MuteStates } from "./MuteStates";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
import { type ProcessorState } from "../livekit/TrackProcessorContext"; import { type ProcessorState } from "../livekit/TrackProcessorContext";
@@ -369,26 +365,36 @@ export class CallViewModel {
*/ */
private readonly localConnection$: Behavior<Async<PublishConnection> | null> = private readonly localConnection$: Behavior<Async<PublishConnection> | null> =
this.scope.behavior( this.scope.behavior(
this.localTransport$.pipe( generateKeyed$<
map( Async<LivekitTransport> | null,
(transport) => PublishConnection,
transport && Async<PublishConnection> | null
mapAsync(transport, (transport) => { >(
const opts: ConnectionOpts = { this.localTransport$,
transport, (transport, createOrGet) =>
client: this.matrixRTCSession.room.client, transport &&
scope: this.scope, mapAsync(transport, (transport) =>
remoteTransports$: this.remoteTransports$, createOrGet(
}; // Stable key that uniquely idenifies the transport
return new PublishConnection( JSON.stringify({
opts, url: transport.livekit_service_url,
this.mediaDevices, alias: transport.livekit_alias,
this.muteStates, }),
this.e2eeLivekitOptions(), (scope) =>
this.scope.behavior(this.trackProcessorState$), new PublishConnection(
); {
}), transport,
), client: this.matrixRoom.client,
scope,
remoteTransports$: this.remoteTransports$,
},
this.mediaDevices,
this.muteStates,
this.e2eeLivekitOptions(),
this.scope.behavior(this.trackProcessorState$),
),
),
),
), ),
); );
@@ -415,61 +421,47 @@ export class CallViewModel {
* is *distinct* from the local transport. * is *distinct* from the local transport.
*/ */
private readonly remoteConnections$ = this.scope.behavior( private readonly remoteConnections$ = this.scope.behavior(
this.transports$.pipe( generateKeyed$<typeof this.transports$.value, Connection, Connection[]>(
accumulate(new Map<string, Connection>(), (prev, transports) => { this.transports$,
const next = new Map<string, Connection>(); (transports, createOrGet) => {
const connections: Connection[] = [];
// Until the local transport becomes ready we have no idea which // Until the local transport becomes ready we have no idea which
// transports will actually need a dedicated remote connection // transports will actually need a dedicated remote connection
if (transports?.local.state === "ready") { if (transports?.local.state === "ready") {
const oldestMembership = this.matrixRTCSession.getOldestMembership(); // TODO: Handle custom transport.livekit_alias values here
const localServiceUrl = transports.local.value.livekit_service_url; const localServiceUrl = transports.local.value.livekit_service_url;
const remoteServiceUrls = new Set( const remoteServiceUrls = new Set(
transports.remote.flatMap(({ membership, transport }) => { transports.remote.map(
const t = membership.getTransport(oldestMembership ?? membership); ({ transport }) => transport.livekit_service_url,
return t && ),
isLivekitTransport(t) &&
t.livekit_service_url !== localServiceUrl
? [t.livekit_service_url]
: [];
}),
); );
remoteServiceUrls.delete(localServiceUrl);
for (const remoteServiceUrl of remoteServiceUrls) { for (const remoteServiceUrl of remoteServiceUrls)
let nextConnection = prev.get(remoteServiceUrl); connections.push(
if (!nextConnection) { createOrGet(
logger.log(
"SFU remoteConnections$ construct new connection: ",
remoteServiceUrl, remoteServiceUrl,
); (scope) =>
new RemoteConnection(
const args: ConnectionOpts = { {
transport: { transport: {
type: "livekit", type: "livekit",
livekit_service_url: remoteServiceUrl, livekit_service_url: remoteServiceUrl,
livekit_alias: this.livekitAlias, livekit_alias: this.livekitAlias,
}, },
client: this.matrixRTCSession.room.client, client: this.matrixRoom.client,
scope: this.scope, scope,
remoteTransports$: this.remoteTransports$, remoteTransports$: this.remoteTransports$,
}; },
nextConnection = new RemoteConnection( this.e2eeLivekitOptions(),
args, ),
this.e2eeLivekitOptions(), ),
); );
} else {
logger.log(
"SFU remoteConnections$ use prev connection: ",
remoteServiceUrl,
);
}
next.set(remoteServiceUrl, nextConnection);
}
} }
return next; return connections;
}), },
map((transports) => [...transports.values()]),
), ),
); );

View File

@@ -21,6 +21,7 @@ import {
type CallMembership, type CallMembership,
type LivekitTransport, type LivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { logger } from "matrix-js-sdk/lib/logger";
import { BehaviorSubject, combineLatest, type Observable } from "rxjs"; import { BehaviorSubject, combineLatest, type Observable } from "rxjs";
import { import {
@@ -218,6 +219,9 @@ export class Connection {
public readonly livekitRoom: LivekitRoom, public readonly livekitRoom: LivekitRoom,
opts: ConnectionOpts, opts: ConnectionOpts,
) { ) {
logger.log(
`[Connection] Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`,
);
const { transport, client, scope, remoteTransports$ } = opts; const { transport, client, scope, remoteTransports$ } = opts;
this.transport = transport; this.transport = transport;

View File

@@ -58,7 +58,7 @@ export class PublishConnection extends Connection {
trackerProcessorState$: Behavior<ProcessorState>, trackerProcessorState$: Behavior<ProcessorState>,
) { ) {
const { scope } = args; const { scope } = args;
logger.info("[LivekitRoom] Create LiveKit room"); logger.info("[PublishConnection] Create LiveKit room");
const { controlledAudioDevices } = getUrlParams(); const { controlledAudioDevices } = getUrlParams();
const factory = const factory =