Files
element-call/LOTUS.md
Lotus CIandClaude Opus 5.5 a951e5b1f2
CI / Build embedded bundle (pull_request) Successful in 3m1s
CI / Publish to Gitea npm registry (pull_request) Skipped
feat(lotus): detailed call summary for the host's Details card (cinny #253)
io.lotus.call_summary gains an optional `details`: the other Matrix users
seen and the peak in the call; how long your connection, each other
person's, and everyone at once (likely the server) was poor/lost; RTT
median/p95, outbound and inbound packet loss and jitter (sampled every 5 s
from LiveKit sender/receiver stats); and your talk, mute and screenshare
time. People already present when you connect are recorded on Connected
(LiveKit sends no ParticipantConnected for them), and joins are idempotent
per device. Older hosts ignore the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-20 19:45:02 -04:00

18 KiB
Raw Permalink Blame History

Lotus fork of Element Call

This fork is embedded by Lotus Chat (cinny) as the in-app call frame. Everything it adds is opt-in: without the URL flags and widget actions below it behaves like upstream Element Call.

The source of truth is src/lotus/ (the io.lotus.* actions are declared in src/lotus/lotusActions.ts). Keep this file in step with it: when the host and the fork drift, features fail silently (element-call#43, cinny#260).

URL flags

The host sets these on the widget URL (cinny: CallEmbed.getWidget).

Flag Effect
lotusCallState=true Stream call state to the host: io.lotus.call_state (participants, speaking, mute) and io.lotus.mic_level.
lotusTheme=true Apply the host theme; the frame's root color-scheme follows it.
lotusTransparent=true Transparent canvas so the host's wallpaper shows through. Only meaningful with lotusTheme (the fork applies it anyway and warns).
lotusHostControls=true The host draws the call bar: hide Element Call's footer and enable the host-control actions.
lotusFrameScreenshare=true With lotusHostControls: show the frame's own screenshare button, for engines without Capability Delegation (Firefox, Safari) where the host can't start a share.
lotusAudioInject=true Accept io.lotus.inject_audio (soundboard).
lotusDenoiseSource=true Run ML noise suppression in-source as a LiveKit audio TrackProcessor.
lotusModel=rnnoise|speex|dtln|deepfilternet Denoise model (default rnnoise; falls back to rnnoise once if the chosen model fails).
lotusGate=true, lotusGateThreshold=<dB> Noise gate after denoise (default −45 dB).
lotusDenoiseFloor=<0..1> Denoise mix floor.
lotusDenoiseBase=<url> Where the denoise worklets / wasm / models are served (same-origin by default: ./denoise/).
sendNotificationType=none Upstream param, extended: none sends no call-start notification at all (cinny#295; voice channels shouldn't @room-ping).

Widget actions (io.lotus.*)

"host → call" actions must be allow-listed in LOTUS_TO_WIDGET_ACTIONS. The table is generated from src/lotus/lotusActions.ts; the last column was checked against cinny's lotus branch.

Action Direction Payload / meaning Handled by cinny (lotus)
io.lotus.call_state call → host in-call per-participant speaking / mute state. NOTE: matrix-widget-api transport.send is request/response — the host MUST register a handler that replies/acks each one (cinny's listenAction does, replying {}). If the host has no handler, ClientWidgetApi immediately error-replies "unsupported from-widget action", so the data is silently dropped and each throttled send rejects (caught) — functional miss + log churn, not a hang. yes
io.lotus.focus_participant host → call pin/spotlight (or clear, with userId=null) a participant. yes
io.lotus.inject_audio host → call play an audio clip into the call: as a separate published track, or mixed into the microphone where the token only allows listed sources (rooms with a call policy, #47). Replies { played: false, reason: "muted" | "not_permitted" } when it can't. yes
io.lotus.set_quality host → call set audio/screenshare encoding quality (bitrate/framerate). yes
io.lotus.decorations host → call per-user avatar-decoration image URLs for in-call tiles. yes
io.lotus.set_deafen host → call deafen remote audio (and optionally mute screenshare audio). yes
io.lotus.request_state call → host sent on (re)registration of a lotus toWidget handler (e.g. decorations) so the host can re-push state that would otherwise only be sent on change (decorations roster, the current focus pin) after an EC-side reconnect/remount. cinny's resendForkState() should respond to this the same way it responds to a fresh join. yes
io.lotus.denoise_state call → host real state of the in-source denoise engine — { active: boolean, model: string, error?: string } — sent once the processor attaches (or after the rnnoise fallback also fails), so the host toggle can reflect reality rather than the requested state. yes
io.lotus.call_summary call → host one-shot end-of-call readout { durationMs, reconnects, poorMs, verdict, details? } (#143); details (cinny #253): participants, peak, per-person poor time, everyone-poor time, RTT/loss/jitter, your talk/mute/screenshare time. yes
io.lotus.set_audio_output host → call select the audio output device { deviceId } (#119). yes
io.lotus.audio_output_state call → host the currently selected output { deviceId } (#119). cinny #417
io.lotus.screenshare_audio_state call → host { present }: whether any remote screenshare audio is being received, sent on change (cinny #277). cinny #277
io.lotus.screenshare_notice call → host local screenshare reminder { kind: "ended" | "no-frames" | "alone" } — window closed, no frames for 15 s, or 30 min of sharing with nobody else in the call (#39). Each fires at most once per share. yes
io.lotus.set_layout host → call switch the call layout { layout: "grid" | "spotlight" } (cinny #43 — replaces the host clicking EC's hidden layout radio). yes
io.lotus.open_settings host → call open EC's settings modal, or close it with { open: false }; omit open to toggle (cinny #43). yes
io.lotus.toggle_reactions host → call toggle the reactions / raise-hand menu (cinny #43). yes
io.lotus.set_screenshare host → call start/stop sharing { on?: boolean } (omit to toggle; cinny #43). Starting calls getDisplayMedia, which needs the user's click: the host sends this with Capability Delegation (delegate: "display-capture"), and without it the browser rejects the share. yes
io.lotus.set_hotkeys host → call { codes: string[] } — KeyboardEvent.code values (PTT, deafen) to report while focus is inside this frame (cinny #43). [] stops. yes
io.lotus.set_frame_screenshare host → call { visible: boolean } — whether the in-frame screenshare button (lotusFrameScreenshare) may show, e.g. false while the room's call policy forbids screensharing (cinny #43). Defaults to visible. yes
io.lotus.prompt_screenshare host → call {} — show the "Share your screen?" prompt inside this frame (cinny #43). For hosts that can't delegate the user's click: the prompt's Share button is clicked here, so getDisplayMedia gets its activation. yes
io.lotus.hotkey call → host a watched key went down/up here, or this window's focus changed; see LotusHotkeyReport (cinny #43). yes
io.lotus.controls_state call → host { screensharing: boolean, layout: "grid" | "spotlight" | null, screenshareAction: true, hotkeys: true, frameScreenshare: false, screensharePrompt: boolean, screensharePromptOpen: boolean } whenever either changes (cinny #43), so the host stops reading EC's DOM for them. Its arrival also tells the host this fork supports the actions above. yes
io.lotus.mic_level call → host local mic level { bars: 0 | 1 | 2 | 3 } (cinny #146), sent only when it changes (≤ 10 Hz); 0 while muted or with no mic. yes
io.lotus.call_error call → host the call's error page is up ({ code, recoverable }) or cleared ({ code: null }) (cinny #318), so the host stops showing the call as joined after "Connection lost". cinny #418
io.lotus.reconnect host → call press the error page's Reconnect / Try again (cinny #318). cinny #418

Entries naming a cinny PR are handled once that PR merges.

Fork-only behaviour (no flag needed)

Per-person volume memory, screenshare notices (window closed / no frames / sharing alone), mono mic capture, an SFU refusal reason instead of "Something went wrong", "Try again" for failed connects, and support for running on its own origin (cinny#43).

Releasing

  1. Merge the change into lotus.
  2. Bump version in embedded/web/package.json (0.25.0-lotus.N), commit chore(lotus): 0.25.0-lotus.N on lotus, and push a tag v0.25.0-lotus.N.
  3. CI builds the embedded bundle and, for v* tags, publishes @lotusguild/element-call-embedded to the Gitea npm registry (https://code.lotusguild.org/api/packages/LotusGuild/npm/).
  4. In cinny, pin the exact version in package.json (npm install --save-exact @lotusguild/element-call-embedded@0.25.0-lotus.N) and open a PR.