call: consume self-built Element Call fork + activate Lotus features
CI / Build & Quality Checks (push) Successful in 11m5s
CI / Trigger Desktop Build (push) Successful in 25s

Switch to @lotusguild/element-call-embedded@0.20.1-lotus.1 (our self-built
fork) and turn on the source-level features it adds:

- #1 denoise CUTOVER: in-source ML denoise (lotusDenoiseSource=1) replaces
  the build-time getUserMedia shim — removed the shim injection from
  vite.config.js (denoise/ assets still shipped; the processor loads them).
  Survives reconnects (fixes A7).
- #2 call-state: CallEmbed consumes io.lotus.call_state; useCallSpeakers /
  useRemoteAllMuted prefer it over scraping EC's DOM (DOM fallback kept;
  empty payloads ignored).
- #4 focus: CallControl.focusCameraParticipant sends io.lotus.focus_participant
  (works during screenshare), replacing the DOM tile-click hack.
- #5 theming: lotusTransparent=1 (native transparent background).
- #6 decorations: LotusDecorationPusher sends each member's decoration URL
  via io.lotus.decorations -> rendered on in-call tiles.

#3 soundboard / #7 quality ship dormant (EC-ready; no host UI sends them yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 01:33:52 -04:00
parent 149ec8e4e4
commit 89cf171efc
11 changed files with 640 additions and 135 deletions
+27
View File
@@ -35,6 +35,19 @@ export const useCallSpeakers = (callEmbed: CallEmbed): Set<string> => {
callEmbed.iframe.contentDocument ?? callEmbed.iframe.contentWindow?.document ?? undefined;
const syncState = (): void => {
// [lotus #2] Prefer the fork's io.lotus.call_state events over scraping
// EC's rendered DOM. Falls back to the DOM path below when the fork hasn't
// sent yet (null) OR sent a spurious empty list (you're always present in
// your own joined call, so [] means "no usable data", not "nobody").
const lotus = callEmbed.getLotusParticipants();
if (lotus !== null && lotus.length > 0) {
const ls = new Set<string>();
lotus.forEach((p) => {
if (p.speaking && isUserId(p.userId)) ls.add(p.userId);
});
setSpeakers(ls);
return;
}
const doc = getDoc();
if (!doc) {
setSpeakers(new Set<string>());
@@ -91,6 +104,8 @@ export const useCallSpeakers = (callEmbed: CallEmbed): Set<string> => {
};
attachObserver();
// [lotus #2] Re-derive whenever the fork pushes new call-state.
const unsubLotus = callEmbed.onLotusCallState(syncState);
// If iframe isn't ready yet, wait for body to be available.
let bodyWatcher: MutationObserver | undefined;
@@ -109,6 +124,7 @@ export const useCallSpeakers = (callEmbed: CallEmbed): Set<string> => {
return () => {
tileObserver?.disconnect();
bodyWatcher?.disconnect();
unsubLotus();
};
}, [callEmbed, callMembers, joined]);
@@ -137,6 +153,14 @@ export const useRemoteAllMuted = (callEmbed: CallEmbed | undefined): boolean =>
const localUserId = callEmbed.room.client?.getUserId() ?? '';
const syncState = (): void => {
// [lotus #2] Prefer the fork's io.lotus.call_state over DOM scraping;
// ignore a spurious empty list (fall back to DOM).
const lotus = callEmbed.getLotusParticipants();
if (lotus !== null && lotus.length > 0) {
const remote = lotus.filter((p) => p.userId !== localUserId);
setMuted(remote.length > 0 && remote.every((p) => !p.audioEnabled));
return;
}
const doc = getDoc();
if (!doc) {
setMuted(false);
@@ -190,6 +214,8 @@ export const useRemoteAllMuted = (callEmbed: CallEmbed | undefined): boolean =>
};
attachObserver();
// [lotus #2] Re-derive whenever the fork pushes new call-state.
const unsubLotus = callEmbed.onLotusCallState(syncState);
// If iframe isn't ready yet, wait for body to be available.
let bodyWatcher: MutationObserver | undefined;
@@ -208,6 +234,7 @@ export const useRemoteAllMuted = (callEmbed: CallEmbed | undefined): boolean =>
return () => {
tileObserver?.disconnect();
bodyWatcher?.disconnect();
unsubLotus();
};
}, [callEmbed]);