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
18 KiB
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
- Merge the change into
lotus. - Bump
versioninembedded/web/package.json(0.25.0-lotus.N), commitchore(lotus): 0.25.0-lotus.Nonlotus, and push a tagv0.25.0-lotus.N. - CI builds the embedded bundle and, for
v*tags, publishes@lotusguild/element-call-embeddedto the Gitea npm registry (https://code.lotusguild.org/api/packages/LotusGuild/npm/). - 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.