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 {
|
|
|
|
|
type ConnectionState,
|
|
|
|
|
type E2EEOptions,
|
|
|
|
|
Room as LivekitRoom,
|
|
|
|
|
type RoomOptions,
|
|
|
|
|
} from "livekit-client";
|
|
|
|
|
import {
|
|
|
|
|
type CallMembership,
|
|
|
|
|
type LivekitTransport,
|
|
|
|
|
} from "matrix-js-sdk/lib/matrixrtc";
|
2025-10-01 10:06:43 +02:00
|
|
|
import { BehaviorSubject, combineLatest } from "rxjs";
|
2025-08-28 13:52:12 +02:00
|
|
|
|
2025-10-07 16:24:02 +02:00
|
|
|
import {
|
|
|
|
|
getSFUConfigWithOpenID,
|
|
|
|
|
type OpenIDClientParts,
|
|
|
|
|
type SFUConfig,
|
|
|
|
|
} 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 17:02:48 +02:00
|
|
|
export interface ConnectionOpts {
|
|
|
|
|
/** The focus server to connect to. */
|
2025-10-07 10:33:31 +02:00
|
|
|
transport: LivekitTransport;
|
2025-09-30 17:02:48 +02:00
|
|
|
/** The Matrix client to use for OpenID and SFU config requests. */
|
|
|
|
|
client: OpenIDClientParts;
|
|
|
|
|
/** The observable scope to use for this connection. */
|
|
|
|
|
scope: ObservableScope;
|
|
|
|
|
/** An observable of the current RTC call memberships and their associated focus. */
|
2025-10-07 16:24:02 +02:00
|
|
|
remoteTransports$: Behavior<
|
|
|
|
|
{ membership: CallMembership; transport: LivekitTransport }[]
|
|
|
|
|
>;
|
2025-10-01 10:06:43 +02:00
|
|
|
|
|
|
|
|
/** Optional factory to create the Livekit room, mainly for testing purposes. */
|
|
|
|
|
livekitRoomFactory?: (options?: RoomOptions) => LivekitRoom;
|
2025-09-30 17:02:48 +02:00
|
|
|
}
|
2025-10-01 10:06:43 +02:00
|
|
|
|
|
|
|
|
export type FocusConnectionState =
|
2025-10-07 16:24:02 +02:00
|
|
|
| { state: "Initialized" }
|
|
|
|
|
| { state: "FetchingConfig"; focus: LivekitTransport }
|
|
|
|
|
| { state: "ConnectingToLkRoom"; focus: LivekitTransport }
|
|
|
|
|
| { state: "PublishingTracks"; focus: LivekitTransport }
|
|
|
|
|
| { state: "FailedToStart"; error: Error; focus: LivekitTransport }
|
|
|
|
|
| {
|
|
|
|
|
state: "ConnectedToLkRoom";
|
|
|
|
|
connectionState: ConnectionState;
|
|
|
|
|
focus: LivekitTransport;
|
|
|
|
|
}
|
|
|
|
|
| { state: "Stopped"; focus: LivekitTransport };
|
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-10-08 18:10:26 -04:00
|
|
|
private readonly _focusConnectionState$ =
|
2025-10-07 16:24:02 +02:00
|
|
|
new BehaviorSubject<FocusConnectionState>({ state: "Initialized" });
|
2025-10-01 10:06:43 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The current state of the connection to the focus server.
|
|
|
|
|
*/
|
2025-10-08 18:10:26 -04:00
|
|
|
public readonly focusConnectionState$: Behavior<FocusConnectionState> =
|
|
|
|
|
this._focusConnectionState$;
|
2025-10-01 16:39:21 +02:00
|
|
|
|
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;
|
2025-10-01 10:06:43 +02:00
|
|
|
try {
|
2025-10-08 18:10:26 -04:00
|
|
|
this._focusConnectionState$.next({
|
2025-10-07 16:24:02 +02:00
|
|
|
state: "FetchingConfig",
|
|
|
|
|
focus: this.localTransport,
|
|
|
|
|
});
|
2025-10-01 10:06:43 +02:00
|
|
|
// TODO could this be loaded earlier to save time?
|
|
|
|
|
const { url, jwt } = await this.getSFUConfigWithOpenID();
|
|
|
|
|
// If we were stopped while fetching the config, don't proceed to connect
|
|
|
|
|
if (this.stopped) return;
|
|
|
|
|
|
2025-10-08 18:10:26 -04:00
|
|
|
this._focusConnectionState$.next({
|
2025-10-07 16:24:02 +02:00
|
|
|
state: "ConnectingToLkRoom",
|
|
|
|
|
focus: this.localTransport,
|
|
|
|
|
});
|
2025-10-01 10:06:43 +02:00
|
|
|
await this.livekitRoom.connect(url, jwt);
|
|
|
|
|
// If we were stopped while connecting, don't proceed to update state.
|
|
|
|
|
if (this.stopped) return;
|
|
|
|
|
|
2025-10-08 18:10:26 -04:00
|
|
|
this._focusConnectionState$.next({
|
2025-10-07 16:24:02 +02:00
|
|
|
state: "ConnectedToLkRoom",
|
|
|
|
|
focus: this.localTransport,
|
|
|
|
|
connectionState: this.livekitRoom.state,
|
|
|
|
|
});
|
2025-10-01 10:06:43 +02:00
|
|
|
} catch (error) {
|
2025-10-08 18:10:26 -04:00
|
|
|
this._focusConnectionState$.next({
|
2025-10-07 16:24:02 +02:00
|
|
|
state: "FailedToStart",
|
|
|
|
|
error: error instanceof Error ? error : new Error(`${error}`),
|
|
|
|
|
focus: this.localTransport,
|
|
|
|
|
});
|
2025-10-01 10:06:43 +02:00
|
|
|
throw error;
|
|
|
|
|
}
|
2025-08-28 13:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-30 17:02:48 +02:00
|
|
|
protected async getSFUConfigWithOpenID(): Promise<SFUConfig> {
|
|
|
|
|
return await getSFUConfigWithOpenID(
|
|
|
|
|
this.client,
|
2025-10-07 10:33:31 +02:00
|
|
|
this.localTransport.livekit_service_url,
|
2025-10-07 16:24:02 +02:00
|
|
|
this.localTransport.livekit_alias,
|
|
|
|
|
);
|
2025-09-30 17:02:48 +02: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-09-26 13:20:55 -04:00
|
|
|
if (this.stopped) return;
|
2025-10-01 16:39:21 +02:00
|
|
|
await this.livekitRoom.disconnect();
|
2025-10-08 18:10:26 -04:00
|
|
|
this._focusConnectionState$.next({
|
2025-10-07 16:24:02 +02:00
|
|
|
state: "Stopped",
|
|
|
|
|
focus: this.localTransport,
|
|
|
|
|
});
|
2025-08-28 13:52:12 +02:00
|
|
|
this.stopped = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 11:33:45 +02:00
|
|
|
/**
|
|
|
|
|
* An observable of the participants that are publishing on this connection.
|
2025-10-08 18:17:42 -04:00
|
|
|
* This is derived from `participantsIncludingSubscribers$` and `remoteTransports$`.
|
2025-09-30 11:33:45 +02:00
|
|
|
* 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
|
|
|
|
|
|
|
|
/**
|
2025-09-30 17:02:48 +02:00
|
|
|
* The focus server to connect to.
|
2025-09-30 11:33:45 +02:00
|
|
|
*/
|
2025-10-07 10:33:31 +02:00
|
|
|
public readonly localTransport: LivekitTransport;
|
2025-08-28 17:45:14 +02:00
|
|
|
|
2025-09-30 17:02:48 +02:00
|
|
|
private readonly client: OpenIDClientParts;
|
2025-09-30 11:33:45 +02:00
|
|
|
/**
|
|
|
|
|
* Creates a new connection to a matrix RTC LiveKit backend.
|
|
|
|
|
*
|
2025-09-30 17:02:48 +02:00
|
|
|
* @param livekitRoom - LiveKit room instance to use.
|
|
|
|
|
* @param opts - Connection options {@link ConnectionOpts}.
|
|
|
|
|
*
|
2025-09-30 11:33:45 +02:00
|
|
|
*/
|
2025-09-30 17:02:48 +02:00
|
|
|
protected constructor(
|
|
|
|
|
public readonly livekitRoom: LivekitRoom,
|
|
|
|
|
opts: ConnectionOpts,
|
2025-08-28 17:45:14 +02:00
|
|
|
) {
|
2025-10-07 16:24:02 +02:00
|
|
|
const { transport, client, scope, remoteTransports$ } = opts;
|
2025-09-30 17:02:48 +02:00
|
|
|
|
2025-10-07 10:33:31 +02:00
|
|
|
this.localTransport = transport;
|
2025-09-30 17:02:48 +02:00
|
|
|
this.client = client;
|
|
|
|
|
|
|
|
|
|
const participantsIncludingSubscribers$ = scope.behavior(
|
2025-08-28 17:45:14 +02:00
|
|
|
connectedParticipantsObserver(this.livekitRoom),
|
2025-10-07 16:24:02 +02:00
|
|
|
[],
|
2025-08-28 17:45:14 +02:00
|
|
|
);
|
|
|
|
|
|
2025-09-30 17:02:48 +02:00
|
|
|
this.publishingParticipants$ = scope.behavior(
|
2025-09-25 21:29:02 -04:00
|
|
|
combineLatest(
|
2025-10-07 10:33:31 +02:00
|
|
|
[participantsIncludingSubscribers$, remoteTransports$],
|
2025-10-03 14:43:22 -04:00
|
|
|
(participants, remoteTransports) =>
|
|
|
|
|
remoteTransports
|
2025-08-28 15:32:46 +02:00
|
|
|
// Find all members that claim to publish on this connection
|
2025-10-03 14:43:22 -04:00
|
|
|
.flatMap(({ membership, transport }) =>
|
|
|
|
|
transport.livekit_service_url ===
|
2025-10-07 10:33:31 +02:00
|
|
|
this.localTransport.livekit_service_url
|
2025-08-28 15:32:46 +02:00
|
|
|
? [membership]
|
2025-10-07 16:24:02 +02:00
|
|
|
: [],
|
2025-08-28 15:32:46 +02:00
|
|
|
)
|
2025-10-03 19:14:48 -04:00
|
|
|
// Pair with their associated LiveKit participant (if any)
|
2025-10-07 10:33:31 +02:00
|
|
|
// Uses flatMap to filter out memberships with no associated rtc participant ([])
|
2025-08-28 17:45:14 +02:00
|
|
|
.flatMap((membership) => {
|
2025-10-03 19:14:48 -04:00
|
|
|
const id = `${membership.sender}:${membership.deviceId}`;
|
|
|
|
|
const participant = participants.find((p) => p.identity === id);
|
2025-08-28 17:45:14 +02:00
|
|
|
return participant ? [{ participant, membership }] : [];
|
2025-08-28 15:32:46 +02:00
|
|
|
}),
|
2025-08-28 13:52:12 +02:00
|
|
|
),
|
2025-10-07 16:24:02 +02:00
|
|
|
[],
|
2025-08-28 13:52:12 +02:00
|
|
|
);
|
2025-10-01 10:06:43 +02:00
|
|
|
|
2025-10-07 16:24:02 +02:00
|
|
|
scope
|
|
|
|
|
.behavior<ConnectionState>(connectionStateObserver(this.livekitRoom))
|
|
|
|
|
.subscribe((connectionState) => {
|
2025-10-08 18:10:26 -04:00
|
|
|
const current = this._focusConnectionState$.value;
|
2025-10-07 16:24:02 +02:00
|
|
|
// Only update the state if we are already connected to the LiveKit room.
|
|
|
|
|
if (current.state === "ConnectedToLkRoom") {
|
2025-10-08 18:10:26 -04:00
|
|
|
this._focusConnectionState$.next({
|
2025-10-07 16:24:02 +02:00
|
|
|
state: "ConnectedToLkRoom",
|
|
|
|
|
connectionState,
|
|
|
|
|
focus: current.focus,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-26 13:20:55 -04:00
|
|
|
|
2025-10-01 16:39:21 +02:00
|
|
|
scope.onEnd(() => void this.stop());
|
2025-08-28 17:45:14 +02:00
|
|
|
}
|
2025-08-28 13:52:12 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-30 17:02:48 +02:00
|
|
|
/**
|
|
|
|
|
* A remote connection to the Matrix RTC LiveKit backend.
|
|
|
|
|
*
|
|
|
|
|
* This connection is used for subscribing to remote participants.
|
|
|
|
|
* It does not publish any local tracks.
|
|
|
|
|
*/
|
|
|
|
|
export class RemoteConnection extends Connection {
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new remote connection to a matrix RTC LiveKit backend.
|
|
|
|
|
* @param opts
|
|
|
|
|
* @param sharedE2eeOption - The shared E2EE options to use for the connection.
|
|
|
|
|
*/
|
2025-10-07 16:24:02 +02:00
|
|
|
public constructor(
|
|
|
|
|
opts: ConnectionOpts,
|
|
|
|
|
sharedE2eeOption: E2EEOptions | undefined,
|
|
|
|
|
) {
|
|
|
|
|
const factory =
|
|
|
|
|
opts.livekitRoomFactory ??
|
|
|
|
|
((options: RoomOptions): LivekitRoom => new LivekitRoom(options));
|
2025-10-01 10:06:43 +02:00
|
|
|
const livekitRoom = factory({
|
2025-09-30 17:02:48 +02:00
|
|
|
...defaultLiveKitOptions,
|
2025-10-07 16:24:02 +02:00
|
|
|
e2ee: sharedE2eeOption,
|
2025-09-30 17:02:48 +02:00
|
|
|
});
|
|
|
|
|
super(livekitRoom, opts);
|
|
|
|
|
}
|
|
|
|
|
}
|