Repository navigation
Releases: element-hq/matrix-rust-rtc
Release list
v0.5.0-rc.5
Breaking
CallJoinOptions::kind(FFIFfiJoinSessionParams::kind, required) replacesnotify:Group { notify }for a group call's notification,Direct { ring }for a DM call the library rings, follows and ends; webjoinCallrefusesnotification_type: "ring".- An MSC4075 ring now lasts 90 s by default, as Element Call's does.
notify_session_startedtakes theDialectBackend, so a notification always goes out in the room's membership format.CallEvent::Ended,CallEvent::MediaConnectionStateandEndedReason(FFIFfiEndedReason, webendedandmedia_connection_state) are removed: the call's status says both.
Added
RtcCall::status/subscribe_status(FFIstatus/subscribe_status, webstatusandonStatus,LiveKitCall::status) report the call as connecting, calling (a DM's ring, with the callee's user ID and our media's progress), connected since the pickup or our media, or ended with a reason, including a DM's decline, no answer and everyone else leaving.ApplicationIntake::sticky_event_types/on_sticky_eventshand an application the sticky events of its types, andBaseRtcRoom::room_membersthe room's joined users.
v0.5.0-rc.4
Breaking
HandRaisedandHandLowerednow announce only hands raised or lowered while we are in the call, never the hands we arrive to or a member arrives with, andReactionis sent only while we are in the call and for reactions newer thanReactionsConfig::max_age_ms(10 s by default).
Added
- Participants and tiles carry
hand_raised_position, their 1-based place in the raised-hand queue.
v0.5.0-rc.3
Breaking
ParticipantJoinedandParticipantLeftnow report only arrivals and departures while we are in the call, never the roster we join, lapse or return to, and carryparticipant_count, the memberships after the change.
Added
- A local video publication reports send statistics per simulcast layer (size, frame rate, bytes, whether dynacast paused it), as
LocalTrackHandle::send_statsandFfiLocalTrack::send_stats.
v0.5.0-rc.2
Added
sdk_version()(websdkVersion()) returns the SDK release version, for hosts to display or attach to log reports, and logging setup logs it.set_ranking_threshold(tiles)keeps tiles in join order, screen shares first, while a call has no more than that many remote tiles, so small calls stop shuffling on speaking, video or a raised hand; sticky and MSC4143 memberships take their join time from the first member event'sorigin_server_ts.
v0.5.0-rc.1
Breaking
matrix-rtc-mediais renamedmatrix-rtc-call-sdk, andLiveKitCall,open_slot,attach_livekitand the examples and e2e test move there frommatrix-rtc-livekit(featureslivekitandmatrix-sdk), leavingmatrix-rtc-livekitthe transport alone, without amatrix-sdkfeature.- The transport contract, frames, constraints and media key handler move from
matrix-rtc-mediato the newmatrix-rtc-transport, andmatrix-rtc-livekit-protobecomes itslivekitmodule;matrix-rtc-mediakeeps the call's roster, tiles, events andattach_media. LiveKitMediaTransport::connect_livekitbecomesOwnFocusTransport::connect_own, andLiveKitTransportConnection::set_local_key_indexmoves ontoTransportConnection.connect_media_session/connectMediatake the own focus from the call and the account from the backend:MediaSessionConfigloseslivekit_service_url,user_idanddevice_id, and a receive-only call connects only to its peers' foci.MembershipSnapshotSubscription::next_snapshot(), which polled, becomes an asyncnext()that waits for the next roster and returnsNoneonce the room is gone.- FFI and web
joinCalltake an optionalapplication_slot_id(m.call#{application_slot_id},roomwhen omitted) instead ofslot_idandapplication. - The room-wide slot is
{application}#room, as MSC4143 spells it, instead of{application}#ROOM(the 2025 Element Call format still respells it#ROOMon the wire);ROOM_SLOT_NAMEbecomesROOM_APPLICATION_SLOT_ID. CallEvent::KeyImported(webkey_imported) carries theidentitythe key was installed under.- A join refused because its slot is not open fails with
MatrixRtcFfiError::SlotClosedinstead ofInvalidInput. - A join's transport is one choice,
Advertised,Publishon a LiveKit focus, orReceiveOnly, replacingtransport,receive_onlyandcan_subscribein the FFI and web join params. - The library keeps a joined session alive and performs its key rotations at their deadline itself;
heartbeat(),flushDueKeyRotation()/flush_due_key_rotation,keyRotationDueAtMs()/key_rotation_due_at_msandHEARTBEAT_INTERVAL_MSare gone, andLiveKitCallOptions::heartbeat_intervalbecomeskeep_alive_interval_ms(default 10 s). RtcClient::roomspawns the room's feeds onmatrix_rtc_core::executorand returns the room alone (RoomRunsis gone); natively it must be called within a tokio runtime, andLiveKitCall::joinno longer needs aLocalSet.- The session managers are replaced by client → room → session (
RtcClient→RtcRoom→RtcSession/RtcCall, likewise on the FFI and wasm), and the core's manager by the per-roomBaseRtcRoom. matrix_rtc_livekit::Call,CallOptionsandCallErrorare renamedLiveKitCall,LiveKitCallOptionsandLiveKitCallError, so they no longer read as the call crate'sRtcCall.compat,feederandtransportsmove frommatrix-rtc-bridgetomatrix-rtc-call, and what remains is renamedmatrix-rtc-matrix-sdk, with nomatrix-sdkfeature; itsSdkBackendbecomesSdkMatrixBackend.- One
MatrixBackendtrait (matrix-rtc-core) replacesRtcCommandSenderand every inbound method on the managers and bindings (set_current_sticky_state/set_current_membership,on_room_slots_received,on_room_members_received,on_room_encryption_received,receive_encryption_key/receive_legacy_encryption_key,on_room_timeline_events,on_event_redacted,on_relations_received). A host implements the trait — the sends, a per-room subscription delivering the room's complete current sets, a to-device subscription,/relations, the OpenID token andGET /rtc/transports— and opens rooms throughRtcClient::room; the feeder (matrix_rtc_call::feeder) does the feeding, including the pre-2026 compatibility funnels, for every host. Gone with it:CommandSenderCallback,OpenIdTokenProvider,OpenIdTokenSource,SdkCommandSender,run_membership_bridge,discover_livekit_transport, and the typed inbound records (StickyEvent,SlotEvent,FfiTimelineEvent,FfiReceivedEncryptionKey,RawMemberEvent, …). send_delayed_eventtakes an optional state key;send_delayed_state_eventis folded into it.- The compatibility mode is given when a room is opened, not on
join; the FFI and wasm join params no longer takeuser_id/device_id(the backend knows them) and theirtransportis optional — the library picks fromrtc_transports()when it is absent. LiveKitCall::jointakesLiveKitCallOptions::livekit_transportas an override instead oflivekit_service_url_fallback;connect_media_sessionno longer takes a token provider.- Joining a slot the room's state says is closed (or that is absent) fails with
JoinError::SlotClosed; nothing opens a slot implicitly —open_slotis the room admin's, or the room's initial state. - Call features (reactions, raised hand, MSC4075 notify) moved from
matrix-rtc-coreto the newmatrix-rtc-callcrate, joined withRtcRoom::join_callandCallJoinOptions. CallMembershipEventis renamedRtcMembershipEvent, andtry_into_call_membership_eventis renamedtry_into_membership_event.JoinSessionParams.applicationandJoinedMembership.applicationare the whole MSC4143ApplicationInfo, and the wasm roster serializes it as an object.BaseRtcRoom::joinreturns the event id of the membership it sent, andRtcSession::membership_event_idreads the current one.- The feeder feeds any
ApplicationIntake, subscribing to the event types it names. - The feeder moves from
matrix_rtc_call::feedertomatrix_rtc_core::feeder, reading a room through anIngestDialect. JoinSessionParamsno longer takes the user and device, which are the backend's:JoinSessionParams::application("org.example.board").slot("planning")composes the slot id ({application}#roomwithout.slot), and every other field is a named setter (.transport(..), required,.member_id(..),.keep_alive_interval_ms(..), …);JoinSessionParams::newandwith_transport_intentare gone. The call layer's options read the same way:CallJoinOptions::new()joinsm.call#roomand.slot("standup")names another,JoinOptions::application(..).slot(..)for any application;JoinOptions::newandCallJoinOptions::new(slot_id)are gone.- The pre-2026 membership formats move from
matrix_rtc_call::compattomatrix_rtc_core::compat, andElementCallCompat { Off, StickyEvents, StateEvents }becomesMembershipFormat { Current, Sticky2025, RoomState }on every host (FfiMembershipFormat, the wasmformat: "current" | "sticky_2025" | "room_state"room option,LiveKitCallOptions::format,RoomOptions::format, the load test's--format). WasmRtcSession, the bare single-session wasm API, is removed;WasmRtcClientover aMatrixBackendHostis the one entry point, andjoinCallrings like every other host.
Added
- A call leaves on its own when its slot closes (
leave_reasonslot_closed), and every ending — leave, slot close, room close — ends its media and closes its SFU connections on one path — at once on a leave, even one that fails to send — reported byRtcSession::subscribe_ended/ FFIRtcCall.waitForEnd(). matrix_rtc_transport::pool::MediaPool, the MSC4195 multi-SFU connection pool extracted from the call engine, reports a session's media permember_idfor any application on the core.matrix_rtc_call_sdk::attach_mediaandattach_livekitattach media to a joined call, now the one wiring behind the FFI and web media sessions andLiveKitCall.- A room still waiting on its first deliveries logs which
RoomSinkcalls are missing every 5 s, so a host that never delivers one sees whyroom()has not returned. BaseRtcClientopens a core room that subscribes through the backend and feeds itself, so a host of the core alone feeds and ticks nothing.- A core room opens in a
MembershipFormat(BaseRtcClient::room(room_id, RoomOptions)) and renders its joins in it, so a host of the core alone can join in the 2025 sticky or the pre-sticky room-state format. BaseRtcRoom::joinstarts the slot's upkeep (keep-alive, sticky refresh, key rotations at their deadline) until the leave, so a host on the core alone ticks nothing either;upkeep_abort_handlestops it for an owner that drops the join without leaving.BaseRtcRoom::add_membership_listener: synchronous notice of every change to a slot's joined memberships, for applications built on the core.ApplicationIntakeandRelationsRequest, how a host feeds an application beyond membership.BaseRtcRoom::backend(), and a coretestingfeature exposingMockBackend, whose sinks a test delivers sets into.- Reactions, the raised hand and the
/relationsbackfill on every host (FFI, wasm,LiveKitCall), since the feeder does the work once. matrix_rtc_call::transports::choose: the library's transport choice from the homeserver'sGET /rtc/transports.SdkMatrixBackenddiscovers transports through matrix-sdk's cacheddiscover_rtc_transports, falling back to the well-knownrtc_foci; a failed request is an error rather than "none advertised".- A member event carrying both
msc4354_sticky_keyandsticky_key(as seen on the wire) now parses; it was dropped as a duplicate field.
Fixed
- The web
onEventcallback now receiveskey_importedfor keys that arrived beforeconnectMedia, andLiveKitCall::joinleaves the slot on any media attach failure, not only a refused SFU. - In the 2025 Element Call format the room-wide slot is sent and read as
m.call#ROOM, including in the token request, so we share a session and an SFU room with Element Call again. - The media engine's first event subscriber now receives events emitted before it subscribed, so
KeyImportedfor keys replayed during connect is no longer lost.
v0.4.0-rc.1
Added
- The call tile roster:
MediaSession::next_roster()/next_local_state()
publish one ranked, dampedCallTileper renderable stream, with thresholds
configurable viastability. - A tile's kind is a
TileKind—PersonorScreenShare— rather than a stream
kind:TileId.kind,CallTile.kindand theirFfiTileKindmirrors. The stream a
tile draws isTileKind::video_stream(); a microphone is never a tile. MediaSession::receive_stats_for(streams)(andCallEngine::receive_stats_for)
reads receive counters for a set of(member_id, kind)in one call, awaiting
the transports concurrently: one round trip per sample for a host that polls
the tiles it draws, instead of one per stream.
Breaking
- MSC4354 sticky events now come from upstream matrix-rust-sdk and are always on with the
matrix-sdkfeature; theexperimental-stickyfeature,STICKY_EVENTS_SUPPORTEDandCallError::StickyEventsUnsupportedare gone.
Removed
FfiCallEvent::ActiveSpeakers. Who is speaking isFfiCallTile::speaking, damped
into the roster's order; the event was the noisiest thing on the stream and no
host drew from it. The event stream now carries one-shots and diagnostics only —
seeMediaSession::next_event's docs. wasm keeps its ownactive_speakers.
Drive audio playback from the tile roster'sorder, not fromStreamStarted;
participants()is a diagnostics pull, not something to re-read per event.
v0.3.0-rc.2
Added
PublishOptions::mutedpublishes a track already muted, for a lobby that
joins muted; also onFfiPublishOptions.
Fixed
- Our own publications now land on the roster when they are made before our
membership does, mute state included (#50).
v0.3.0-rc.1
Project
- The project is now dual licensed by Element Creations Ltd: AGPL-3.0-only
OR the Element Commercial License (AGPL-3.0-only OR LicenseRef-Element-Commercial).
Breaking
Hosts implementing the FFI/WASM command sender must update. All of these are
compile errors, not silent behaviour changes.
-
The generated bindings have their release names. Kotlin moves from
packageuniffi.matrix_rtc_ffitoorg.matrix.rtc(the package of the
hand-writtenMatrixRtc/RtcLogginghelpers, so one import prefix covers
the whole API); the Swift module isMatrixRtcand the C module inside the
xcframeworkMatrixRtcFFI(wasmatrix_rtc_ffiFFI). The Android module now
declares JNA as anapidependency (net.java.dev.jna:jna:5.19.1@aar), so a
consumer gets it from the POM, and drops theappcompatandjna-platform
dependencies nothing used. The Swift package moves to the repository root
(Package.swift+Sources/MatrixRtc) and is consumed as a remote package
at av*tag;mobile/ios/Package.swiftis gone. -
The Rust-driven path now depends on upstream
matrix-org/matrix-rust-sdk
by default; MSC4354 sticky events are behind a newexperimental-sticky
feature.matrix-rtc-bridgeandmatrix-rtc-livekit(featurematrix-sdk)
no longer pin theBillCarsonFr/matrix-rust-sdkfork. Against the stock SDK
they carry membership asorg.matrix.msc3401.call.memberroom state only, so
Call::joinacceptsElementCallCompat::StateEventsand returns the new
CallError::StickyEventsUnsupportedforOffandStickyEvents; a sticky
send reachingSdkCommandSenderdirectly fails with
CommandError::ClientRejected.experimental-stickycompiles the sticky
paths and needs the fork, selected with the.cargo/experimental-sticky.toml
overlay (scripts/cargo-sticky.sh,make check-sticky/clippy-sticky/
test-e2e-sticky), plusmatrix-sdk/unstable-msc4354and
matrix-sdk-ui/unstable-msc4354on the command line — a cargo feature cannot
forward to an SDK feature upstream lacks without breaking resolution for
everyone.matrix_rtc_bridge::STICKY_EVENTS_SUPPORTEDsays which build a
caller has.run_sticky_bridgeis renamedrun_membership_bridge. Consumers
must replicate the SDK's[patch]blocks as before; the set is now
upstream's (thetracingpins are gone, arumapatch is added). The FFI and
WASM bindings are unaffected: they never had matrix-sdk in their graph. -
In
StateEventsmode the MSC4075 notification goes out through
send_room_event/sendRoomEvent, not the sticky send. The pre-sticky
wire has no sticky map, so that generation's ring is an ordinary room event.
MemberEventRoutegains aRoomvariant for it (exhaustive matches break).
A host whose SDK has no MSC4354 support can now run aStateEventscall end
to end, notification included, without ever implementingsendStickyEvent. -
The command sender gains
sendRoomEvent(roomId, eventType, content)and
redactEvent(roomId, eventId, reason?). Kotlin/Swift: two new methods on
CommandSenderCallback; JS: two new methods on the client object handed to
setupCommandSender, resolving likesendStickyEventdoes ({event_id})
and to anything at all, respectively. They carry Element Call's reactions
(io.element.call.reaction) and the raised hand (anm.reaction
annotation, lowered by redacting it) — plain message-like sends, encrypted
in an encrypted room by the client SDK's ordinary send. Thematrix-js-sdk
host module implements both. -
Sticky events fed in should now carry their
event_id.StickyEvent
(FFI),StickyEventIn/RawMemberEventIn/LegacyStateMemberEventIn
(JS) and the FFIRawMemberEvent/LegacyStateMemberEventrecords gain an
optionalevent_id. Optional so existing hosts compile, but a member fed
without one cannot be reacted for and none of their reactions validate:
Element Call relates every reaction to the reacting member's membership
event.JoinedMembership/MembershipSnapshotgrow the matching
membership_event_id, which moves on every sticky refresh.
v0.3.0 sweep — deliberately batched into one release while there is still a
single integrator, rather than dripped out over several:
-
CallOptions::legacy_element_call: boolis now
CallOptions::element_call_compat: ElementCallCompat(Off/StickyEvents
/StateEvents). There are two pre-2026 Element Call generations to render for
now, and they disagree about the carrier of a membership rather than just its
fields, so they are mutually exclusive by construction — an enum makes the
impossible fourth state unrepresentable.LiveKitTransportConfiglikewise
gains atoken_endpoint: TokenEndpointfield. The load generator's
--legacy-element-callflag becomes--element-call-compat <off|sticky|state>,
andjoin_and_record'sLEGACY_ELEMENT_CALLnow takesstickyorstate
(any other value still meanssticky). -
sendStickyEventandsendStateEventnow return the event id.
Kotlin/Swift:String; JS: resolve to matrix-js-sdk's{event_id}(a bare
string and{eventId}are accepted too), and resolving to anything else is
now a failed send rather than a silent success.The id is what MSC4075 needs: a call notification must carry an
m.reference
relation to the membership event that justifies it. It is not optional because
every Matrix send responds with one —PUT /rooms/{id}/send/{type}/{txn}and
PUT /rooms/{id}/state/{type}/{key}both do — so an implementation that
cannot produce it is broken, and failing loudly beats a call that joins fine
and quietly never rings.sendStateEventis included because inStateEvents
compat mode the membership goes out through it; nothing reads the id of a
slot event.The alternative, recovering the id from our own membership echoing back
through sync (what matrix-js-sdk does), puts a full sync round trip in front
of the ring. -
sendToDeviceMessagenow takes a recipient list and returns a result per
recipient, replacing the per-device call whose only failure channel was an
exception. That exception landed inside the core's loop over members, so one
undeliverable recipient silenced every member after it; and because the core
had no way to learn who was actually served, it recorded everyone as holding
the key and never retried. Now: returnFfiToDeviceDelivery { userId, deviceId, error }per recipient — a recipient you report as delivered is
never re-sent to, one you mark failed or omit is retried on the next rollout.
ReserveErrfor "the batch could not be attempted at all". This also removes
a loop and several FFI crossings, and maps onto matrix-rust-sdk's own
recipient-listsendToDeviceMessage. -
restartDelayedEvent/cancelDelayedEventtakedelayId, not
eventId. The value was always the MSC4140 delay id; the parameter name
said otherwise, and transposing the two fails silently, surfacing minutes
later as the dead man's switch retiring a live membership. Renamed everywhere,
includingsendDelayedEvent's documented return. -
CommandSenderCallbackis async, and so is every manager-handle method
that touches the core. In Kotlin they aresuspend funs; in Swift,async.
A synchronous callback forced hosts to bridge to their async Matrix client
themselves —runBlockingon a dedicated dispatcher on Android — while every
corresponding matrix-rust-sdk call is already async.setCommandSendernow takes the interface directly rather than a boxed one
(uniffiwith_foreign), andCommandSenderErrorgained aDisplayimpl,
which uniffi requires of a foreign-trait error type.This was blocked until now by a subtlety worth recording: uniffi's async
exports must beSend, whilematrix-rtc-core's command traits were
?Sendon every target to accommodate WASM's JS-backed futures. They are now
Sendeverywhere exceptwasm32, via acfg_attrpair that every
implementation of those traits must carry. A consequence is that
matrix-rtc-wasmno longer compiles for a host target — its crate body is
gated towasm32and aClippy (wasm32)CI job type-checks it instead.Internally this removed the blocking bridge entirely: no
block_on, and the
keep-alive is a spawned task rather than a thread per session (it needed a
thread only because its future held astd::sync::MutexGuardand so was not
Send). -
One way in for membership:
setCurrentStickyState(roomId, events). It
carries the room's complete current sticky state and replaces what the
core held — a member absent from it is gone, and an empty list clears the
room.initialStickyForRoom,onStickyEventsSnapshotReceived,
stickyUpdateForRoomandonStickyEventsUpdateReceivedare all gone, along
with theStickyEventUpdaterecord.A delta bought nothing: the core discarded the
previoushalf of an update,
flattened every removal to a plain leave, and an explicit leave arrives inside
the current state anyway as a leave-shaped sticky replacing the join under the
same key. Meanwhilematrix-sdk-ffihands hosts a full snapshot on every
change (it collapses the SDK's delta before it crosses the boundary), so the
current state is the shape hosts already have — and the core now does the
diffing once instead of each host doing it. Feeding a shrunken set is how an
expired membership departs. -
RtcSessionHandleis gone. Itsleave()could only ever return an error,
and a session created through it was invisible toRtcSessionManagerHandle,
so nothing else could act on it. Its one unique capability — observing the
roster — is now on the manager. Drive sessions through the manager handle. -
FfiAudioFrame.datais aByteArrayof little-endianint16, not a
List<Short>. uni...