# MatrixRTC SDK architecture `@element-hq/matrixrtc-sdk` is the model under Element Call, on its own: one `MatrixRTCClient` per room that joins and leaves the MatrixRTC session, discovers and connects to the media transports, publishes the local media, distributes the encryption keys, and hands out every member with its media as observables. It has no UI, reads no configuration and no settings, and never names LiveKit in its interface. Element Call becomes one consumer of it: `CallViewModel` keeps what makes a session a _call_ (ringing, auto-leave, layout, sounds, reactions, settings, the host bridge) and takes everything else from the client. The dependency direction is therefore `Element Call → sdk`, never the reverse. The migration from today's `CallViewModel` to that shape is in [`SdkMigration.md`](./SdkMigration.md). ## The architecture ``` host application (Element Call, a whiteboard, watch-together, …) │ │ createMatrixRTCClient(scope, matrixClient, room, options) ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ MatrixRTCClient │ │ │ │ public API, all Behaviors: │ │ join() leave() status$ connected$ reconnecting$ fatalError$ │ │ localMember$ remoteMembers$ memberCount$ sendData() data$ │ │ keyRotationSuppressed$ connectedTransports$ │ │ │ │ per member RTCMember { id userId deviceId membership$ displayName$ │ │ avatarUrl$ transport$ tracks$ encryptionError$ }│ │ per track MediaTrack { source kind muted$ encrypted$ stats$ attach() }│ │ │ │ ┌───────────────────────────┐ MediaBackend ┌───────────────────────────┐ │ │ │ session: MatrixRTC │ backend/api.ts │ media backend │ │ │ │ │ │ backend/livekit/ today, │ │ │ │ js-sdk MatrixRTCSession, │ │ or options.backend │ │ │ │ later the rust-rtc │ prepareLocal │ │ │ │ │ crate behind a bridge │ Transport ───▶│ token service: │ │ │ │ memberships from room │ delegateDelayed │ OpenID → JWT, │ │ │ │ state and sticky events │ Leave ───────▶│ delegation probe │ │ │ │ own membership, delayed │ │ ConnectionManager: │ │ │ │ leave, retries │ mediaKeys$ ────▶│ one LiveKit room per │ │ │ │ discovery of the local │ │ transport url, local │ │ │ │ transport (homeserver) │ mediaFor$ ─────▶│ published, remote │ │ │ │ join state machine │ ◀────── media$ │ subscribed │ │ │ │ (LocalMember) │ local.publish, │ Publisher: local tracks, │ │ │ │ transport registry │ setPublishing▶│ upstream paused until │ │ │ │ (TransportMetadata) │ ◀─ connections$ │ joined │ │ │ │ data packet → member │ ◀─ data$ │ LivekitMemberMedia: │ │ │ │ │ │ participant → tracks │ │ │ └───────────────────────────┘ └───────────────────────────┘ │ │ │ │ RTCMember = membership + mediaFor$(membership): the backend matches the │ │ member's rtcBackendIdentity to the identity it sees on the transport │ └──────────────────────────────────────────────────────────────────────────────┘ │ │ ▼ ▼ MatrixClient (sync, room state, to-device, OpenID) LiveKit SFU ``` The left box is MatrixRTC: who is in the session, with which transport, the keys, the delayed leave, and the join state machine. The right box is a media backend: whatever carries the media, behind the `MediaBackend` interface in `sdk/src/media-backend/api.ts`. The client is the join of the two: a member exists once its membership does, and gets its media once the backend has something for that membership on its transport. LiveKit is the one backend today; a cascading SFU or full mesh is another folder under `sdk/src/media-backend/`, handed in through `options.backend`, and nothing in the left box changes. ### How Element Call uses it ``` Element Call, as app, widget or component ┌────────────────────────────────────────────────────────────────┐ │ views: InCallView, tiles, header, footer, settings │ │ ▲ Behaviors │ │ CallViewModel │ │ layout, ringing and notifications, auto-leave, sounds, │ │ reactions, hand raise, settings, audio routing, host bridge │ │ ▲ status$, localMember$, remoteMembers$, tracks$ │ │ MatrixRTCClient @element-hq/matrixrtc-sdk │ └────────────────────────────────────────────────────────────────┘ │ MatrixClient + Room │ livekit-client ▼ ▼ homeserver SFU ``` All three modes already hold a `MatrixClient` and a `Room` by the time a call starts: standalone after `useLoadGroupCall`, the component from the host's `client` prop, the widget from `createRoomWidgetClient`. The view model passes them on and builds its media view models from `RTCMember` and its `tracks$` instead of from LiveKit participants. ### Joining, in order ``` host MatrixRTCClient / backend homeserver / JWT service LiveKit SFU │ create ───▶│ │ │ │ │ GET rtc/transports (session) ────────▶│ │ │ │◀── first transport of the backend's type │ │ │ prepareLocalTransport (backend): │ │ │ │ OpenID token, then /sfu/get ───────▶│ │ │ │◀── sfu url, jwt, alias, identity ─────│ = resolved$ of the │ │ │ probe delegate_delayed_leave ──────▶│ local transport │ │ │ connect(url, jwt) ───────────────────────────────────────────▶│ │ join() ───▶│ │ │ │ │ membership state event + delayed leave ──▶│ │ │ │ delegateDelayedLeave(delayId) (backend) ─▶│ │ │ │ create tracks, resume upstream ──────────────────────────────▶│ │ │ media key to every other member (to-device) ──▶│ │ │ │◀── other memberships (sync) ──────────│◀── participants ────────│ │◀─ status$ "connected", localMember$, remoteMembers$ with tracks$ ─────────────│ ``` The session discovers the transport from the homeserver and the backend prepares it, both before `join()`, so that the membership can name it and the first frames go out as soon as the state event is sent. Preparing is the backend's own business: for LiveKit the token exchange, and the probe that decides whether the SFU can take over the delayed leave, which picks the delayed leave timings the join uses. Until `join()` the tracks exist but their upstream is paused: a host can show a preview without anyone hearing it. The upstream flows exactly while the member is joined and the homeserver is reachable; `leave()` or a sync outage pauses it, and `leave()` also sends the leave. ## Vocabulary | Word | Meaning here | Elsewhere | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | client | a `MatrixRTCClient`: one room, one slot, one local member | `MatrixClient` is always written out | | membership | one device's MatrixRTC state event, seen by hosts as `RTCMembership`, the js-sdk `CallMembership` behind it | | | member | an `RTCMember`: a membership plus what the SDK derives from it (display name, transport, media) | Element Call's "matrixLivekitMember" | | media | what a member sends, once it has arrived on its transport: `tracks$` on the member, `null` while waiting for media, and `encryptionError$` beside it. The backend supplies the same two fields | Element Call's `MediaViewModel` is a view of it | | track | a `MediaTrack`: one publication of a member, audio or video, with `source`, `kind`, `id` and the behaviors on it. `LocalAudioMediaTrack` and `LocalVideoMediaTrack` add the controls over our own | LiveKit `TrackPublication` and `Track`, which never leave the SDK | | source | a `MediaSource`: what a track is captured from, `microphone`, `camera`, `screenShare`, `screenShareAudio` or `unknown`. Picked out of `tracks$` with `trackBySource$` | LiveKit `Track.Source` | | publish request | a `PublishRequest`: a source, where to capture it from and how to encode it. Given at the join in `options.publish`, or later to `publish` on the local member | | | active | `isActive$` on an audio track: the track carries sound, as the backend measures it. "Speaking" is a call's reading of it on the microphone track | LiveKit "speaking", `isSpeaking` | | attach, detach | handing a `