2025-08-28 13:52:12 +02:00
/*
2025-11-18 10:13:10 +01:00
Copyright 2025 Element Creations Ltd.
2025-08-28 13:52:12 +02:00
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
2025-10-07 16:24:02 +02:00
import {
connectedParticipantsObserver ,
connectionStateObserver ,
} from "@livekit/components-core" ;
import {
2025-10-14 10:46:57 +02:00
ConnectionError ,
2025-10-30 00:09:07 +01:00
type Room as LivekitRoom ,
2025-11-05 18:57:24 +01:00
type RemoteParticipant ,
2025-10-07 16:24:02 +02:00
} from "livekit-client" ;
2026-01-28 14:22:21 +01:00
import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc" ;
2025-12-01 17:29:21 +01:00
import { BehaviorSubject , map } from "rxjs" ;
2025-10-28 21:18:47 +01:00
import { type Logger } from "matrix-js-sdk/lib/logger" ;
2025-12-17 09:53:49 +01:00
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager" ;
2025-08-28 13:52:12 +02:00
2025-10-07 16:24:02 +02:00
import {
getSFUConfigWithOpenID ,
type OpenIDClientParts ,
type SFUConfig ,
2025-11-07 08:44:44 +01:00
} from "../../../livekit/openIDSFU.ts" ;
import { type Behavior } from "../../Behavior.ts" ;
import { type ObservableScope } from "../../ObservableScope.ts" ;
2025-10-14 10:46:57 +02:00
import {
2025-12-10 21:14:13 +01:00
ElementCallError ,
2025-10-14 10:46:57 +02:00
InsufficientCapacityError ,
SFURoomCreationRestrictedError ,
2025-12-10 21:14:13 +01:00
UnknownCallError ,
2025-11-07 08:44:44 +01:00
} from "../../../utils/errors.ts" ;
2026-01-09 13:38:26 +01:00
import { type JwtEndpointVersion } from "../localMember/LocalTransport.ts" ;
2025-08-28 13:52:12 +02:00
2025-09-30 17:02:48 +02:00
export interface ConnectionOpts {
2026-01-09 13:38:26 +01:00
/**
* For the local transport we already do know the jwt token and url. We can reuse it.
* On top the local transport will send additional data to the jwt server to use delayed event delegation.
*/
existingSFUConfig? : SFUConfig ;
/**
* For local connections that use the oldest member pattern. here we have not prefetched the sfuConfig
* and hence we need to let the connection do the jwt token fetching.
*/
forceJwtEndpoint? : JwtEndpointVersion ;
/** The identity parts to use on this connection */
ownMembershipIdentity : CallMembershipIdentityParts ;
2025-10-14 14:38:37 -04:00
/** The media transport to connect to. */
2026-01-28 14:22:21 +01:00
transport : LivekitTransportConfig ;
2025-09-30 17:02:48 +02:00
/** The Matrix client to use for OpenID and SFU config requests. */
client : OpenIDClientParts ;
2026-01-28 14:22:21 +01:00
/** The room ID this connection is associated with. */
roomId : string ;
2025-09-30 17:02:48 +02:00
/** The observable scope to use for this connection. */
scope : ObservableScope ;
2025-10-01 10:06:43 +02:00
2025-10-22 18:50:16 -04:00
/** Optional factory to create the LiveKit room, mainly for testing purposes. */
2025-10-30 00:09:07 +01:00
livekitRoomFactory : () => LivekitRoom ;
2025-09-30 17:02:48 +02:00
}
2025-12-05 19:48:02 +01:00
export class FailedToStartError extends Error {
public constructor ( message : string ) {
super ( message );
this . name = "FailedToStartError" ;
}
}
2025-10-01 10:06:43 +02:00
2025-12-05 19:48:02 +01:00
export enum ConnectionState {
2025-12-09 15:23:30 +01:00
/** The start state of a connection. It has been created but nothing has loaded yet. */
2025-12-02 19:40:08 +01:00
Initialized = "Initialized" ,
2025-12-09 15:23:30 +01:00
/** `start` has been called on the connection. It aquires the jwt info to conenct to the LK Room */
2025-12-02 19:40:08 +01:00
FetchingConfig = "FetchingConfig" ,
Stopped = "Stopped" ,
2025-12-09 15:23:30 +01:00
/** The same as ConnectionState.Disconnected from `livekit-client` */
2025-12-05 19:48:02 +01:00
LivekitDisconnected = "disconnected" ,
2025-12-09 15:23:30 +01:00
/** The same as ConnectionState.Connecting from `livekit-client` */
2025-12-05 19:48:02 +01:00
LivekitConnecting = "connecting" ,
2025-12-09 15:23:30 +01:00
/** The same as ConnectionState.Connected from `livekit-client` */
2025-12-05 19:48:02 +01:00
LivekitConnected = "connected" ,
2025-12-09 15:23:30 +01:00
/** The same as ConnectionState.Reconnecting from `livekit-client` */
2025-12-05 19:48:02 +01:00
LivekitReconnecting = "reconnecting" ,
2025-12-09 15:23:30 +01:00
/** The same as ConnectionState.SignalReconnecting from `livekit-client` */
2025-12-05 19:48:02 +01:00
LivekitSignalReconnecting = "signalReconnecting" ,
2025-12-02 19:40:08 +01:00
}
2025-10-01 10:06:43 +02:00
2025-09-30 11:33:45 +02:00
/**
* A connection to a Matrix RTC LiveKit backend.
*
* Expose observables for participants and connection state.
*/
2025-08-28 13:52:12 +02:00
export class Connection {
2025-10-01 10:06:43 +02:00
// Private Behavior
2025-12-10 21:14:13 +01:00
private readonly _state$ = new BehaviorSubject <
ConnectionState | ElementCallError
> ( ConnectionState . Initialized );
2025-10-01 10:06:43 +02:00
/**
2025-10-14 14:38:37 -04:00
* The current state of the connection to the media transport.
2025-10-01 10:06:43 +02:00
*/
2025-12-09 15:23:30 +01:00
public readonly state$ : Behavior < ConnectionState | Error > = this . _state$ ;
2025-10-01 16:39:21 +02:00
2025-11-25 20:18:34 +01:00
/**
* The media transport to connect to.
*/
2026-01-28 14:22:21 +01:00
public readonly transport : LivekitTransportConfig ;
2025-11-25 20:18:34 +01:00
public readonly livekitRoom : LivekitRoom ;
2025-12-01 17:29:21 +01:00
private scope : ObservableScope ;
2025-11-25 20:18:34 +01:00
/**
2025-12-10 15:09:40 -05:00
* The remote LiveKit participants that are visible on this connection.
*
* Note that this may include participants that are connected only to
* subscribe, or publishers that are otherwise unattested in MatrixRTC state.
* It is therefore more low-level than what should be presented to the user.
2025-11-25 20:18:34 +01:00
*/
2025-12-10 15:09:40 -05:00
public readonly remoteParticipants$ : Behavior < RemoteParticipant [] >;
2025-11-25 20:18:34 +01:00
2026-01-13 15:14:19 +01:00
/**
* The alias of the LiveKit room.
*/
public get livekitAlias () : string | undefined {
return this . _livekitAlias ;
}
private _livekitAlias? : string ;
2025-09-30 11:33:45 +02:00
/**
* Whether the connection has been stopped.
* @see Connection.stop
* */
2025-08-28 17:45:14 +02:00
protected stopped = false ;
2025-08-28 13:52:12 +02:00
2026-01-28 14:22:21 +01:00
// TODO: can we just keep the ConnectionOpts object instead of spreading?
private readonly client : OpenIDClientParts ;
private readonly roomId : string ;
private readonly logger : Logger ;
private readonly ownMembershipIdentity : CallMembershipIdentityParts ;
private readonly existingSFUConfig? : SFUConfig ;
/**
* Creates a new connection to a matrix RTC LiveKit backend.
*
* @param opts - Connection options {@link ConnectionOpts}.
*
* @param logger - The logger to use.
*/
public constructor ( opts : ConnectionOpts , logger : Logger ) {
this . ownMembershipIdentity = opts . ownMembershipIdentity ;
this . existingSFUConfig = opts . existingSFUConfig ;
this . roomId = opts . roomId ;
this . logger = logger . getChild (
"[Connection " + opts . transport . livekit_service_url + "]" ,
);
this . logger . info (
`constructor: ${ opts . transport . livekit_service_url } roomId: ${ this . roomId } withSfuConfig?: ${ opts . existingSFUConfig ? JSON . stringify ( opts . existingSFUConfig ) : "undefined" } ` ,
);
const { transport , client , scope } = opts ;
this . scope = scope ;
this . livekitRoom = opts . livekitRoomFactory ();
this . transport = transport ;
this . client = client ;
this . remoteParticipants$ = scope . behavior (
// Only tracks remote participants
connectedParticipantsObserver ( this . livekitRoom ),
);
scope . onEnd (() => {
this . logger . info ( `Connection scope ended, stopping connection` );
void this . stop ();
});
}
2025-09-30 11:33:45 +02:00
/**
* Starts the connection.
*
* This will:
* 1. Request an OpenId token `request_token` (allows matrix users to verify their identity with a third-party service.)
* 2. Use this token to request the SFU config to the MatrixRtc authentication service.
* 3. Connect to the configured LiveKit room.
2025-10-14 10:46:57 +02:00
*
2025-10-29 18:31:58 +01:00
* The errors are also represented as a state in the `state$` observable.
* It is safe to ignore those errors and handle them accordingly via the `state$` observable.
2025-10-14 10:46:57 +02:00
* @throws {InsufficientCapacityError} if the LiveKit server indicates that it has insufficient capacity to accept the connection.
* @throws {SFURoomCreationRestrictedError} if the LiveKit server indicates that the room does not exist and cannot be created.
2025-09-30 11:33:45 +02:00
*/
2025-10-29 18:31:58 +01:00
// TODO consider an autostart pattern...
2025-08-28 13:52:12 +02:00
public async start () : Promise < void > {
2025-11-14 16:18:31 +01:00
this . logger . debug ( "Starting Connection" );
2025-08-28 13:52:12 +02:00
this . stopped = false ;
2025-10-01 10:06:43 +02:00
try {
2025-12-05 19:48:02 +01:00
this . _state$ . next ( ConnectionState . FetchingConfig );
2025-12-10 21:14:13 +01:00
// We should already have this information after creating the localTransport.
2026-01-09 13:38:26 +01:00
// only call getSFUConfigWithOpenID for connections where we do not have a token yet. (existingJwtTokenData === undefined)
2026-01-13 15:14:19 +01:00
const { url , jwt , livekitAlias } =
2026-01-09 13:38:26 +01:00
this . existingSFUConfig ??
( await this . getSFUConfigForRemoteConnection ());
2026-01-15 18:13:34 +01:00
this . logger . debug (
"Starting Connection to: " ,
this . transport . livekit_service_url ,
"jwt: " ,
jwt ,
"wss: " ,
url ,
"livekitAlias: " ,
livekitAlias ,
);
2026-01-13 15:14:19 +01:00
this . _livekitAlias = livekitAlias ;
2025-10-01 10:06:43 +02:00
// If we were stopped while fetching the config, don't proceed to connect
if ( this . stopped ) return ;
2025-12-11 16:04:12 +01:00
// Setup observer once we are done with getSFUConfigWithOpenID
connectionStateObserver ( this . livekitRoom )
. pipe (
this . scope . bind (),
map (( s ) => s as unknown as ConnectionState ),
)
. subscribe (( lkState ) => {
// It is save to cast lkState to ConnectionState as they are fully overlapping.
this . _state$ . next ( lkState );
});
2025-10-14 10:46:57 +02:00
try {
2026-01-15 18:13:34 +01:00
this . logger . info ( `livekitRoom.connect ${ url } ` );
2025-10-14 10:46:57 +02:00
await this . livekitRoom . connect ( url , jwt );
2026-01-15 18:13:34 +01:00
this . logger . info ( `livekitRoom.connect SUCCESS ${ url } ` );
2025-10-14 10:46:57 +02:00
} catch ( e ) {
2026-01-15 18:13:34 +01:00
this . logger . info ( `livekitRoom.connect FAILED ${ url } ` , e );
2025-10-14 10:46:57 +02:00
// LiveKit uses 503 to indicate that the server has hit its track limits.
// https://github.com/livekit/livekit/blob/fcb05e97c5a31812ecf0ca6f7efa57c485cea9fb/pkg/service/rtcservice.go#L171
// It also errors with a status code of 200 (yes, really) for room
// participant limits.
// LiveKit Cloud uses 429 for connection limits.
// Either way, all these errors can be explained as "insufficient capacity".
if ( e instanceof ConnectionError ) {
if ( e . status === 503 || e . status === 200 || e . status === 429 ) {
throw new InsufficientCapacityError ();
}
if ( e . status === 404 ) {
2025-12-10 18:50:19 +01:00
// error msg is "Failed to create call"
// error description is "Call creation might be restricted to authorized users only. Try again later, or contact your server admin if the problem persists."
2025-10-14 10:46:57 +02:00
// The room does not exist. There are two different modes of operation for the SFU:
// - the room is created on the fly when connecting (livekit `auto_create` option)
// - Only authorized users can create rooms, so the room must exist before connecting (done by the auth jwt service)
// In the first case there will not be a 404, so we are in the second case.
throw new SFURoomCreationRestrictedError ();
}
}
throw e ;
}
2025-10-01 10:06:43 +02:00
// If we were stopped while connecting, don't proceed to update state.
if ( this . stopped ) return ;
} catch ( error ) {
2025-11-14 16:18:31 +01:00
this . logger . debug ( `Failed to connect to LiveKit room: ${ error } ` );
2025-12-10 21:14:13 +01:00
this . _state$ . next (
2025-12-10 21:17:33 +01:00
error instanceof ElementCallError
? error
: error instanceof Error
? new UnknownCallError ( error )
: new UnknownCallError ( new Error ( ` ${ error } ` )),
2025-12-10 21:14:13 +01:00
);
2025-12-10 18:50:19 +01:00
// Its okay to ignore the throw. The error is part of the state.
2025-10-01 10:06:43 +02:00
throw error ;
}
2025-08-28 13:52:12 +02:00
}
2026-01-09 13:38:26 +01:00
protected async getSFUConfigForRemoteConnection () : Promise < SFUConfig > {
// This will only be called for sfu's where we do not publish ourselves.
// For the local connection we will use the existingJwtTokenData
2025-09-30 17:02:48 +02:00
return await getSFUConfigWithOpenID (
this . client ,
2025-12-17 09:53:49 +01:00
this . ownMembershipIdentity ,
2025-10-14 14:34:51 -04:00
this . transport . livekit_service_url ,
2026-01-28 14:22:21 +01:00
this . roomId ,
2026-01-09 13:38:26 +01:00
// dont pass any custom opts for the subscribe only connections
{},
2025-12-29 17:38:54 +01:00
this . logger ,
2025-10-07 16:24:02 +02:00
);
2025-09-30 17:02:48 +02:00
}
2025-10-30 00:09:07 +01:00
2025-09-30 11:33:45 +02:00
/**
* Stops the connection.
*
* This will disconnect from the LiveKit room.
* If the connection is already stopped, this is a no-op.
*/
2025-10-01 16:39:21 +02:00
public async stop () : Promise < void > {
2025-11-14 16:18:31 +01:00
this . logger . debug (
2026-01-15 18:13:34 +01:00
`stop: disconnecing from lk room ${ this . transport . livekit_service_url } ` ,
2025-11-14 16:18:31 +01:00
);
2025-09-26 13:20:55 -04:00
if ( this . stopped ) return ;
2025-10-01 16:39:21 +02:00
await this . livekitRoom . disconnect ();
2025-12-09 15:23:30 +01:00
this . _state$ . next ( ConnectionState . Stopped );
2025-08-28 13:52:12 +02:00
this . stopped = true ;
2026-01-15 18:13:34 +01:00
this . logger . debug (
`stop: DONE disconnecing from lk room ${ this . transport . livekit_service_url } ` ,
);
2025-08-28 13:52:12 +02:00
}
}