fix(lotus): Wave-1 audit fixes (EC1–EC6)
- EC1: lotusQuality — track + clearTimeout the 500ms settle re-apply per room
(was leaking a timer that fired on torn-down rooms).
- EC2/EC3: lotusQuality + lotusAudioInject drive off vm.allConnections$ instead
of the remote-gated livekitRoomItems$ (were no-ops when alone), matching
lotusDenoise.
- EC4: lotusDecorations resets its roster to {} on teardown so a decoration from
a previous call can't render on a shared user in the next one.
- EC5: hoisted a stable useSyncExternalStore subscribe fn (was re-subscribing
every tile render).
- EC6: lotusFocus only sets the spotlight when the userId field is present
(a partial payload no longer clears the pin).
tsc clean. Needs a republish to ship.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
02666c0c04
commit
0ffe247929
@@ -38,10 +38,13 @@ export function startLotusAudioInject(vm: CallViewModel): () => void {
|
|||||||
const w = widget;
|
const w = widget;
|
||||||
if (!w) return () => undefined;
|
if (!w) return () => undefined;
|
||||||
|
|
||||||
// Track the set of connected LiveKit rooms to publish into.
|
// Track the set of connected LiveKit rooms to publish into. Drive off the
|
||||||
|
// LOCAL participant's connection(s), not `livekitRoomItems$` — that stream
|
||||||
|
// omits rooms with no remote members, so inject would no-op while you're
|
||||||
|
// alone. Map the connections to their livekit rooms like `lotusDenoise.ts`.
|
||||||
let rooms: LivekitRoom[] = [];
|
let rooms: LivekitRoom[] = [];
|
||||||
const sub = vm.livekitRoomItems$.subscribe((items) => {
|
const sub = vm.allConnections$.subscribe((data) => {
|
||||||
rooms = items.map((i) => i.livekitRoom);
|
rooms = data.getConnections().map((c) => c.livekitRoom);
|
||||||
});
|
});
|
||||||
|
|
||||||
// In-flight clips, so we can abort them on teardown (unmount / vm change /
|
// In-flight clips, so we can abort them on teardown (unmount / vm change /
|
||||||
|
|||||||
@@ -26,15 +26,16 @@ function emit(): void {
|
|||||||
for (const l of listeners) l();
|
for (const l of listeners) l();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stable module-scope subscribe reference, so `useSyncExternalStore` doesn't
|
||||||
|
// re-subscribe (add/remove the listener) on every render of a tile.
|
||||||
|
function subscribe(cb: () => void): () => void {
|
||||||
|
listeners.add(cb);
|
||||||
|
return () => listeners.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
/** Subscribe a tile avatar to its participant's decoration URL (or undefined). */
|
/** Subscribe a tile avatar to its participant's decoration URL (or undefined). */
|
||||||
export function useLotusDecoration(userId: string): string | undefined {
|
export function useLotusDecoration(userId: string): string | undefined {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(subscribe, () => decorations[userId]);
|
||||||
(cb) => {
|
|
||||||
listeners.add(cb);
|
|
||||||
return () => listeners.delete(cb);
|
|
||||||
},
|
|
||||||
() => decorations[userId],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeImageUrl(raw: unknown): string | null {
|
function safeImageUrl(raw: unknown): string | null {
|
||||||
@@ -94,8 +95,12 @@ export function startLotusDecorations(): () => void {
|
|||||||
registrations = 0;
|
registrations = 0;
|
||||||
unregister?.();
|
unregister?.();
|
||||||
unregister = null;
|
unregister = null;
|
||||||
// Intentionally keep the last decorations map: a transient remount must
|
// Reset the roster once the last registration goes away, so a decoration
|
||||||
// not drop decorations until the host next pushes an update.
|
// pushed in call A can't leak onto a shared user in call B before the
|
||||||
|
// host re-pushes. Notify listeners so any still-mounted tile drops the
|
||||||
|
// now-stale overlay via the render path.
|
||||||
|
decorations = {};
|
||||||
|
emit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ export function startLotusFocus(vm: CallViewModel): () => void {
|
|||||||
// Always reply so the host transport doesn't time out.
|
// Always reply so the host transport doesn't time out.
|
||||||
void w.api.transport.reply(ev.detail, {});
|
void w.api.transport.reply(ev.detail, {});
|
||||||
const data = ev.detail.data as { userId?: unknown } | undefined;
|
const data = ev.detail.data as { userId?: unknown } | undefined;
|
||||||
const userId = typeof data?.userId === "string" ? data.userId : null;
|
// Mirror deafen's partial-payload semantics: a payload that OMITS `userId`
|
||||||
vm.setManualSpotlight(userId);
|
// must keep the current spotlight, not clear it. Only act when the key is
|
||||||
|
// actually present — an explicit `null` clears, a string pins that user.
|
||||||
|
if (data && "userId" in data) {
|
||||||
|
const userId = typeof data.userId === "string" ? data.userId : null;
|
||||||
|
vm.setManualSpotlight(userId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
w.lazyActions.on(LotusWidgetActions.FocusParticipant, handler);
|
w.lazyActions.on(LotusWidgetActions.FocusParticipant, handler);
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export function startLotusQuality(vm: CallViewModel): () => void {
|
|||||||
// Per-room LocalTrackPublished listeners, so sticky settings re-apply on
|
// Per-room LocalTrackPublished listeners, so sticky settings re-apply on
|
||||||
// every (re)publish.
|
// every (re)publish.
|
||||||
const roomListeners = new Map<LivekitRoom, () => void>();
|
const roomListeners = new Map<LivekitRoom, () => void>();
|
||||||
|
// Per-room settle re-apply timers, so we can cancel a pending 500ms re-apply
|
||||||
|
// when a room is removed or on teardown — otherwise it would fire against a
|
||||||
|
// torn-down room.
|
||||||
|
const settleTimers = new Map<LivekitRoom, ReturnType<typeof setTimeout>>();
|
||||||
let rooms: LivekitRoom[] = [];
|
let rooms: LivekitRoom[] = [];
|
||||||
|
|
||||||
const applyToRoom = (room: LivekitRoom): void => {
|
const applyToRoom = (room: LivekitRoom): void => {
|
||||||
@@ -76,8 +80,12 @@ export function startLotusQuality(vm: CallViewModel): () => void {
|
|||||||
const applyToAll = (): void => rooms.forEach(applyToRoom);
|
const applyToAll = (): void => rooms.forEach(applyToRoom);
|
||||||
|
|
||||||
// Keep the LocalTrackPublished listeners in sync with the connected rooms.
|
// Keep the LocalTrackPublished listeners in sync with the connected rooms.
|
||||||
const sub = vm.livekitRoomItems$.subscribe((items) => {
|
// Drive off the LOCAL participant's connection(s), not `livekitRoomItems$` —
|
||||||
const next = items.map((i) => i.livekitRoom);
|
// that stream omits rooms with no remote members (returns null for isLocal),
|
||||||
|
// so caps wouldn't apply to the local senders while you're alone. Map the
|
||||||
|
// connections to their livekit rooms exactly like `lotusDenoise.ts` does.
|
||||||
|
const sub = vm.allConnections$.subscribe((data) => {
|
||||||
|
const next = data.getConnections().map((c) => c.livekitRoom);
|
||||||
rooms = next;
|
rooms = next;
|
||||||
// Remove listeners for rooms that went away.
|
// Remove listeners for rooms that went away.
|
||||||
for (const [room, off] of roomListeners) {
|
for (const [room, off] of roomListeners) {
|
||||||
@@ -97,7 +105,18 @@ export function startLotusQuality(vm: CallViewModel): () => void {
|
|||||||
// recompute so our cap wins.
|
// recompute so our cap wins.
|
||||||
const reapply = (): void => {
|
const reapply = (): void => {
|
||||||
applyToRoom(room);
|
applyToRoom(room);
|
||||||
setTimeout(() => applyToRoom(room), 500);
|
// Store the settle timer per room and cancel any pending one, so it
|
||||||
|
// can be cleared on removal/teardown and never fires against a
|
||||||
|
// torn-down room.
|
||||||
|
const prev = settleTimers.get(room);
|
||||||
|
if (prev !== undefined) clearTimeout(prev);
|
||||||
|
settleTimers.set(
|
||||||
|
room,
|
||||||
|
setTimeout(() => {
|
||||||
|
settleTimers.delete(room);
|
||||||
|
applyToRoom(room);
|
||||||
|
}, 500),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const events = [
|
const events = [
|
||||||
ParticipantEvent.LocalTrackPublished,
|
ParticipantEvent.LocalTrackPublished,
|
||||||
@@ -106,6 +125,9 @@ export function startLotusQuality(vm: CallViewModel): () => void {
|
|||||||
for (const e of events) room.localParticipant.on(e, reapply);
|
for (const e of events) room.localParticipant.on(e, reapply);
|
||||||
roomListeners.set(room, () => {
|
roomListeners.set(room, () => {
|
||||||
for (const e of events) room.localParticipant.off(e, reapply);
|
for (const e of events) room.localParticipant.off(e, reapply);
|
||||||
|
const t = settleTimers.get(room);
|
||||||
|
if (t !== undefined) clearTimeout(t);
|
||||||
|
settleTimers.delete(room);
|
||||||
});
|
});
|
||||||
applyToRoom(room);
|
applyToRoom(room);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user