lots of fixup in the new classes
This commit is contained in:
@@ -41,7 +41,7 @@ import {
|
||||
import { ObservableScope } from "../ObservableScope.ts";
|
||||
import { type OpenIDClientParts } from "../../livekit/openIDSFU.ts";
|
||||
import { FailToGetOpenIdToken } from "../../utils/errors.ts";
|
||||
import { PublishConnection } from "../ownMember/PublishConnection.ts";
|
||||
import { PublishConnection } from "../ownMember/Publisher.ts";
|
||||
import { mockMediaDevices, mockMuteStates } from "../../utils/test.ts";
|
||||
import type { ProcessorState } from "../../livekit/TrackProcessorContext.tsx";
|
||||
import { type MuteStates } from "../MuteStates.ts";
|
||||
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
import {
|
||||
ConnectionError,
|
||||
type ConnectionState as LivekitConenctionState,
|
||||
type E2EEOptions,
|
||||
Room as LivekitRoom,
|
||||
type RoomOptions,
|
||||
type Room as LivekitRoom,
|
||||
type Participant,
|
||||
RoomEvent,
|
||||
} from "livekit-client";
|
||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { BehaviorSubject, combineLatest, type Observable } from "rxjs";
|
||||
import { BehaviorSubject, type Observable } from "rxjs";
|
||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import {
|
||||
@@ -29,7 +27,6 @@ import {
|
||||
} from "../../livekit/openIDSFU.ts";
|
||||
import { type Behavior } from "../Behavior.ts";
|
||||
import { type ObservableScope } from "../ObservableScope.ts";
|
||||
import { defaultLiveKitOptions } from "../../livekit/options.ts";
|
||||
import {
|
||||
InsufficientCapacityError,
|
||||
SFURoomCreationRestrictedError,
|
||||
@@ -44,19 +41,9 @@ export interface ConnectionOpts {
|
||||
client: OpenIDClientParts;
|
||||
/** The observable scope to use for this connection. */
|
||||
scope: ObservableScope;
|
||||
/**
|
||||
* An observable of the current RTC call memberships and their associated transports.
|
||||
* Used to differentiate between publishing and subscribging participants on each connection.
|
||||
* Used to find out which rtc member should upload to this connection (publishingParticipants$).
|
||||
* The livekit room gives access to all the users subscribing to this connection, we need
|
||||
* to filter out the ones that are uploading to this connection.
|
||||
*/
|
||||
// membershipsWithTransport$: Behavior<
|
||||
// { membership: CallMembership; transport: LivekitTransport }[]
|
||||
// >;
|
||||
|
||||
/** Optional factory to create the LiveKit room, mainly for testing purposes. */
|
||||
livekitRoomFactory?: (options?: RoomOptions) => LivekitRoom;
|
||||
livekitRoomFactory: () => LivekitRoom;
|
||||
}
|
||||
|
||||
export type ConnectionState =
|
||||
@@ -173,6 +160,7 @@ export class Connection {
|
||||
this.transport.livekit_alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the connection.
|
||||
*
|
||||
@@ -195,10 +183,7 @@ export class Connection {
|
||||
* It filters the participants to only those that are associated with a membership that claims to publish on this connection.
|
||||
*/
|
||||
|
||||
public readonly publishingParticipants$: Behavior<PublishingParticipant[]>;
|
||||
public readonly participantsWithPublishTrack$: Behavior<
|
||||
PublishingParticipant[]
|
||||
>;
|
||||
public readonly participantsWithTrack$: Behavior<PublishingParticipant[]>;
|
||||
|
||||
/**
|
||||
* The media transport to connect to.
|
||||
@@ -206,6 +191,8 @@ export class Connection {
|
||||
public readonly transport: LivekitTransport;
|
||||
|
||||
private readonly client: OpenIDClientParts;
|
||||
public readonly livekitRoom: LivekitRoom;
|
||||
|
||||
/**
|
||||
* Creates a new connection to a matrix RTC LiveKit backend.
|
||||
*
|
||||
@@ -213,20 +200,17 @@ export class Connection {
|
||||
* @param opts - Connection options {@link ConnectionOpts}.
|
||||
*
|
||||
*/
|
||||
protected constructor(
|
||||
public readonly livekitRoom: LivekitRoom,
|
||||
opts: ConnectionOpts,
|
||||
logger?: Logger,
|
||||
) {
|
||||
public constructor(opts: ConnectionOpts, logger?: Logger) {
|
||||
logger?.info(
|
||||
`[Connection] Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`,
|
||||
);
|
||||
const { transport, client, scope } = opts;
|
||||
|
||||
this.livekitRoom = opts.livekitRoomFactory();
|
||||
this.transport = transport;
|
||||
this.client = client;
|
||||
|
||||
this.participantsWithPublishTrack$ = scope.behavior(
|
||||
this.participantsWithTrack$ = scope.behavior(
|
||||
connectedParticipantsObserver(
|
||||
this.livekitRoom,
|
||||
// VALR: added that while I think about it
|
||||
@@ -243,30 +227,3 @@ export class Connection {
|
||||
scope.onEnd(() => void this.stop());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public constructor(
|
||||
opts: ConnectionOpts,
|
||||
sharedE2eeOption: E2EEOptions | undefined,
|
||||
) {
|
||||
const factory =
|
||||
opts.livekitRoomFactory ??
|
||||
((options: RoomOptions): LivekitRoom => new LivekitRoom(options));
|
||||
const livekitRoom = factory({
|
||||
...defaultLiveKitOptions,
|
||||
e2ee: sharedE2eeOption,
|
||||
});
|
||||
super(livekitRoom, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,21 @@ import { BehaviorSubject, combineLatest, map, switchMap } from "rxjs";
|
||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||
import {
|
||||
type E2EEOptions,
|
||||
type Room as LivekitRoom,
|
||||
Room as LivekitRoom,
|
||||
type Participant as LivekitParticipant,
|
||||
type RoomOptions,
|
||||
} from "livekit-client";
|
||||
import { type MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
import { type Behavior } from "../Behavior";
|
||||
import { type Connection, RemoteConnection } from "./Connection";
|
||||
import { Connection } from "./Connection";
|
||||
import { type ObservableScope } from "../ObservableScope";
|
||||
import { generateKeyed$ } from "../../utils/observable";
|
||||
import { areLivekitTransportsEqual } from "./matrixLivekitMerger";
|
||||
import { getUrlParams } from "../../UrlParams";
|
||||
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
||||
import { type MediaDevices } from "../MediaDevices";
|
||||
import { defaultLiveKitOptions } from "../../livekit/options";
|
||||
|
||||
export type ParticipantByMemberIdMap = Map<
|
||||
ParticipantId,
|
||||
@@ -33,25 +38,57 @@ export type ParticipantByMemberIdMap = Map<
|
||||
// multiple times to several livekit rooms.
|
||||
{ participant: LivekitParticipant; connection: Connection }[]
|
||||
>;
|
||||
|
||||
// - write test for scopes (do we really need to bind scope)
|
||||
// TODO - write test for scopes (do we really need to bind scope)
|
||||
export class ConnectionManager {
|
||||
/**
|
||||
* The transport to use for publishing.
|
||||
* This extends the list of tranports
|
||||
*/
|
||||
private publishTransport$ = new BehaviorSubject<LivekitTransport | undefined>(
|
||||
undefined,
|
||||
);
|
||||
private livekitRoomFactory: () => LivekitRoom;
|
||||
public constructor(
|
||||
private client: MatrixClient,
|
||||
private scope: ObservableScope,
|
||||
private devices: MediaDevices,
|
||||
private processorState: ProcessorState,
|
||||
private e2eeLivekitOptions$: Behavior<E2EEOptions | undefined>,
|
||||
private logger?: Logger,
|
||||
livekitRoomFactory?: () => LivekitRoom,
|
||||
) {
|
||||
this.scope = scope;
|
||||
const defaultFactory = (): LivekitRoom =>
|
||||
new LivekitRoom(
|
||||
generateRoomOption(
|
||||
this.devices,
|
||||
this.processorState,
|
||||
this.e2eeLivekitOptions$.value,
|
||||
),
|
||||
);
|
||||
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
|
||||
}
|
||||
|
||||
private transportSubscriptions$ = new BehaviorSubject<
|
||||
/**
|
||||
* A list of Behaviors each containing a LIST of LivekitTransport.
|
||||
* Each of these behaviors can be interpreted as subscribed list of transports.
|
||||
*
|
||||
* Using `registerTransports` independent external modules can control what connections
|
||||
* are created by the ConnectionManager.
|
||||
*
|
||||
* The connection manager will remove all duplicate transports in each subscibed list.
|
||||
*
|
||||
* See `unregisterAllTransports` and `unregisterTransport` for details on how to unsubscribe.
|
||||
*/
|
||||
private readonly transportsSubscriptions$ = new BehaviorSubject<
|
||||
Behavior<LivekitTransport[]>[]
|
||||
>([]);
|
||||
|
||||
private transports$ = this.scope.behavior(
|
||||
this.transportSubscriptions$.pipe(
|
||||
/**
|
||||
* All transports currently managed by the ConnectionManager.
|
||||
*
|
||||
* This list does not include duplicate transports.
|
||||
*
|
||||
* It is build based on the list of subscribed transports (`transportsSubscriptions$`).
|
||||
* externally this is modified via `registerTransports()`.
|
||||
*/
|
||||
private readonly transports$ = this.scope.behavior(
|
||||
this.transportsSubscriptions$.pipe(
|
||||
switchMap((subscriptions) =>
|
||||
combineLatest(subscriptions.map((s) => s.transports)).pipe(
|
||||
combineLatest(subscriptions).pipe(
|
||||
map((transportsNested) => transportsNested.flat()),
|
||||
map(removeDuplicateTransports),
|
||||
),
|
||||
@@ -59,24 +96,6 @@ export class ConnectionManager {
|
||||
),
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private client: MatrixClient,
|
||||
private e2eeLivekitOptions: () => E2EEOptions | undefined,
|
||||
private scope: ObservableScope,
|
||||
private logger?: Logger,
|
||||
private livekitRoomFactory?: () => LivekitRoom,
|
||||
) {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public getOrCreatePublishConnection(
|
||||
transport: LivekitTransport,
|
||||
): Connection | undefined {
|
||||
this.publishTransport$.next(transport);
|
||||
const equalsRequestedTransport = (c: Connection): boolean =>
|
||||
areLivekitTransportsEqual(c.transport, transport);
|
||||
return this.connections$.value.find(equalsRequestedTransport);
|
||||
}
|
||||
/**
|
||||
* Connections for each transport in use by one or more session members.
|
||||
*/
|
||||
@@ -87,16 +106,16 @@ export class ConnectionManager {
|
||||
const createConnection =
|
||||
(
|
||||
transport: LivekitTransport,
|
||||
): ((scope: ObservableScope) => RemoteConnection) =>
|
||||
): ((scope: ObservableScope) => Connection) =>
|
||||
(scope) => {
|
||||
const connection = new RemoteConnection(
|
||||
const connection = new Connection(
|
||||
{
|
||||
transport,
|
||||
client: this.client,
|
||||
scope: scope,
|
||||
livekitRoomFactory: this.livekitRoomFactory,
|
||||
},
|
||||
this.e2eeLivekitOptions(),
|
||||
this.logger,
|
||||
);
|
||||
void connection.start();
|
||||
return connection;
|
||||
@@ -114,15 +133,23 @@ export class ConnectionManager {
|
||||
);
|
||||
|
||||
/**
|
||||
* Add an a Behavior containing a list of transports to this ConnectionManager.
|
||||
*
|
||||
* @param transports$
|
||||
* The intended usage is:
|
||||
* - create a ConnectionManager
|
||||
* - register one `transports$` behavior using registerTransports
|
||||
* - add new connections to the `ConnectionManager` by updating the `transports$` behavior
|
||||
* - remove a single connection by removing the transport.
|
||||
* - remove this subscription by calling `unregisterTransports` and passing
|
||||
* the same `transports$` behavior reference.
|
||||
* @param transports$ The Behavior containing a list of transports to subscribe to.
|
||||
*/
|
||||
public registerTransports(
|
||||
transports$: Behavior<LivekitTransport[]>,
|
||||
): Connection[] {
|
||||
if (!this.transportSubscriptions$.value.some((t$) => t$ === transports$)) {
|
||||
this.transportSubscriptions$.next(
|
||||
this.transportSubscriptions$.value.concat(transports$),
|
||||
if (!this.transportsSubscriptions$.value.some((t$) => t$ === transports$)) {
|
||||
this.transportsSubscriptions$.next(
|
||||
this.transportsSubscriptions$.value.concat(transports$),
|
||||
);
|
||||
}
|
||||
// After updating the subscriptions our connection list is also updated.
|
||||
@@ -135,22 +162,30 @@ export class ConnectionManager {
|
||||
.filter((c) => c !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the given transports.
|
||||
* @param transports$ The behavior to unsubscribe from
|
||||
* @returns
|
||||
*/
|
||||
public unregisterTransports(
|
||||
transports$: Behavior<LivekitTransport[]>,
|
||||
): boolean {
|
||||
const subscriptions = this.transportSubscriptions$.value;
|
||||
const subscriptions = this.transportsSubscriptions$.value;
|
||||
const subscriptionsUnregistered = subscriptions.filter(
|
||||
(t$) => t$ !== transports$,
|
||||
);
|
||||
const canUnregister =
|
||||
subscriptions.length !== subscriptionsUnregistered.length;
|
||||
if (canUnregister)
|
||||
this.transportSubscriptions$.next(subscriptionsUnregistered);
|
||||
this.transportsSubscriptions$.next(subscriptionsUnregistered);
|
||||
return canUnregister;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all transports.
|
||||
*/
|
||||
public unregisterAllTransports(): void {
|
||||
this.transportSubscriptions$.next([]);
|
||||
this.transportsSubscriptions$.next([]);
|
||||
}
|
||||
|
||||
// We have a lost of connections, for each of these these
|
||||
@@ -161,7 +196,7 @@ export class ConnectionManager {
|
||||
switchMap((connections) => {
|
||||
const listsOfParticipantWithConnection = connections.map(
|
||||
(connection) => {
|
||||
return connection.participantsWithPublishTrack$.pipe(
|
||||
return connection.participantsWithTrack$.pipe(
|
||||
map((participants) =>
|
||||
participants.map((p) => ({
|
||||
participant: p,
|
||||
@@ -178,7 +213,13 @@ export class ConnectionManager {
|
||||
),
|
||||
);
|
||||
|
||||
// Filters the livekit participants
|
||||
/**
|
||||
* This field makes the connection manager to behave as close to a single SFU as possible.
|
||||
* Each participant that is found on all connections managed by the manager will be listed.
|
||||
*
|
||||
* They are stored an a map keyed by `participant.identity`
|
||||
* (which is equivalent to the `member.id` field in the `m.rtc.member` event)
|
||||
*/
|
||||
public allParticipantsByMemberId$ = this.scope.behavior(
|
||||
this.allParticipantsWithConnection$.pipe(
|
||||
map((participantsWithConnections) => {
|
||||
@@ -191,10 +232,10 @@ export class ConnectionManager {
|
||||
acc.set(participant.identity, [{ connection, participant }]);
|
||||
} else {
|
||||
// already known
|
||||
// This is user is publishing on several SFUs
|
||||
// This is for users publishing on several SFUs
|
||||
currentVal.push({ connection, participant });
|
||||
this.logger?.info(
|
||||
`Participant ${participant.identity} is publishing on several SFUs ${currentVal.join()}`,
|
||||
`Participant ${participant.identity} is publishing on several SFUs ${currentVal.map((v) => v.connection.transport.livekit_service_url).join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -217,3 +258,37 @@ function removeDuplicateTransports(
|
||||
return acc;
|
||||
}, [] as LivekitTransport[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
||||
*/
|
||||
function generateRoomOption(
|
||||
devices: MediaDevices,
|
||||
processorState: ProcessorState,
|
||||
e2eeLivekitOptions: E2EEOptions | undefined,
|
||||
): RoomOptions {
|
||||
const { controlledAudioDevices } = getUrlParams();
|
||||
return {
|
||||
...defaultLiveKitOptions,
|
||||
videoCaptureDefaults: {
|
||||
...defaultLiveKitOptions.videoCaptureDefaults,
|
||||
deviceId: devices.videoInput.selected$.value?.id,
|
||||
processor: processorState.processor,
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
...defaultLiveKitOptions.audioCaptureDefaults,
|
||||
deviceId: devices.audioInput.selected$.value?.id,
|
||||
},
|
||||
audioOutput: {
|
||||
// When using controlled audio devices, we don't want to set the
|
||||
// deviceId here, because it will be set by the native app.
|
||||
// (also the id does not need to match a browser device id)
|
||||
deviceId: controlledAudioDevices
|
||||
? undefined
|
||||
: devices.audioOutput.selected$.value?.id,
|
||||
},
|
||||
e2ee: e2eeLivekitOptions,
|
||||
// TODO test and consider this:
|
||||
// webAudioMix: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type Room as MatrixRoom } from "matrix-js-sdk/lib/matrix";
|
||||
|
||||
import { type ObservableScope } from "../ObservableScope";
|
||||
import { calculateDisplayName, shouldDisambiguate } from "../../utils/displayname";
|
||||
import {
|
||||
calculateDisplayName,
|
||||
shouldDisambiguate,
|
||||
} from "../../utils/displayname";
|
||||
import { type Behavior } from "../Behavior";
|
||||
|
||||
/**
|
||||
* Displayname for each member of the call. This will disambiguate
|
||||
@@ -21,12 +25,12 @@ import { calculateDisplayName, shouldDisambiguate } from "../../utils/displaynam
|
||||
*/
|
||||
// don't do this work more times than we need to. This is achieved by converting to a behavior:
|
||||
export const memberDisplaynames$ = (
|
||||
scope: ObservableScope,
|
||||
matrixRoom: Room,
|
||||
memberships$: Observable<CallMembership[]>,
|
||||
scope: ObservableScope,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
) =>
|
||||
): Behavior<Map<string, string>> =>
|
||||
scope.behavior(
|
||||
combineLatest(
|
||||
[
|
||||
|
||||
@@ -148,7 +148,7 @@ export class MatrixLivekitMerger {
|
||||
* together when it might change together is what you have to do in RxJS to
|
||||
* avoid reading inconsistent state or observing too many changes.)
|
||||
*/
|
||||
private mapMembershipsToMembershipWithTransport$(): Observable<
|
||||
private mapMembershipsToMembershipWithTransport$(): Behavior<
|
||||
{ membership: CallMembership; transport?: LivekitTransport }[]
|
||||
> {
|
||||
return this.scope.behavior(
|
||||
|
||||
Reference in New Issue
Block a user