mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
The OpenID flow moves to src/livekit/auth/openID.ts with the SFU config types in a sibling file, takes an options object, and replaces the MatrixRTC mode with the role we take on the transport plus the session's slot ID instead of a hard-coded m.call#ROOM. This changes behaviour: publishers on a livekit_service_url transport always use the legacy /sfu/get endpoint, so that subscribers can predict their user:device participant identity, and subscribers always use the default /get_token endpoint. The MatrixRTC mode no longer influences which endpoint is used. On the SDK this lands as sdk/src/media-backend/livekit/auth/ with the connection map, the discriminated connection state and the client-server API flow of the following commits of element-hq/element-call#4331, which rewrite the same files: - Model ConnectionState as a discriminated union - Show the whole transport in debug UI and logs (SDK part) - Let the publishing Connection authenticate itself - Reduce ConnectionManager to a ConnectionMap - Use the unstable LiveKit transport type - Identify transports by server name as well as URLs - Get SFU tokens and delegate delayed leaves through the MSC4195 CS API - Fall back to get_token as subscriber on widget API errors - Share a connection between homeservers using the same JWT service - Make the auth choice mode independent again Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org> (cherry picked from commit adb380cd9ed108856e42d992fc03ced8c44f1e56)
197 lines
7.5 KiB
TypeScript
197 lines
7.5 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 seam between the session and whatever carries its media. The session
|
|
* speaks MatrixRTC: memberships, transports as they appear in them, the
|
|
* delayed leave. A backend speaks one media protocol and hands back the
|
|
* media of each member. `media-backend/livekit/` is the one backend today.
|
|
*/
|
|
|
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
|
import { type Transport } from "matrix-js-sdk/lib/matrixrtc";
|
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
|
import { type Observable } from "rxjs";
|
|
|
|
import { type ResolvedTransport, type RTCMember } from "../api";
|
|
import {
|
|
type LocalAudioMediaTrack,
|
|
type LocalMemberMedia,
|
|
type LocalVideoMediaTrack,
|
|
type MediaSource,
|
|
type MemberMedia,
|
|
type PublishRequest,
|
|
} from "../media-api";
|
|
import { type SessionTimings } from "../config";
|
|
import { type EncryptionSystem } from "../encryption";
|
|
import { type Behavior } from "../reactive/Behavior";
|
|
import { type ObservableScope } from "../reactive/ObservableScope";
|
|
|
|
export enum MediaConnectionState {
|
|
/** No connection attempt yet, or no connection at all. */
|
|
Initialized = "initialized",
|
|
/** Fetching whatever the connection needs first; the SFU token for LiveKit. */
|
|
Preparing = "preparing",
|
|
Connecting = "connecting",
|
|
Connected = "connected",
|
|
Reconnecting = "reconnecting",
|
|
Disconnected = "disconnected",
|
|
Stopped = "stopped",
|
|
}
|
|
|
|
/** One per-participant media key, as the session's key exchange delivers it. */
|
|
export interface MediaKey {
|
|
/** The `rtcBackendIdentity` of the member the key belongs to. */
|
|
participantId: string;
|
|
index: number;
|
|
key: Uint8Array<ArrayBuffer>;
|
|
}
|
|
|
|
export interface MediaBackendContext {
|
|
roomId: string;
|
|
/** The slot the session is in, `application#id`; a token is issued for it. */
|
|
slotId: string;
|
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
|
/** What the local member publishes at the join. */
|
|
publish: PublishRequest[];
|
|
encryptionSystem: EncryptionSystem;
|
|
/**
|
|
* Every media key the session knows: the current ones replayed on
|
|
* subscribe, new ones as they arrive. Empty for a shared key, which is in
|
|
* `encryptionSystem`.
|
|
*/
|
|
mediaKeys$: Observable<MediaKey>;
|
|
timings: SessionTimings;
|
|
logger: Logger;
|
|
}
|
|
|
|
/**
|
|
* The type of the `backend` option: the session calls it once with its scope
|
|
* and the context. A backend's own factory keeps the shape of every other
|
|
* factory here, `createLivekitBackend(scope, context, options)`, and a host
|
|
* closes over its options. The Matrix client is not in the context: a backend
|
|
* that needs it takes a `Pick` of exactly what it calls in its own options.
|
|
*/
|
|
export type MediaBackendFactory = (
|
|
scope: ObservableScope,
|
|
context: MediaBackendContext,
|
|
) => MediaBackend;
|
|
|
|
/** What preparing the local transport established; what the MatrixRTC join needs from the media side. */
|
|
export interface TransportCapabilities {
|
|
/**
|
|
* Whether the backend can restart the delayed leave on the client's behalf,
|
|
* so that a client that vanishes is removed by the backend rather than by
|
|
* the timeout. Decides which delayed leave timings the join uses, and
|
|
* whether `delegateDelayedLeave` is ever called.
|
|
*/
|
|
canDelegateDelayedLeave: boolean;
|
|
}
|
|
|
|
/** One connection a backend holds, for diagnostics. */
|
|
export interface BackendConnection {
|
|
/** The transport as it appears in memberships; what the registry keys on. */
|
|
transport: Transport;
|
|
state: MediaConnectionState | Error;
|
|
/**
|
|
* What the backend fetched to connect; undefined until it has, and again
|
|
* after the connection stops. One object per fetch, so that a change of
|
|
* state does not read as a change of this.
|
|
*/
|
|
resolved: ResolvedTransport | undefined;
|
|
}
|
|
|
|
export interface DataPacket {
|
|
/** The backend identity of the sender. */
|
|
senderId: string;
|
|
topic: string;
|
|
text: string;
|
|
}
|
|
|
|
export interface MediaBackend {
|
|
/** The membership transport type this backend serves: "livekit". */
|
|
readonly transportType: string;
|
|
|
|
/**
|
|
* Makes `transport` usable as the one the local member publishes on. The
|
|
* session found it; the backend does whatever has to succeed before it is
|
|
* advertised in the membership. Rejects with the error the session reports
|
|
* as fatal. Called once per session.
|
|
*/
|
|
prepareLocalTransport(transport: Transport): Promise<TransportCapabilities>;
|
|
|
|
/**
|
|
* Hands the delayed leave event to the backend, which restarts it from
|
|
* then on. Called each time the membership manager reports a new delay id,
|
|
* only when `canDelegateDelayedLeave` is true. A rejection is logged by
|
|
* the session, not fatal.
|
|
*/
|
|
delegateDelayedLeave(delayId: string): Promise<void>;
|
|
|
|
readonly local: LocalMediaBackend;
|
|
|
|
/**
|
|
* The media of one remote member: its tracks, null while nothing has
|
|
* arrived for it on its transport, and its key errors. Built in `scope`,
|
|
* which the caller ends with the member or the participation, whichever
|
|
* goes first. This is also how the backend learns which remote transports
|
|
* exist: it follows the member's `transport$` for as long as `scope` lives,
|
|
* and connects to every transport some member is on.
|
|
*/
|
|
mediaFor(scope: ObservableScope, member: RTCMember): MemberMedia;
|
|
|
|
/** Every connection the backend holds. For debugging and devtool purposes.*/
|
|
readonly connections$: Behavior<BackendConnection[]>;
|
|
|
|
/**
|
|
* Plays the members' audio on this device: on the local transport's
|
|
* connection, whether it is connected yet or not, and on every connection
|
|
* created from then on. A connection to a remote transport that already
|
|
* exists keeps the device it was created with.
|
|
*/
|
|
setAudioOutputDeviceId(deviceId: string): Promise<void>;
|
|
|
|
/**
|
|
* A text channel beside the media: sent on the local transport, received on every one.
|
|
* This could well be just another track kind. Not needed for element call. just for the sdk target.
|
|
* Keeping it on the top level for visibility. Should be tackled in a future refactor.
|
|
*/
|
|
sendData(topic: string, text: string): Promise<void>;
|
|
readonly data$: Observable<DataPacket>;
|
|
}
|
|
|
|
/** `tracks$` is null until the connection carries a local participant. */
|
|
export interface LocalMediaBackend extends LocalMemberMedia {
|
|
/**
|
|
* State of the connection the local member publishes on. `Initialized`
|
|
* while there is none; an `Error` is the connection's failure.
|
|
*/
|
|
readonly connectionState$: Behavior<MediaConnectionState | Error>;
|
|
|
|
/**
|
|
* Whether the local tracks reach the transport. The session sets it to
|
|
* "joined and the homeserver is reachable". Remembered across connections:
|
|
* the tracks are created on the first `true`, and their upstream is paused
|
|
* while `false`, so the local preview stays live while nothing is heard.
|
|
*/
|
|
setPublishing(publish: boolean): void;
|
|
/** The first failure to publish, kept until the session ends. Not fatal. */
|
|
readonly publishError$: Behavior<Error | null>;
|
|
|
|
/**
|
|
* Publishes a source and resolves with its track once it is in `tracks$`.
|
|
* Remembered across connections, so a request before the
|
|
* transport is up is applied once it is, and republished after a
|
|
* reconnection. Rejects where the device could not be used.
|
|
*/
|
|
publish(
|
|
request: PublishRequest,
|
|
): Promise<LocalAudioMediaTrack | LocalVideoMediaTrack>;
|
|
/** Removes one of our publications; a screen share takes its audio with it. */
|
|
unpublish(source: MediaSource): Promise<void>;
|
|
}
|