mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
sdk implementation
This commit is contained in:
+7
-251
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user