[SDK] split the clint object into -> slot and participation

This commit is contained in:
Timo K.
2026-10-08 13:53:57 +02:00
parent 9cc9eb0889
commit dc983e5493
17 changed files with 653 additions and 415 deletions
+118 -90
View File
@@ -6,13 +6,12 @@ Please see LICENSE in the repository root for full details.
*/
/**
* The public types of the SDK: the client, its members and transports. What a
* member sends is in `media-api.ts`. `createMatrixRTCClient` in
* `MatrixRTCClient.ts` is the only way to obtain an implementation of them.
* The public types of the SDK: the slot, the participation, their members and
* transports. What a member sends is in `media-api.ts`. `createRTCSlot` in
* `RTCSlot.ts` is the only way to obtain an implementation of them.
*/
import {
type CallMembership,
type RTCNotificationType,
type Transport,
} from "matrix-js-sdk/lib/matrixrtc";
@@ -20,12 +19,12 @@ import { type Observable } from "rxjs";
import { type Behavior } from "./reactive/Behavior";
import {
type AudioMediaTrack,
type EncryptionError,
type LocalAudioMediaTrack,
type LocalMemberMedia,
type LocalVideoMediaTrack,
type MediaSource,
type MemberMedia,
type PublishRequest,
type VideoMediaTrack,
} from "./media-api";
import { type MediaBackendFactory } from "./media-backend/api";
import { type EncryptionSystem } from "./encryption";
@@ -34,26 +33,72 @@ import {
type MediaQuality,
type SessionTimings,
} from "./config";
import { type MatrixRTCError } from "./errors";
export { MatrixRTCError } from "./errors";
// ---------------------------------------------------------------------------
// MatrixRTCClient
// Slot
export interface MatrixRTCClientOptions {
encryptionSystem: EncryptionSystem;
/** Resolved by the host; the SDK reads neither config.json nor settings. */
export interface RTCSlotOptions {
/** The application the slot belongs to, as named in the membership. Default `m.call`. */
application?: string;
/** The application's slot in the room. Default `ROOM`. */
id?: string;
/** How memberships and keys in this slot are written. Resolved by the host. */
matrixRTCMode: MatrixRTCMode;
/** Published at the join; `publish` on the local member adds to it from then on. */
encryptionSystem: EncryptionSystem;
}
/**
* The namespace in a room that memberships join into. It exists while the
* room does, with nobody in it too. The room has to be the one the client's
* sync loop maintains (`client.getRoom(roomId)`, once the join has synced):
* the detached copy `joinRoom` returns for a room joined just now never
* receives the state the members are read from.
*/
export interface RTCSlot {
readonly roomId: string;
readonly application: string;
readonly id: string;
/** From the slot state event; undefined while the room has none. */
status$: Behavior<"open" | "closed" | undefined>;
/**
* Everyone in the slot, one object per membership, kept for as long as the
* membership is. Our own membership is in here too, with `local: true`,
* once the room has seen it. No media: that is the participation's.
*/
members$: Behavior<RTCMember[]>;
/** The participation we are in, null between `leave()` and the next `join()`. */
participation$: Behavior<RTCParticipation | null>;
/**
* Joins the slot and returns the participation that lasts until its
* `leave()`. Throws while `participation$` is not null: a slot has one local
* member.
*/
join(options: RTCParticipationOptions): RTCParticipation; // Maybe MediaSession,
}
// ---------------------------------------------------------------------------
// Participation
export interface RTCParticipationOptions {
/**
* Published at the join; `publish` on the participation adds to it from then
* on. Kept as an option, rather than only the method, because the LiveKit
* backend reads the initial requests to build the room's default capture
* options before the first connection exists.
*/
publish: PublishRequest[];
/**
* LEGACY
*
* Conside already removing it. just skips code in js-sdk. But rust sdk will not have this feature.
* MSC4075 notification sent with the join. A parameter of the MatrixRTC
* join itself, so it is here even though it is named after calls; reacting
* to a notification (ringing, timeouts, declines) is the application's job.
*/
sendNotificationType?: RTCNotificationType;
/** The application the session belongs to, as named in the membership. Default `m.call`. */
application?: string;
/** The application's slot in the room. Default `ROOM`. */
slot?: string;
/**
* Whatever the application wants to say about itself in the membership,
* under namespaced keys. Opaque to the SDK, apart from `m.call.intent`
@@ -61,7 +106,7 @@ export interface MatrixRTCClientOptions {
* field of its own.
*/
applicationData?: Record<string, unknown>;
/** Session timings the host has configured; the defaults otherwise. */
/** Timings the host has configured; the defaults otherwise. */
timings?: Partial<SessionTimings>;
/** Limits on what is published; LiveKit's defaults otherwise. */
mediaQuality?: MediaQuality;
@@ -76,51 +121,58 @@ export interface MatrixRTCClientOptions {
backend?: MediaBackendFactory;
}
export type ConnectionStatus =
export type ParticipationStatus =
| "waitingForTransport"
| "connecting"
| "connected"
| "reconnecting"
| "disconnected";
| "disconnected"
/** `leave()` was called; the participation is over and stays so. */
| "left";
/** Why the client is not connected: the first failing of its three links. */
/** Why the participation is not connected: the first failing of its three links. */
export type DisconnectReason = "sync" | "membership" | "probablyLeft" | "media";
import { type MatrixRTCError } from "./errors";
export { MatrixRTCError } from "./errors";
/**
* A session in one room. The room has to be the one the client's sync loop
* maintains (`client.getRoom(roomId)`, once the join has synced): the
* detached copy `joinRoom` returns for a room joined just now never receives
* the state the members are read from.
*/
export interface MatrixRTCClient {
join(): void;
/** The span between our `join()` of a slot and our `leave()`. */
export interface RTCParticipation {
readonly slot: RTCSlot;
/** Sends the leave, tears the connections down and ends the participation. */
leave(): void;
/** Collapsed view of the local member's state machine. */
status$: Behavior<ConnectionStatus>;
status$: Behavior<ParticipationStatus>;
connected$: Behavior<boolean>;
reconnecting$: Behavior<boolean>;
/** Null while connected, the first failing link otherwise. */
disconnectReason$: Behavior<DisconnectReason | null>;
/** A transport, Matrix or connection error that stops the session. */
/** A transport, Matrix or connection error that stops the participation. */
fatalError$: Behavior<MatrixRTCError | null>;
/** Null until our own membership has been seen in the room. */
localMember$: Behavior<LocalRTCMember | null>;
/**
* The slot's other members, each with the media this participation carries
* for it. A call view needs nothing but this and `localMember$` to render.
*/
remoteMembers$: Behavior<RemoteRTCMember[]>;
/** `remoteMembers.length`, plus one for the local member once it exists. */
memberCount$: Behavior<number>;
/**
* Whether the session has grown large enough that MatrixRTC has stopped
* Publishes a source and resolves with its track, which also shows up in
* `localMember$.tracks$`. Works from the moment `join()` returns: before the
* transport is connected.
*/
publish(
request: PublishRequest,
): Promise<LocalAudioMediaTrack | LocalVideoMediaTrack>;
/** Removes one of our publications; a screen share takes its audio with it. */
unpublish(source: MediaSource): Promise<void>;
/**
* Whether the slot has grown large enough that MatrixRTC has stopped
* rotating the media encryption key.
*/
keyRotationSuppressed$: Behavior<boolean>;
/** Transports the session currently holds a live connection to. */
/** Transports the participation currently holds a live connection to. */
connectedTransports$: Behavior<TransportMetadata[]>;
/**
@@ -139,9 +191,9 @@ export interface MatrixRTCClient {
*/
sendData(topic: string, text: string): Promise<void>;
/**
* What remote members sent with `sendData`, on every transport the client
* is connected to. A message from an identity that is not a member is
* dropped, so a host only ever hears from attested members.
* What remote members sent with `sendData`, on every transport the
* participation is connected to. A message from an identity that is not a
* member is dropped, so a host only ever hears from attested members.
*/
data$: Observable<DataMessage>;
}
@@ -164,7 +216,7 @@ export interface DataMessage {
export interface TransportMetadata {
/** `"livekit"` today. */
type: string;
/** Stable key, unique per transport in the session: the raw transport, serialised with sorted keys. */
/** Stable key, unique per transport in the slot: the raw transport, serialised with sorted keys. */
id: string;
/** The transport object as it appears in the membership. */
raw: Transport;
@@ -190,63 +242,39 @@ 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"
>;
/** A membership in the slot, with what the SDK derives from it. No media. */
export interface RTCMember {
local: boolean;
/** The identity the media backend knows this member by. */
id: string;
/**
* The identity this member has on the media backend's transport: the
* participant identity the SFU sees, and the sender of its data packets. The
* SDK uses it to match what arrives on the transport back to the member; a
* host needs it only as a stable key per membership or as a debug label.
*
* Today it is read off the membership, where the js-sdk computes it:
* `${userId}:${deviceId}` before sticky events, a hash of `memberId` in
* Matrix 2.0 mode. Eventually the backend will derive it itself, so treat it
* as opaque.
*/
rtcBackendIdentity: string;
userId: string;
deviceId: string;
membership$: Behavior<RTCMembership>;
displayName$: Behavior<string>;
avatarUrl$: Behavior<string | undefined>;
/** The membership's own id; what the Matrix 2.0 identity is derived from. */
memberId: string;
/** Which transport this member is on; undefined when the membership has none. */
transport$: Behavior<TransportMetadata | undefined>;
/**
* The member's tracks, in publication order, once it has shown up on its
* transport; null until then ("waiting for media"). An entry stays the same
* object for as long as the same publication is behind it. Which track is
* which is in its `source`; `trackBySource$` picks one out.
*/
tracks$: Behavior<(AudioMediaTrack | VideoMediaTrack)[] | null>;
/** Emits when the SFU reports a key problem for this member. */
encryptionError$: Observable<EncryptionError>;
/** What the member's application says about itself, e.g. `m.call.intent`. */
applicationData$: Behavior<Record<string, unknown>>;
displayName$: Behavior<string>;
avatarUrl$: Behavior<string | undefined>;
}
export interface RemoteRTCMember extends RTCMember {
export interface RemoteRTCMember extends RTCMember, MemberMedia {
local: false;
}
export interface LocalRTCMember extends RTCMember {
/** Our member: the same as a remote one, with tracks that carry the controls. */
export interface LocalRTCMember extends RTCMember, LocalMemberMedia {
local: true;
tracks$: Behavior<(LocalAudioMediaTrack | LocalVideoMediaTrack)[] | null>;
/**
* Publishes a source and resolves with its track once it is in `tracks$`.
* Before the transport is connected the request is remembered and applied
* once it is. Rejects where the device could not be used, including the
* user closing the picker. One publication per source: publishing a source
* again unmutes it.
*/
publish(
request: PublishRequest,
): Promise<LocalAudioMediaTrack | LocalVideoMediaTrack>;
/** Removes one of our tracks; a screen share takes its audio with it. */
unpublish(id: string): Promise<void>;
}