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-09-30 11:33:45 +02:00
import { connectedParticipantsObserver , connectionStateObserver } from "@livekit/components-core" ;
import { type ConnectionState , type E2EEOptions , Room as LivekitRoom } from "livekit-client" ;
2025-08-28 13:52:12 +02:00
import { type MatrixClient } from "matrix-js-sdk" ;
2025-09-30 11:33:45 +02:00
import { type CallMembership , type LivekitFocus } from "matrix-js-sdk/lib/matrixrtc" ;
import { combineLatest } from "rxjs" ;
2025-08-28 13:52:12 +02:00
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU" ;
2025-08-29 18:46:24 +02:00
import { type Behavior } from "./Behavior" ;
2025-08-28 13:52:12 +02:00
import { type ObservableScope } from "./ObservableScope" ;
2025-08-28 17:45:14 +02:00
import { defaultLiveKitOptions } from "../livekit/options" ;
2025-08-28 13:52:12 +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-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
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-08-28 13:52:12 +02:00
public async start () : Promise < void > {
this . stopped = false ;
const { url , jwt } = await this . sfuConfig ;
if ( ! this . stopped ) await this . livekitRoom . connect ( url , jwt );
}
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-08-28 13:52:12 +02:00
public stop () : void {
2025-09-26 13:20:55 -04:00
if ( this . stopped ) return ;
2025-08-28 13:52:12 +02:00
void this . livekitRoom . disconnect ();
this . stopped = true ;
}
2025-08-28 17:45:14 +02:00
protected readonly sfuConfig = getSFUConfigWithOpenID (
this . client ,
this . focus . livekit_service_url ,
2025-09-30 11:33:45 +02:00
this . focus . livekit_alias
2025-08-28 13:52:12 +02:00
);
2025-09-30 11:33:45 +02:00
/*
* An observable of the participants in the livekit room, including subscribers.
* Converts the livekit room events ParticipantConnected/ParticipantDisconnected/StateChange to an observable.
*/
protected readonly participantsIncludingSubscribers$ ;
/**
* An observable of the participants that are publishing on this connection.
* This is derived from `participantsIncludingSubscribers$` and `membershipsFocusMap$`.
* It filters the participants to only those that are associated with a membership that claims to publish on this connection.
*/
2025-08-28 17:45:14 +02:00
public readonly publishingParticipants$ ;
2025-09-30 11:33:45 +02:00
/**
* The LiveKit room instance.
*/
2025-08-29 18:46:24 +02:00
public readonly livekitRoom : LivekitRoom ;
2025-08-28 17:45:14 +02:00
2025-09-30 11:33:45 +02:00
/**
* An observable of the livekit connection state.
* Converts the livekit room events StateChange to an observable.
*/
2025-08-28 17:45:14 +02:00
public connectionState$ : Behavior < ConnectionState >;
2025-09-30 11:33:45 +02:00
/**
* Creates a new connection to a matrix RTC LiveKit backend.
*
* @param livekitRoom - Optional LiveKit room instance to use. If not provided, a new instance will be created.
* @param focus - The focus server to connect to.
* @param livekitAlias - The livekit alias to use when connecting to the focus server. TODO duplicate of focus?
* @param client - The matrix client, used to fetch the OpenId token. TODO refactor to avoid passing the whole client
* @param scope - The observable scope to use for creating observables.
* @param membershipsFocusMap$ - The observable of the current call RTC memberships and their associated focus.
* @param e2eeLivekitOptions - The E2EE options to use for the LiveKit room. Use to share the same key provider across connections!. TODO refactor to avoid passing the whole options?
*/
2025-08-28 17:45:14 +02:00
public constructor (
protected readonly focus : LivekitFocus ,
2025-09-30 11:33:45 +02:00
// TODO : remove livekitAlias, it's already in focus?
2025-08-28 17:45:14 +02:00
protected readonly livekitAlias : string ,
protected readonly client : MatrixClient ,
protected readonly scope : ObservableScope ,
protected readonly membershipsFocusMap$ : Behavior <
{ membership : CallMembership ; focus : LivekitFocus }[]
> ,
e2eeLivekitOptions : E2EEOptions | undefined ,
2025-09-30 11:33:45 +02:00
livekitRoom : LivekitRoom | undefined = undefined
2025-08-28 17:45:14 +02:00
) {
2025-08-29 18:46:24 +02:00
this . livekitRoom =
livekitRoom ??
new LivekitRoom ({
... defaultLiveKitOptions ,
2025-09-30 11:33:45 +02:00
e2ee : e2eeLivekitOptions
2025-08-29 18:46:24 +02:00
});
2025-08-28 17:45:14 +02:00
this . participantsIncludingSubscribers$ = this . scope . behavior (
connectedParticipantsObserver ( this . livekitRoom ),
2025-09-30 11:33:45 +02:00
[]
2025-08-28 17:45:14 +02:00
);
this . publishingParticipants$ = this . scope . behavior (
2025-09-25 21:29:02 -04:00
combineLatest (
[ this . participantsIncludingSubscribers$ , this . membershipsFocusMap$ ],
( participants , membershipsFocusMap ) =>
2025-08-28 15:32:46 +02:00
membershipsFocusMap
// Find all members that claim to publish on this connection
. flatMap (({ membership , focus }) =>
focus . livekit_service_url === this . focus . livekit_service_url
? [ membership ]
2025-09-30 11:33:45 +02:00
: []
2025-08-28 15:32:46 +02:00
)
// Find all associated publishing livekit participant objects
2025-08-28 17:45:14 +02:00
. flatMap (( membership ) => {
2025-08-28 15:32:46 +02:00
const participant = participants . find (
2025-08-28 17:45:14 +02:00
( p ) =>
2025-09-30 11:33:45 +02:00
p . identity === ` ${ membership . sender } : ${ membership . deviceId } `
2025-08-28 15:32:46 +02:00
);
2025-08-28 17:45:14 +02:00
return participant ? [{ participant , membership }] : [];
2025-09-30 11:33:45 +02:00
})
2025-08-28 13:52:12 +02:00
),
2025-09-30 11:33:45 +02:00
[]
2025-08-28 13:52:12 +02:00
);
2025-08-28 17:45:14 +02:00
this . connectionState$ = this . scope . behavior < ConnectionState >(
2025-09-30 11:33:45 +02:00
connectionStateObserver ( this . livekitRoom )
2025-08-28 17:45:14 +02:00
);
2025-09-26 13:20:55 -04:00
this . scope . onEnd (() => this . stop ());
2025-08-28 17:45:14 +02:00
}
2025-08-28 13:52:12 +02:00
}