sdk implementation

This commit is contained in:
Timo K.
2026-10-08 13:53:56 +02:00
parent 9910496b90
commit 1a999eaa54
31 changed files with 3012 additions and 345 deletions
+7 -251
View File
@@ -10,262 +10,18 @@ Please see LICENSE in the repository root for full details.
*
* MatrixRTC sessions with LiveKit media, without Element Call's UI: the call
* model Element Call's own view model is built on, for hosts that want to
* build a different one.
*
* Only the interface exists so far. `createRtcSession` returns an object that
* does nothing, so that the development harness in `sdk/dev` and its e2e test
* can be written against the contract before the implementation lands behind
* it. The design and the migration from Element Call's `CallViewModel` are in
* `sdk-plan.md` at the repository root.
* build a different one. The design and the migration from Element Call's
* `CallViewModel` are in `sdk-plan.md` at the repository root.
*/
import { type MatrixClient, type Room } from "matrix-js-sdk";
import {
type CallMembership,
type RTCCallIntent,
type RTCNotificationType,
type Transport,
} from "matrix-js-sdk/lib/matrixrtc";
import { type Track, type TrackProcessor } from "livekit-client";
import { type Observable } from "rxjs";
import { type Behavior } from "../src/state/Behavior";
import { type ObservableScope } from "../src/state/ObservableScope";
import { type EncryptionSystem } from "../src/e2ee/sharedKeyManagement";
import { type MatrixRTCMode } from "../src/config/ConfigOptions";
// Shared with Element Call. They live in `src` until the SDK has an
// implementation to move them with; a consumer gets them from here either way.
// Shared with Element Call. They live in `src` until the view model consumes
// the SDK, at which point they move here; a consumer gets them from this
// module either way.
export { type Behavior, constant } from "../src/state/Behavior";
export { ObservableScope } from "../src/state/ObservableScope";
export { E2eeType } from "../src/e2ee/e2eeType";
export { type EncryptionSystem } from "../src/e2ee/sharedKeyManagement";
export { MatrixRTCMode } from "../src/config/ConfigOptions";
// ---------------------------------------------------------------------------
// Session
export interface RtcSessionOptions {
encryptionSystem: EncryptionSystem;
/** Resolved by the host; the SDK reads neither config.json nor settings. */
matrixRTCMode: MatrixRTCMode;
/**
* MSC4075 notification sent with the join. Parameters of the MatrixRTC join
* itself, so they are here even though they are named after calls; reacting
* to a notification (ringing, timeouts, declines) is the application's job.
*/
sendNotificationType?: RTCNotificationType;
callIntent?: RTCCallIntent;
}
/** What the local member publishes. */
export interface LocalMediaInputs {
microphoneEnabled$: Behavior<boolean>;
cameraEnabled$: Behavior<boolean>;
audioInputDeviceId$: Behavior<string | undefined>;
videoInputDeviceId$: Behavior<string | undefined>;
/** Background blur and the like. */
videoProcessor$: Behavior<TrackProcessor<Track.Kind.Video> | undefined>;
}
export type SessionConnectionStatus =
| "waitingForTransport"
| "connecting"
| "connected"
| "reconnecting"
| "disconnected";
export class RtcSessionError extends Error {
public constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "RtcSessionError";
}
}
export interface RtcSession {
join(): void;
leave(): void;
/** Collapsed view of the local member's state machine. */
status$: Behavior<SessionConnectionStatus>;
connected$: Behavior<boolean>;
reconnecting$: Behavior<boolean>;
/** A transport, Matrix or connection error that stops the session. */
fatalError$: Behavior<RtcSessionError | null>;
localMember$: Behavior<LocalRtcMember | null>;
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
* rotating the media encryption key.
*/
keyRotationSuppressed$: Behavior<boolean>;
/** Transports the session currently holds a live connection to. */
connectedTransports$: Behavior<TransportMetadata[]>;
}
// ---------------------------------------------------------------------------
// Transports
/**
* One transport advertised in a membership. Transport independent: `type` and
* `id` are all the SDK needs; `raw` and `resolved$` are there for a
* backend-specific developer panel and for connection diagnostics.
*/
export interface TransportMetadata {
/** `"livekit"` today. */
type: string;
/** Stable key, unique per transport in the session. For LiveKit, the service url. */
id: string;
/** The transport object as it appears in the membership. */
raw: Transport;
/**
* What the backend had to fetch before it could connect. Undefined until the
* connection has resolved it, and again after the connection stops.
*/
resolved$: Behavior<ResolvedTransport | undefined>;
}
export type ResolvedTransport =
| {
type: "livekit";
/** The SFU websocket url, as opposed to the JWT service url in `raw`. */
url: string;
/** A secret: fit for a developer panel, never for a log line. */
token: string;
roomAlias: string;
identity: string;
}
| { type: string; [key: string]: unknown };
// ---------------------------------------------------------------------------
// Members
export interface RtcMember {
local: boolean;
/** `${userId}:${deviceId}` before sticky events, a uuid in Matrix 2.0 mode. The backend identity. */
id: string;
userId: string;
deviceId: string;
membership$: Behavior<CallMembership>;
displayName$: Behavior<string>;
avatarUrl$: Behavior<string | undefined>;
/** Which transport this member is on; undefined when the membership has none. */
transport$: Behavior<TransportMetadata | undefined>;
/**
* Null while the member has a transport but no media has arrived on it yet
* ("waiting for media").
*/
media$: Behavior<MemberMedia | null>;
}
export interface RemoteRtcMember extends RtcMember {
local: false;
}
export interface LocalRtcMember extends RtcMember {
local: true;
media$: Behavior<LocalMemberMedia | null>;
sharingScreen$: Behavior<boolean>;
/** Null when the platform cannot share a screen. */
toggleScreenSharing: (() => void) | null;
screenShareError$: Behavior<Error | null>;
dismissScreenShareError(): void;
}
// ---------------------------------------------------------------------------
// Media
export type MediaSource =
| "microphone"
| "camera"
| "screenShare"
| "screenShareAudio";
export type MediaStreamStats =
| RTCInboundRtpStreamStats
| RTCOutboundRtpStreamStats
| undefined;
/** One published track of a member. */
export interface MediaTrack {
source: MediaSource;
kind: "audio" | "video";
/** Stable for the life of the track. */
id: string;
muted$: Behavior<boolean>;
/** False when the SFU reports the track as unencrypted. */
encrypted$: Behavior<boolean>;
/** Polled while subscribed. */
stats$: Behavior<MediaStreamStats>;
/**
* Rendering. The view hands its `<video>` or `<audio>` element over; the SDK
* sets its stream and, for video, registers the size and on-screen observers
* that pick a simulcast layer and pause the subscription while the element is
* hidden. `attach` is idempotent per element; `detach` has to be called
* before the element leaves the DOM so those observers are released.
*/
attach(element: HTMLMediaElement): void;
detach(element: HTMLMediaElement): void;
}
export interface AudioMediaTrack extends MediaTrack {
kind: "audio";
/** Route playback through Web Audio, for earpiece pan and gain. Undefined resets. */
setAudioContext(ctx: AudioContext | undefined, plugins?: AudioNode[]): void;
setVolume(volume: number): void;
}
export interface VideoMediaTrack extends MediaTrack {
kind: "video";
/** Undefined for remote tracks. */
facingMode$?: Behavior<"user" | "environment" | undefined>;
}
export type EncryptionError = "MissingKey" | "InvalidKey";
/**
* The media of one member, backed by a LiveKit participant inside the SDK.
* There is no identity field: the LiveKit identity is the member's `id`.
*/
export interface MemberMedia {
local: boolean;
speaking$: Behavior<boolean>;
screenShareEnabled$: Behavior<boolean>;
microphone$: Behavior<AudioMediaTrack | undefined>;
camera$: Behavior<VideoMediaTrack | undefined>;
screenShare$: Behavior<VideoMediaTrack | undefined>;
screenShareAudio$: Behavior<AudioMediaTrack | undefined>;
/** Emits when the SFU reports a key problem for this member. */
encryptionError$: Observable<EncryptionError>;
}
export interface LocalMemberMedia extends MemberMedia {
local: true;
}
// ---------------------------------------------------------------------------
// Factory
/**
* Takes the whole `MatrixClient` rather than a slice of it, and finds the
* MatrixRTC session itself. The js-sdk is the MatrixRTC implementation today;
* when the rust-rtc crate replaces it, the SDK has to bridge the crate to the
* js-sdk client, and only the SDK knows what that bridge needs. Holding the
* client keeps that change inside the SDK.
*/
export function createRtcSession(
scope: ObservableScope,
client: MatrixClient,
room: Room,
localMedia: LocalMediaInputs,
options: RtcSessionOptions,
): RtcSession {
// The interface ships ahead of the implementation; see the module comment.
return {} as RtcSession;
}
export * from "./src/api";
export { createRtcSession } from "./src/session/RtcSession";