fix(lotus): denoise restart fallback must not hand LiveKit its own raw track

On a failed restart() the processor set processedTrack to the LiveKit-owned
input track. LiveKit's internalStopProcessor() stops processedTrack and then
republishes _mediaStreamTrack — the same object — so any later
stopProcessor()/teardown killed the live mic for the rest of the session.
Leave processedTrack undefined so LiveKit falls through to its own track.
Unit-tested.

Fixes #2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-12 11:34:08 -04:00
co-authored by Claude Opus 5
parent f1cfcc7377
commit 936a083533
2 changed files with 92 additions and 17 deletions
+55
View File
@@ -0,0 +1,55 @@
/*
Copyright 2026 Lotus Guild
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { describe, expect, test } from "vitest";
import { type AudioProcessorOptions } from "livekit-client";
import { LotusDenoiseProcessor } from "./lotusDenoiseProcessor";
function makeProcessor(): LotusDenoiseProcessor {
return new LotusDenoiseProcessor({
model: "rnnoise",
assetBase: "https://example.invalid/denoise/",
gate: false,
gateThreshold: -45,
floor: 0.15,
});
}
describe("LotusDenoiseProcessor.restart", () => {
test("falls back to processedTrack = undefined (never the raw LiveKit track) when graph rebuild fails", async () => {
const processor = makeProcessor();
// Stub out the AudioContext/graph plumbing: pretend the context is fine
// but the graph rebuild (wasm load / worklet construction) throws, which
// is the path this test targets.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(processor as any).ensureContext = async (): Promise<void> => {
await Promise.resolve();
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(processor as any).buildGraph = async (): Promise<never> => {
await Promise.resolve();
throw new Error("simulated graph build failure");
};
const rawTrack = {
stop: (): void => undefined,
} as unknown as MediaStreamTrack;
await processor.restart({
track: rawTrack,
} as unknown as AudioProcessorOptions);
// Must NOT be the raw, LiveKit-owned track: LiveKit's
// internalStopProcessor() calls `processor.processedTrack?.stop()` then
// re-publishes the same `_mediaStreamTrack` object, which would kill the
// live mic on the next stopProcessor()/teardown if we handed it back here.
expect(processor.processedTrack).toBeUndefined();
expect(processor.processedTrack).not.toBe(rawTrack);
});
});