# 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() state$ │ │ 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 │ media-backend/ │ media backend │ │ │ │ │ api.ts │ media-backend/livekit/ │ │ │ │ js-sdk MatrixRTCSession, │ │ today, 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 │ mediaFor$ ─────▶│ transport url, local │ │ │ │ transport (homeserver) │ ◀─ tracks$, │ published, remote │ │ │ │ membership state machine │ encryptionError$│ subscribed │ │ │ │ (LocalMembership) │ 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 membership 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; `state$` is the membership state combined with the local media connection. 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 │ │ ▲ state$, 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 ────────│ │◀─ state$ 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 membership is `joined`: a sync outage pauses it, and `leave()` pauses it for good and sends the leave. ## Vocabulary The words the SDK is written in. They name the shape after the slot and participation split in [`client-into-slot-session-split-plan.md`](./client-into-slot-session-split-plan.md); the sections below still describe `MatrixRTCClient` until that lands. | Word | Meaning here | Elsewhere | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | slot | an `RTCSlot`: the namespace in a room that memberships join into, `application` plus `id`. It exists while the room does, with nobody in it too. Holds `members$`, its open or closed `status$`, and `join()` | the js-sdk `MatrixRTCSession` is one per slot; the MSC4143 slot event | | participation | an `RTCParticipation`: the span between our `join()` of a slot and our `leave()`. Holds the connection and its `state$`, `localMember$`, `remoteMembers$`, the connected transports and the data channel. Made by `slot.join()`, never reused | "session" is not used here, because in the js-sdk it means the slot | | member | an `RTCMember`: one membership in a slot with what the SDK derives from it, `rtcBackendIdentity`, `userId`, `deviceId`, `memberId`, `displayName$`, `avatarUrl$`, `transport$` and `applicationData$`. No media. What the slot hands out in `members$` | the js-sdk `CallMembership` behind it, which never leaves the SDK; Element Call's old "matrixLivekitMember" | | local member, remote member | `LocalRTCMember` and `RemoteRTCMember`: an `RTCMember` plus its `MemberMedia`, which only a participation can carry. The local one's tracks carry the controls. What the participation hands out in `localMember$` and `remoteMembers$` | LiveKit `LocalParticipant` and `RemoteParticipant`, which never leave the SDK | | media | a `MemberMedia`: what a member sends once it has arrived on its transport, `tracks$`, null while waiting, and `encryptionError$` beside it. The media backend supplies exactly these two fields per member | 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 | | publish request | a `PublishRequest`: a source, where to capture it from and how to encode it. Given at the join in `publish`, or later to `publish` on the participation | | | attach, detach | handing a `