Element Call Integration#
Overview#
Element Call is an MSC3401-based group call backend embedded as a widget in Element Web. The core implementation lives in the ElementCall class, which extends the abstract Call class in src/models/Call.ts. Element Web supports two call backends: JitsiCall for legacy video rooms and ElementCall for MSC3401-based calls . Call.get(room) selects the right backend — ElementCall first, then JitsiCall .
ElementCall is enabled by the feature_group_calls setting, or by both feature_video_rooms + feature_element_call_video_rooms when the room is a call room . Rather than requiring a room state event, Element Call runs as a virtual widget created by createOrGetCallWidget() and added to WidgetStore in memory only. The widget is rendered as a sandboxed iframe by AppTile and communicates with Element Web over a postMessage channel via ClientWidgetApi.
Widget URL Parameters#
ElementCall.generateWidgetUrl() constructs the Element Call iframe URL. The base URL is SdkConfig.get("element_call").url + /room; parameters are passed as a URL hash fragment (#?params) using $variable template syntax so they can be overridden at runtime via widget data .
Lobby / behavior parameters (the most commonly modified):
| Parameter | Template var | Purpose |
|---|---|---|
skipLobby | $skipLobby | Skip EC's pre-join lobby |
returnToLobby | $returnToLobby | Return to lobby after call ends instead of blank screen |
preload | $preload | Preload iframe in background — deprecated, should be false |
perParticipantE2EE | $perParticipantE2EE | Per-participant E2EE; auto-set from room encryption state |
embed | true | Signals EC is embedded in a host app |
hideHeader | true | Hides EC's own header |
Client context (always present): userId, deviceId, roomId, baseUrl, lang, fontScale, theme, analyticsID .
Conditional parameters appended based on settings:
allowIceFallback=trueiffallbackICEServerAllowedis enabledallowVoipWithNoMedia=trueiffeature_allow_screen_share_only_modeis enabled — enables screen-share-only callsfont(repeated) ifuseSystemFontis set
skipLobby and returnToLobby#
These two parameters govern the pre-join and post-call lobby experience. Only ElementCall supports them; JitsiCall and legacy calls do not.
skipLobby — when true, Element Call skips its own pre-join lobby and connects the user directly . Scenarios:
- Shift+click the call button: user explicitly requests direct join →
skipLobby=true - Video rooms: always set to
false(lobby always shown) - Default: passed as
undefined(notfalse) so Element Call applies its own defaults
On the Element Web side, when skipLobby is false/unset, performConnection() sets connectionState = ConnectionState.Lobby and waits indefinitely for a MatrixRTC membership change . When true, that wait is bypassed.
returnToLobby — when true, Element Call shows the lobby again after the call ends rather than a blank screen . Automatically true for video rooms via isVideoRoom() .
Parameter flow: placeCall() dispatches a ViewRoomPayload with skipLobby → RoomViewStore → CallView updates call.widget.data.skipLobby . PR #30848 refactored this to regenerate the widget URL just before start(), and changed the default from false to undefined to defer lobby decisions to Element Call.
Default Media Device States#
Before widget messaging begins, Call.start() reads MediaDeviceHandler to select which devices to pass:
- Microphone:
MediaDeviceHandler.startWithAudioMutedreads theaudioInputMutedsetting. Default:false(mic starts on). - Camera:
MediaDeviceHandler.startWithVideoMutedreads thevideoInputMutedsetting. Default:false(camera starts on). - Both settings are stored at the
DEVICElevel (local browser storage, not server-synced) .
Call.start() selects the saved device ID or falls back to the first available device and passes audioInput/videoInput (MediaDeviceInfo | null) to performConnection(). For Element Call, these are forwarded via the JoinCall widget action only when the deprecated preload=true is set ; otherwise Element Call manages device selection internally.
Screen sharing: no mute default. Enabled by setting feature_allow_screen_share_only_mode, which appends allowVoipWithNoMedia=true to the widget URL , allowing screen-share-only participation without audio/video.
Connection States and Lobby Flow#
The ConnectionState enum tracks the call lifecycle. The Lobby, WidgetLoading, and Disconnected states all represent a disconnected user but carry different context:
| State | Meaning |
|---|---|
Disconnected | No widget connection |
WidgetLoading | Iframe initializing |
Lobby | Widget ready, waiting for user to join EC's lobby |
Connecting | Awaiting MatrixRTC membership |
Connected | Active call |
Disconnecting | Teardown in progress |
ElementCall.performConnection() drives the join flow:
- If
widget.data.preload(deprecated), sendsJoinCallwith device labels immediately . - If
skipLobbyis nottrue, sets state toConnectionState.Lobby; user sees EC's own pre-join lobby. - Waits (no timeout) for the user's MatrixRTC membership to appear via
MembershipsChangedorSessionStarted. - Calls
sendCallNotify()to emitm.call.notifyfor other participants . - Base
Call.start()sets state toConnected.
Widget messaging actions registered during connection : HangupCall, TileLayout, SpotlightLayout, DeviceMute.
Video room persistence: on hangup, onHangup immediately calls start() again to re-show the lobby — both ElementCall and JitsiCall implement this.
Widget Iframe and Messaging Infrastructure#
For the full iframe lifecycle, see the Widget Iframe Management KB article. Element Call-specific notes:
- Virtual widget: no room state event.
createOrGetCallWidget()callsWidgetStore.addVirtualWidget()with a random 24-char ID. If a CALL-type widget already exists, it is reused and its data updated . - iframe sandbox:
allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-presentation allow-downloads - iframe feature policy:
microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; clipboard-read - E2EE:
perParticipantE2EEis auto-set totruefor encrypted rooms unlessfeature_disable_call_per_sender_encryptionis enabled . - Destruction:
ElementCall.destroy()callsActiveWidgetStore.destroyPersistentWidget()(tears downClientWidgetApi) andWidgetStore.removeVirtualWidget().