fix(lotus): don't start two denoise processors on racing LocalTrackPublished
apply() guarded on mic.getProcessor(), which is only set after setProcessor() resolves (after the whole wasm/model load), so mic-published followed by camera-published constructed two processors — two AudioContexts, two model loads, double lock hold time. Track the in-flight processor per room, skip apply() while one is pending, and destroy a pending processor if the module is torn down before setProcessor resolves. Unit-tested. Fixes #10 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5
parent
936a083533
commit
59e0c852ee
@@ -97,13 +97,29 @@ export function startLotusDenoise(vm: CallViewModel): () => void {
|
||||
room.localParticipant.getTrackPublication(Track.Source.Microphone)
|
||||
?.track as LocalAudioTrack | undefined;
|
||||
|
||||
// [lotus] `mic.getProcessor()` only becomes set once `setProcessor()`
|
||||
// resolves — i.e. after the whole wasm/model load. LiveKit fires
|
||||
// `LocalTrackPublished` once per local track (mic, then camera on join with
|
||||
// video), so two calls to `apply()` can both observe `!mic.getProcessor()`
|
||||
// and race to construct a second `LotusDenoiseProcessor` (a second
|
||||
// AudioContext + model load) before the first has attached. Track an
|
||||
// in-flight setProcessor per room and skip `apply()` while one is pending.
|
||||
const pendingProcessors = new Map<LivekitRoom, LotusDenoiseProcessor>();
|
||||
|
||||
const apply = (room: LivekitRoom): void => {
|
||||
const mic = micOf(room);
|
||||
if (mic && !mic.getProcessor()) {
|
||||
void mic
|
||||
.setProcessor(new LotusDenoiseProcessor(config))
|
||||
.catch((e) => logger.warn("[lotus] denoise setProcessor failed", e));
|
||||
}
|
||||
if (!mic || mic.getProcessor() || pendingProcessors.has(room)) return;
|
||||
const processor = new LotusDenoiseProcessor(config);
|
||||
pendingProcessors.set(room, processor);
|
||||
void mic
|
||||
.setProcessor(processor)
|
||||
.catch((e) => logger.warn("[lotus] denoise setProcessor failed", e))
|
||||
.finally(() => {
|
||||
// Only clear if we're still the pending entry (a teardown that ran
|
||||
// while this was in flight may have already replaced/removed it).
|
||||
if (pendingProcessors.get(room) === processor)
|
||||
pendingProcessors.delete(room);
|
||||
});
|
||||
};
|
||||
|
||||
const roomListeners = new Map<LivekitRoom, () => void>();
|
||||
@@ -149,6 +165,15 @@ export function startLotusDenoise(vm: CallViewModel): () => void {
|
||||
for (const room of rooms) {
|
||||
const mic = micOf(room);
|
||||
if (mic?.getProcessor()) void mic.stopProcessor();
|
||||
else {
|
||||
// [lotus] A setProcessor() call may still be in flight (mid wasm/model
|
||||
// load) when teardown runs, in which case `mic.getProcessor()` is
|
||||
// still undefined and `stopProcessor()` above is a no-op. Destroy the
|
||||
// pending processor directly so its AudioContext/graph don't leak.
|
||||
const pending = pendingProcessors.get(room);
|
||||
if (pending) void pending.destroy().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
pendingProcessors.clear();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user