diff --git a/src/lotus/lotusMicLevel.test.ts b/src/lotus/lotusMicLevel.test.ts index 239ad608..d6056e3b 100644 --- a/src/lotus/lotusMicLevel.test.ts +++ b/src/lotus/lotusMicLevel.test.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { describe, expect, it } from "vitest"; -import { MicLevelQuantizer } from "./lotusMicLevel"; +import { MicLevelQuantizer, sameRooms } from "./lotusMicLevel"; describe("MicLevelQuantizer", () => { it("maps RMS to 0–3 bars", () => { @@ -35,3 +35,14 @@ describe("MicLevelQuantizer", () => { expect(q.push(0)).toBe(0); }); }); + +describe("sameRooms (EC #45)", () => { + it("compares rooms by identity and order", () => { + const a = {}; + const b = {}; + expect(sameRooms([a, b], [a, b])).toBe(true); + expect(sameRooms([a], [a, b])).toBe(false); + expect(sameRooms([a, b], [b, a])).toBe(false); + expect(sameRooms([{}], [{}])).toBe(false); + }); +}); diff --git a/src/lotus/lotusMicLevel.ts b/src/lotus/lotusMicLevel.ts index 6fc2f298..3e148532 100644 --- a/src/lotus/lotusMicLevel.ts +++ b/src/lotus/lotusMicLevel.ts @@ -46,6 +46,10 @@ const micPublication = ( : null; }; +/** Pure: the same LiveKit rooms, in the same order (by identity). */ +export const sameRooms = (a: readonly T[], b: readonly T[]): boolean => + a.length === b.length && a.every((r, i) => r === b[i]); + const samplers = new WeakMap< CallViewModel, Observable @@ -62,10 +66,15 @@ export function observeLocalMicSample$( const cached = samplers.get(vm); if (cached) return cached; const sampler$ = vm.allConnections$.pipe( + map((data) => data.getConnections().map((c) => c.livekitRoom)), + // [EC #45] allConnections$ re-emits far more often than the set of + // LiveKit rooms changes; restarting the tap each time closed the + // AudioContext ~1×/s and emitted null, which reset the muted-speech + // gate so the "You're muted" nudge could never fire. + distinctUntilChanged(sameRooms), switchMap( - (data) => + (rooms) => new Observable((subscriber) => { - const rooms = data.getConnections().map((c) => c.livekitRoom); let ctx: AudioContext | null = null; let clone: MediaStreamTrack | null = null; let timer: ReturnType | undefined;