Take the word call out of the member API

membership$ is typed as RTCMembership, the subset of the js-sdk CallMembership that hosts read. UnknownCallError is UnknownRTCError throughout, not only in the export. applicationData reaches the membership whole, since the js-sdk takes it next to the intent. The plan records the three slices as implemented.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Timo K.
2026-10-08 13:53:57 +02:00
co-authored by Claude Fable 5.1
parent b9f79be106
commit 6bd6e3f953
7 changed files with 65 additions and 29 deletions
+13 -12
View File
@@ -383,8 +383,7 @@ No behaviour change; one PR of renames and type narrowing.
## Changed behaviour ## Changed behaviour
What a host sees differently, per slice. The first block is implemented; the What a host sees differently, per slice. All four blocks are implemented.
rest follows the slices above and is marked as planned.
### `tracks$` and `isActive$` (this plan, implemented) ### `tracks$` and `isActive$` (this plan, implemented)
@@ -416,19 +415,20 @@ Differences in what the values mean:
before had an order. before had an order.
- The harness labels audio elements with `data-active`. Test ids are unchanged. - The harness labels audio elements with `data-active`. Test ids are unchanged.
### Slice 1, local track controls (planned) ### Slice 1, local track controls
- `switchCamera()` on the local media becomes `switchFacingMode()` on the local - `switchCamera()` on the local media becomes `switchFacingMode()` on the local
camera track; it rejects, instead of resolving `undefined`, when there is no camera track, so there is always a track to call it on; it still resolves
camera track. `undefined` where the facing mode is unknown.
- Input device and processor changes are calls on the track. The SDK no longer - Input device and processor changes are calls on the track. The SDK no longer
reacts to `audioInputDeviceId$`, `videoInputDeviceId$` or `videoProcessor$`; reacts to `audioInputDeviceId$`, `videoInputDeviceId$` or `videoProcessor$`;
a host that changed a setting and expected the SDK to follow now calls a host that changed a setting and expected the SDK to follow now calls
`setDevice` or `setProcessor` itself. `setDevice` or `setProcessor` itself. The initial device and processor travel
with the publish request instead.
- `setEnabled(false)` on a local track mutes it and keeps the publication, as - `setEnabled(false)` on a local track mutes it and keeps the publication, as
`setMicrophoneEnabled(false)` does today. `setMicrophoneEnabled(false)` does today.
### Slice 2, `publish` and `unpublish` (planned) ### Slice 2, `publish` and `unpublish`
- Screen sharing is `publish({ source: "screenShare" })` and `unpublish(id)`. - Screen sharing is `publish({ source: "screenShare" })` and `unpublish(id)`.
A capture failure rejects the promise; there is no `screenShareError$` to A capture failure rejects the promise; there is no `screenShareError$` to
@@ -444,12 +444,13 @@ false, cameraEnabled: false` before.
- Capture settings travel with each request. A host that set - Capture settings travel with each request. A host that set
`capture.screenShare` once sets it on each screen share publish instead. `capture.screenShare` once sets it on each screen share publish instead.
### Slice 3, names (planned) ### Slice 3, names
- `membership$` is typed as `RTCMembership`. A host that reached for a - `membership$` is typed as `RTCMembership`. A host that reached for a
`CallMembership` member outside the narrowed set must import the js-sdk type `CallMembership` member outside the narrowed set must import the js-sdk type
itself; the object is unchanged. itself; the object is unchanged.
- `UnknownCallError` is renamed; `index.ts` stops exporting it as - `UnknownCallError` is `UnknownRTCError` in the SDK too, not only in the
`UnknownRTCError`. Error codes do not change. export. Error codes do not change. The two `Livekit*` errors are left to
- `applicationData` is forwarded whole once the js-sdk takes it. Until then the slice 11 of `SdkMigration.md`, which renames them on both sides at once.
behaviour is as today: only `m.call.intent` reaches the membership. - `applicationData` is forwarded whole: the js-sdk takes it next to
`m.call.intent`, which the membership format carries as a field of its own.
+21 -6
View File
@@ -115,7 +115,7 @@ without anyone hearing it. `leave()` pauses the upstream again and sends the lea
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| client | a `MatrixRTCClient`: one room, one slot, one local member | `MatrixClient` is always written out | | client | a `MatrixRTCClient`: one room, one slot, one local member | `MatrixClient` is always written out |
| session | the MatrixRTC session in the room: the set of memberships. Only used for the js-sdk object, which stays inside the SDK as `JsSdkRTCSession` | MSC4143 | | session | the MatrixRTC session in the room: the set of memberships. Only used for the js-sdk object, which stays inside the SDK as `JsSdkRTCSession` | MSC4143 |
| membership | one device's MatrixRTC state event, the js-sdk `CallMembership`. The js-sdk name is kept until it renames it | | | 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" | | member | an `RTCMember`: a membership plus what the SDK derives from it (display name, transport, media) | Element Call's "matrixLivekitMember" |
| participant | a LiveKit participant, the media side of a member. Never leaves the SDK | | | participant | a LiveKit participant, the media side of a member. Never leaves the SDK | |
| transport | where media is exchanged: a LiveKit service url today, described by `TransportMetadata` | MSC4143 "focus" | | transport | where media is exchanged: a LiveKit service url today, described by `TransportMetadata` | MSC4143 "focus" |
@@ -334,7 +334,23 @@ export interface DataMessage {
### Members ### Members
```ts ```ts
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc"; /**
* What a membership says, as hosts read it. The js-sdk `CallMembership` is
* behind it; anything beyond this is the js-sdk's API, not the SDK's.
*/
export type RTCMembership = Pick<
CallMembership,
| "userId"
| "deviceId"
| "memberId"
| "rtcBackendIdentity"
| "application"
| "applicationData"
| "getTransport"
| "transports"
| "createdTs"
| "getAbsoluteExpiry"
>;
export interface RTCMember { export interface RTCMember {
local: boolean; local: boolean;
@@ -346,7 +362,7 @@ export interface RTCMember {
id: string; id: string;
userId: string; userId: string;
deviceId: string; deviceId: string;
membership$: Behavior<CallMembership>; membership$: Behavior<RTCMembership>;
/** /**
* Matrix room state rather than MatrixRTC, but the SDK already holds the * Matrix room state rather than MatrixRTC, but the SDK already holds the
* room and every consumer wants them next to the membership. Disambiguated * room and every consumer wants them next to the membership. Disambiguated
@@ -658,9 +674,8 @@ public API only.
- **Publishing before `join()`.** The initial requests are published at the - **Publishing before `join()`.** The initial requests are published at the
join; creating the tracks earlier, for a lobby preview, is still missing. join; creating the tracks earlier, for a lobby preview, is still missing.
- **`applicationData` after the join.** The option is read once. The client - **`applicationData` after the join.** The option is read once; there is no
still updates `m.call.intent` from the camera state, as Element Call did; that way to update it on a live membership yet.
moves to the host once the data can be updated.
- **Raw local state.** The local member's state machine (`LocalMemberState`) stays - **Raw local state.** The local member's state machine (`LocalMemberState`) stays
internal; `status$`, `disconnectReason$` and `fatalError$` are its public view. internal; `status$`, `disconnectReason$` and `fatalError$` are its public view.
A developer panel may want it as `LocalRTCMember.state$`. A developer panel may want it as `LocalRTCMember.state$`.
+1 -1
View File
@@ -49,7 +49,7 @@ export {
NoMatrix2AuthorizationService, NoMatrix2AuthorizationService,
PeerConnectionTimeoutError, PeerConnectionTimeoutError,
SFURoomCreationRestrictedError, SFURoomCreationRestrictedError,
UnknownCallError as UnknownRTCError, UnknownRTCError,
} from "./src/errors"; } from "./src/errors";
export * from "./src/api"; export * from "./src/api";
+23 -4
View File
@@ -48,9 +48,10 @@ export interface MatrixRTCClientOptions {
/** The application's slot in the room. Default `ROOM`. */ /** The application's slot in the room. Default `ROOM`. */
slot?: string; slot?: string;
/** /**
* Whatever the application wants to say about itself in the membership. * Whatever the application wants to say about itself in the membership,
* Opaque to the SDK. Today the js-sdk carries one key, `m.call.intent` * under namespaced keys. Opaque to the SDK, apart from `m.call.intent`
* (`"audio"` or `"video"`); anything else is dropped until it can. * (`"audio"` or `"video"`), which the membership format carries as a
* field of its own.
*/ */
applicationData?: Record<string, unknown>; applicationData?: Record<string, unknown>;
/** Session timings the host has configured; the defaults otherwise. */ /** Session timings the host has configured; the defaults otherwise. */
@@ -211,13 +212,31 @@ export type ResolvedTransport =
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Members // Members
/**
* What a membership says, as hosts read it. The js-sdk `CallMembership` is
* behind it; anything beyond this is the js-sdk's API, not the SDK's.
*/
export type RTCMembership = Pick<
CallMembership,
| "userId"
| "deviceId"
| "memberId"
| "rtcBackendIdentity"
| "application"
| "applicationData"
| "getTransport"
| "transports"
| "createdTs"
| "getAbsoluteExpiry"
>;
export interface RTCMember { export interface RTCMember {
local: boolean; local: boolean;
/** The identity the media backend knows this member by. */ /** The identity the media backend knows this member by. */
id: string; id: string;
userId: string; userId: string;
deviceId: string; deviceId: string;
membership$: Behavior<CallMembership>; membership$: Behavior<RTCMembership>;
displayName$: Behavior<string>; displayName$: Behavior<string>;
avatarUrl$: Behavior<string | undefined>; avatarUrl$: Behavior<string | undefined>;
/** Which transport this member is on; undefined when the membership has none. */ /** Which transport this member is on; undefined when the membership has none. */
+1 -1
View File
@@ -80,7 +80,7 @@ export class MembershipManagerError extends MatrixRTCError {
} }
} }
export class UnknownCallError extends MatrixRTCError { export class UnknownRTCError extends MatrixRTCError {
public constructor(cause: Error) { public constructor(cause: Error) {
super( super(
cause.message || "Unknown error", cause.message || "Unknown error",
+3 -3
View File
@@ -40,7 +40,7 @@ import {
LivekitConnectionError, LivekitConnectionError,
PeerConnectionTimeoutError, PeerConnectionTimeoutError,
SFURoomCreationRestrictedError, SFURoomCreationRestrictedError,
UnknownCallError, UnknownRTCError,
} from "../errors"; } from "../errors";
export interface ConnectionOpts { export interface ConnectionOpts {
@@ -381,8 +381,8 @@ export class Connection {
error instanceof MatrixRTCError error instanceof MatrixRTCError
? error ? error
: error instanceof Error : error instanceof Error
? new UnknownCallError(error) ? new UnknownRTCError(error)
: new UnknownCallError(new Error(`${error}`)), : new UnknownRTCError(new Error(`${error}`)),
); );
// Its okay to ignore the throw. The error is part of the state. // Its okay to ignore the throw. The error is part of the state.
throw error; throw error;
+3 -2
View File
@@ -47,8 +47,8 @@ export function joinJsSdkSession(
delayedLeave, delayedLeave,
}: Options, }: Options,
): void { ): void {
// The one piece of application data the js-sdk can put in a membership // The membership format carries the intent as a field of its own
const intent = applicationData?.["m.call.intent"]; const { "m.call.intent": intent, ...rest } = applicationData ?? {};
const callIntent = const callIntent =
typeof intent === "string" ? (intent as RTCCallIntent) : undefined; typeof intent === "string" ? (intent as RTCCallIntent) : undefined;
// Give up on the network as soon as either the sync has been down for the // Give up on the network as soon as either the sync has been down for the
@@ -61,6 +61,7 @@ export function joinJsSdkSession(
session.joinRTCSession(ownMembershipIdentity, [transport], { session.joinRTCSession(ownMembershipIdentity, [transport], {
notificationType: sendNotificationType, notificationType: sendNotificationType,
callIntent, callIntent,
applicationData: rest,
manageMediaKeys: encryptMedia, manageMediaKeys: encryptMedia,
delayedLeaveEventRestartMs: delayedLeave.restart_ms, delayedLeaveEventRestartMs: delayedLeave.restart_ms,
delayedLeaveEventDelayMs: delayedLeave.delay_ms, delayedLeaveEventDelayMs: delayedLeave.delay_ms,