mirror of
https://github.com/vector-im/element-call.git
synced 2026-11-07 00:52:28 +00:00
A lobby captures the microphone and the camera on the browser's devices alone, with `createLocalMedia`, so that any backend can take them over: `LocalMedia` hands out the same `LocalAudioMediaTrack` and `LocalVideoMediaTrack` a call renders, with mute, device and facing switches and the host's `VideoProcessor` run on the preview. Given to `join` as `publish`, the tracks are moved rather than shared: the publisher takes them over as it starts publishing, their lobby objects are finished, and the LiveKit backend adopts the device tracks as its own and the processor with its running output, so the join captures nothing and initialises nothing again. Also here, found on the way: a track already on the requested device is left alone (LiveKit would restart it, always on Firefox, which echoes no device id in `getConstraints()`), identical captures in flight are coalesced (React's strict mode mounts twice), and every operation on a processor takes its turn, since a stopped track tears its processor down without waiting for the next track's init. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
/**
|
|
* 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 RTCNotificationType,
|
|
type Transport,
|
|
} from "matrix-js-sdk/lib/matrixrtc";
|
|
import { type Observable } from "rxjs";
|
|
|
|
import { type Behavior } from "./reactive/Behavior";
|
|
import {
|
|
type LocalAudioMediaTrack,
|
|
type LocalMedia,
|
|
type LocalMemberMedia,
|
|
type LocalVideoMediaTrack,
|
|
type MediaSource,
|
|
type MemberMedia,
|
|
type PublishRequest,
|
|
} from "./media-api";
|
|
import { type MediaBackendFactory } from "./media-backend/api";
|
|
import { type EncryptionSystem } from "./encryption";
|
|
import {
|
|
type MatrixRTCMode,
|
|
type MediaQuality,
|
|
type SessionTimings,
|
|
} from "./config";
|
|
import { type MatrixRTCError } from "./errors";
|
|
|
|
export { MatrixRTCError } from "./errors";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Slot
|
|
|
|
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;
|
|
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 {
|
|
/**
|
|
* What the local member publishes at the join: requests the SDK captures
|
|
* itself, or the media a lobby captured ahead of the join, which the
|
|
* participation takes over (its tracks leave `media.tracks$`) and publishes
|
|
* in place of capturing. `publish` on the participation adds to either from
|
|
* then on. Kept as an option, rather than only the method, because the
|
|
* LiveKit backend reads it to build the room's default capture options
|
|
* before the first connection exists.
|
|
*/
|
|
publish: PublishRequest[] | LocalMedia;
|
|
/**
|
|
* 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;
|
|
/**
|
|
* 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<string, unknown>;
|
|
/** Timings the host has configured; the defaults otherwise. */
|
|
timings?: Partial<SessionTimings>;
|
|
/** Limits on what is published; LiveKit's defaults otherwise. */
|
|
mediaQuality?: MediaQuality;
|
|
/**
|
|
* Use this transport instead of asking the homeserver. Any shape the
|
|
* backend serves: for LiveKit a `url`, a `livekit_service_url` or both.
|
|
*/
|
|
transport?: Transport;
|
|
/** Use this transport when the homeserver advertises none of the backend's type. */
|
|
fallbackTransport?: Transport;
|
|
/**
|
|
* What carries the media. LiveKit, configured from the options above, when
|
|
* left out; see `createLivekitBackend` for building one with other options.
|
|
*/
|
|
backend?: MediaBackendFactory;
|
|
}
|
|
|
|
/**
|
|
* The homeserver link that is down, in the order they depend on each other:
|
|
* without sync the membership is unknowable, and without a confirmed
|
|
* membership the delayed leave cannot be refreshed.
|
|
*/
|
|
export type MatrixDisconnectReason = "sync" | "membership" | "probablyLeft";
|
|
|
|
/**
|
|
* Why the participation is not connected: the first failing link, in the
|
|
* order they depend on each other. The media connection comes last because
|
|
* it is worth little while the Matrix side is down.
|
|
*/
|
|
export type DisconnectReason = MatrixDisconnectReason | "media";
|
|
|
|
export type ParticipationState =
|
|
/** Asking the homeserver for a transport, or preparing it. */
|
|
| { kind: "waitingForTransport" }
|
|
/** Transport known; joining the session and connecting the media for the first time. */
|
|
| { kind: "connecting" }
|
|
/** Membership joined and the local media connection up. */
|
|
| { kind: "connected" }
|
|
/** Was connected and one link is down. Back to `connected` or on to `failed`. */
|
|
| { kind: "reconnecting"; reason: DisconnectReason }
|
|
/**
|
|
* The membership or the media connection ended with an error. Terminal
|
|
* unless `left`. A failed publication is not fatal: the member can still
|
|
* receive, so it stays on the local member's media.
|
|
*/
|
|
| { kind: "failed"; error: MatrixRTCError }
|
|
/** `leave()` was called. Terminal. */
|
|
| { kind: "left" };
|
|
|
|
/** 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;
|
|
state$: Behavior<ParticipationState>;
|
|
|
|
/** 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[]>;
|
|
|
|
/**
|
|
* 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 participation currently holds a live connection to. */
|
|
connectedTransports$: Behavior<TransportMetadata[]>;
|
|
|
|
/**
|
|
* Plays the members' audio on this device from now on. Rejects where the
|
|
* browser cannot switch to it. A host that routes audio itself, or leaves
|
|
* the browser's choice, never calls it.
|
|
*/
|
|
setAudioOutputDeviceId(deviceId: string): Promise<void>;
|
|
|
|
/**
|
|
* Sends a short text to every member on the local transport, over a
|
|
* reliable data channel beside the media. A packet holds about 15 KiB;
|
|
* anything larger belongs in a room event. Rejects while the local
|
|
* transport is not connected. Encrypted on the wire like the media, but
|
|
* not end to end with the media key.
|
|
*/
|
|
sendData(topic: string, text: string): Promise<void>;
|
|
/**
|
|
* 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>;
|
|
}
|
|
|
|
/** One `sendData` call as it arrives at a remote member. */
|
|
export interface DataMessage {
|
|
member: RemoteRTCMember;
|
|
topic: string;
|
|
text: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 {
|
|
/** `"m.livekit"` or `"livekit"` today. */
|
|
type: string;
|
|
/** 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;
|
|
/**
|
|
* 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
|
|
|
|
/** A membership in the slot, with what the SDK derives from it. No media. */
|
|
export interface RTCMember {
|
|
local: boolean;
|
|
/**
|
|
* The identity this member is known by on its transport for matching
|
|
* purposes, read off the membership: `${userId}:${deviceId}`. A stable key
|
|
* per membership and a debug label for a host; the SDK matches media
|
|
* against `backendIdentities`, not this.
|
|
*/
|
|
rtcBackendIdentity: string;
|
|
/**
|
|
* Every identity this member may publish under on its transport: the
|
|
* participant identity the SFU sees, and the sender of its data packets.
|
|
* One per authentication flow the member's transport allows, so a
|
|
* transport with both a `url` and a `livekit_service_url` lists the hashed
|
|
* and the legacy identity, and the backend matches whichever shows up.
|
|
* Opaque to a host.
|
|
*/
|
|
backendIdentities: readonly string[];
|
|
userId: string;
|
|
deviceId: string;
|
|
/** 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>;
|
|
/** 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, MemberMedia {
|
|
local: false;
|
|
}
|
|
|
|
/** Our member: the same as a remote one, with tracks that carry the controls. */
|
|
export interface LocalRTCMember extends RTCMember, LocalMemberMedia {
|
|
local: true;
|
|
}
|