From 6bd6e3f953ee99fcf400097e097a80399c7b841c Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Sun, 4 Oct 2026 12:51:35 +0200 Subject: [PATCH] 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 --- sdk/MemberMediaPlan.md | 25 +++++++++++++------------ sdk/SdkArchitecture.md | 27 +++++++++++++++++++++------ sdk/index.ts | 2 +- sdk/src/api.ts | 27 +++++++++++++++++++++++---- sdk/src/errors.ts | 2 +- sdk/src/session/Connection.ts | 6 +++--- sdk/src/session/joinJsSdkSession.ts | 5 +++-- 7 files changed, 65 insertions(+), 29 deletions(-) diff --git a/sdk/MemberMediaPlan.md b/sdk/MemberMediaPlan.md index d006bdec9..5b3c11c6a 100644 --- a/sdk/MemberMediaPlan.md +++ b/sdk/MemberMediaPlan.md @@ -383,8 +383,7 @@ No behaviour change; one PR of renames and type narrowing. ## Changed behaviour -What a host sees differently, per slice. The first block is implemented; the -rest follows the slices above and is marked as planned. +What a host sees differently, per slice. All four blocks are implemented. ### `tracks$` and `isActive$` (this plan, implemented) @@ -416,19 +415,20 @@ Differences in what the values mean: before had an order. - 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 - camera track; it rejects, instead of resolving `undefined`, when there is no - camera track. + camera track, so there is always a track to call it on; it still resolves + `undefined` where the facing mode is unknown. - Input device and processor changes are calls on the track. The SDK no longer reacts to `audioInputDeviceId$`, `videoInputDeviceId$` or `videoProcessor$`; 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 `setMicrophoneEnabled(false)` does today. -### Slice 2, `publish` and `unpublish` (planned) +### Slice 2, `publish` and `unpublish` - Screen sharing is `publish({ source: "screenShare" })` and `unpublish(id)`. 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.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 `CallMembership` member outside the narrowed set must import the js-sdk type itself; the object is unchanged. -- `UnknownCallError` is renamed; `index.ts` stops exporting it as - `UnknownRTCError`. Error codes do not change. -- `applicationData` is forwarded whole once the js-sdk takes it. Until then the - behaviour is as today: only `m.call.intent` reaches the membership. +- `UnknownCallError` is `UnknownRTCError` in the SDK too, not only in the + export. Error codes do not change. The two `Livekit*` errors are left to + slice 11 of `SdkMigration.md`, which renames them on both sides at once. +- `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. diff --git a/sdk/SdkArchitecture.md b/sdk/SdkArchitecture.md index 834cc9950..7b3cc96dc 100644 --- a/sdk/SdkArchitecture.md +++ b/sdk/SdkArchitecture.md @@ -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 | | 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" | | 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" | @@ -334,7 +334,23 @@ export interface DataMessage { ### Members ```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 { local: boolean; @@ -346,7 +362,7 @@ export interface RTCMember { id: string; userId: string; deviceId: string; - membership$: Behavior; + membership$: Behavior; /** * Matrix room state rather than MatrixRTC, but the SDK already holds the * 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 join; creating the tracks earlier, for a lobby preview, is still missing. -- **`applicationData` after the join.** The option is read once. The client - still updates `m.call.intent` from the camera state, as Element Call did; that - moves to the host once the data can be updated. +- **`applicationData` after the join.** The option is read once; there is no + way to update it on a live membership yet. - **Raw local state.** The local member's state machine (`LocalMemberState`) stays internal; `status$`, `disconnectReason$` and `fatalError$` are its public view. A developer panel may want it as `LocalRTCMember.state$`. diff --git a/sdk/index.ts b/sdk/index.ts index d3eab3b0d..9c81c5675 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -49,7 +49,7 @@ export { NoMatrix2AuthorizationService, PeerConnectionTimeoutError, SFURoomCreationRestrictedError, - UnknownCallError as UnknownRTCError, + UnknownRTCError, } from "./src/errors"; export * from "./src/api"; diff --git a/sdk/src/api.ts b/sdk/src/api.ts index 1cc7ab2a8..be0035339 100644 --- a/sdk/src/api.ts +++ b/sdk/src/api.ts @@ -48,9 +48,10 @@ export interface MatrixRTCClientOptions { /** The application's slot in the room. Default `ROOM`. */ slot?: string; /** - * 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` - * (`"audio"` or `"video"`); anything else is dropped until it can. + * Whatever the application wants to say about itself in the membership, + * under namespaced keys. Opaque to the SDK, apart from `m.call.intent` + * (`"audio"` or `"video"`), which the membership format carries as a + * field of its own. */ applicationData?: Record; /** Session timings the host has configured; the defaults otherwise. */ @@ -211,13 +212,31 @@ export type ResolvedTransport = // --------------------------------------------------------------------------- // 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 { local: boolean; /** The identity the media backend knows this member by. */ id: string; userId: string; deviceId: string; - membership$: Behavior; + membership$: Behavior; displayName$: Behavior; avatarUrl$: Behavior; /** Which transport this member is on; undefined when the membership has none. */ diff --git a/sdk/src/errors.ts b/sdk/src/errors.ts index 0d3d03198..332963cc8 100644 --- a/sdk/src/errors.ts +++ b/sdk/src/errors.ts @@ -80,7 +80,7 @@ export class MembershipManagerError extends MatrixRTCError { } } -export class UnknownCallError extends MatrixRTCError { +export class UnknownRTCError extends MatrixRTCError { public constructor(cause: Error) { super( cause.message || "Unknown error", diff --git a/sdk/src/session/Connection.ts b/sdk/src/session/Connection.ts index cb02dd079..db881674b 100644 --- a/sdk/src/session/Connection.ts +++ b/sdk/src/session/Connection.ts @@ -40,7 +40,7 @@ import { LivekitConnectionError, PeerConnectionTimeoutError, SFURoomCreationRestrictedError, - UnknownCallError, + UnknownRTCError, } from "../errors"; export interface ConnectionOpts { @@ -381,8 +381,8 @@ export class Connection { error instanceof MatrixRTCError ? error : error instanceof Error - ? new UnknownCallError(error) - : new UnknownCallError(new Error(`${error}`)), + ? new UnknownRTCError(error) + : new UnknownRTCError(new Error(`${error}`)), ); // Its okay to ignore the throw. The error is part of the state. throw error; diff --git a/sdk/src/session/joinJsSdkSession.ts b/sdk/src/session/joinJsSdkSession.ts index 4b6cf0d27..a2959504e 100644 --- a/sdk/src/session/joinJsSdkSession.ts +++ b/sdk/src/session/joinJsSdkSession.ts @@ -47,8 +47,8 @@ export function joinJsSdkSession( delayedLeave, }: Options, ): void { - // The one piece of application data the js-sdk can put in a membership - const intent = applicationData?.["m.call.intent"]; + // The membership format carries the intent as a field of its own + const { "m.call.intent": intent, ...rest } = applicationData ?? {}; const callIntent = typeof intent === "string" ? (intent as RTCCallIntent) : undefined; // 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], { notificationType: sendNotificationType, callIntent, + applicationData: rest, manageMediaKeys: encryptMedia, delayedLeaveEventRestartMs: delayedLeave.restart_ms, delayedLeaveEventDelayMs: delayedLeave.delay_ms,