Files
element-call/LOTUS.md
T
Lotus CIandClaude Opus 5.5 2ef80cef2c
CI / Build embedded bundle (pull_request) Successful in 2m1s
CI / Publish to Gitea npm registry (pull_request) Skipped
docs(lotus): LOTUS.md lists io.lotus.call_error and io.lotus.reconnect (#50)
Regenerate the action table from lotusActions.ts: add the two actions
from #50, and note which cinny PR handles each one still in flight.

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

18 KiB
Raw 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. 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 for the local participant — { durationMs, reconnects, poorMs, verdict } (#143) — sent on SFU disconnect or in-call teardown, whichever comes first. 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_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.