Refactor how we aquire the jwt token for the local user. (only fetch it
once) The local jwt token needs to be aquired via the right endpoint. The endpoint defines how our rtcBackendIdentity is computed. Based on us using sticky events or state events we also need to use the right endpoint. This cannot be done generically in the connection manager. The jwt token now is computed in the localTransport and the resolved sfu config is passed to the connection manager. Add JWT endpoint version and SFU config support Pin matrix-js-sdk to a specific commit and update dev auth image tag. Propagate SFU config and JWT endpoint choice through local transport, ConnectionManager and Connection; add JwtEndpointVersion enum and LocalTransportWithSFUConfig type. Add NO_MATRIX_2 auth error and locale string, thread rtcBackendIdentity through UI props, and include related test, CSS and minor imports updates
This commit is contained in:
@@ -108,13 +108,19 @@ import {
|
||||
enterRTCSession,
|
||||
TransportState,
|
||||
} from "./localMember/LocalMember.ts";
|
||||
import { createLocalTransport$ } from "./localMember/LocalTransport.ts";
|
||||
import {
|
||||
createLocalTransport$,
|
||||
JwtEndpointVersion,
|
||||
} from "./localMember/LocalTransport.ts";
|
||||
import {
|
||||
createMemberships$,
|
||||
membershipsAndTransports$,
|
||||
} from "../SessionBehaviors.ts";
|
||||
import { ECConnectionFactory } from "./remoteMembers/ConnectionFactory.ts";
|
||||
import { createConnectionManager$ } from "./remoteMembers/ConnectionManager.ts";
|
||||
import {
|
||||
type ConnectionManagerData,
|
||||
createConnectionManager$,
|
||||
} from "./remoteMembers/ConnectionManager.ts";
|
||||
import {
|
||||
createMatrixLivekitMembers$,
|
||||
type TaggedParticipant,
|
||||
@@ -263,6 +269,7 @@ export interface CallViewModel {
|
||||
* multiple devices.
|
||||
*/
|
||||
participantCount$: Behavior<number>;
|
||||
allConnections$: Behavior<ConnectionManagerData>;
|
||||
/** Participants sorted by livekit room so they can be used in the audio rendering */
|
||||
livekitRoomItems$: Behavior<LivekitRoomItem[]>;
|
||||
userMedia$: Behavior<UserMedia[]>;
|
||||
@@ -428,14 +435,6 @@ export function createCallViewModel$(
|
||||
memberId: `${userId}:${deviceId}`,
|
||||
};
|
||||
|
||||
const useOldJwtEndpoint$ = scope.behavior(
|
||||
matrixRTCMode$.pipe(
|
||||
map(
|
||||
(v) => v === MatrixRTCMode.Legacy || v === MatrixRTCMode.Compatibility,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const localTransport$ = createLocalTransport$({
|
||||
scope: scope,
|
||||
memberships$: memberships$,
|
||||
@@ -451,7 +450,15 @@ export function createCallViewModel$(
|
||||
matrixRTCSession.delayId ?? null,
|
||||
),
|
||||
roomId: matrixRoom.roomId,
|
||||
useOldJwtEndpoint$,
|
||||
forceJwtEndpoint$: scope.behavior(
|
||||
matrixRTCMode$.pipe(
|
||||
map((v) =>
|
||||
v === MatrixRTCMode.Matrix_2_0
|
||||
? JwtEndpointVersion.Matrix_2_0
|
||||
: JwtEndpointVersion.Legacy,
|
||||
),
|
||||
),
|
||||
),
|
||||
useOldestMember$: scope.behavior(
|
||||
matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Legacy)),
|
||||
),
|
||||
@@ -483,7 +490,6 @@ export function createCallViewModel$(
|
||||
),
|
||||
),
|
||||
remoteTransports$: membershipsAndTransports.transports$,
|
||||
forceOldJwtEndpointForLocalTransport$: useOldJwtEndpoint$,
|
||||
logger: logger,
|
||||
ownMembershipIdentity,
|
||||
});
|
||||
@@ -628,6 +634,9 @@ export function createCallViewModel$(
|
||||
),
|
||||
);
|
||||
|
||||
const allConnections$ = scope.behavior(
|
||||
connectionManager.connectionManagerData$.pipe(map((d) => d.value)),
|
||||
);
|
||||
const livekitRoomItems$ = scope.behavior(
|
||||
matrixLivekitMembers$.pipe(
|
||||
switchMap((members) => {
|
||||
@@ -724,6 +733,7 @@ export function createCallViewModel$(
|
||||
userId,
|
||||
participant satisfies TaggedParticipant as TaggedParticipant, // Widen the type safely
|
||||
connection$,
|
||||
membership$.value,
|
||||
],
|
||||
data: undefined,
|
||||
};
|
||||
@@ -742,7 +752,14 @@ export function createCallViewModel$(
|
||||
// const participantId = membership$.value?.identity;
|
||||
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
||||
yield {
|
||||
keys: [dup, userMediaId, userId, participant, connection$],
|
||||
keys: [
|
||||
dup,
|
||||
userMediaId,
|
||||
userId,
|
||||
participant,
|
||||
connection$,
|
||||
membership$.value,
|
||||
],
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
@@ -756,6 +773,7 @@ export function createCallViewModel$(
|
||||
userId,
|
||||
participant,
|
||||
connection$,
|
||||
membership,
|
||||
) => {
|
||||
const livekitRoom$ = scope.behavior(
|
||||
connection$.pipe(map((c) => c?.livekitRoom)),
|
||||
@@ -773,6 +791,7 @@ export function createCallViewModel$(
|
||||
scope,
|
||||
`${participantId}:${dup}`,
|
||||
userId,
|
||||
membership,
|
||||
participant,
|
||||
options.encryptionSystem,
|
||||
livekitRoom$,
|
||||
@@ -1523,6 +1542,7 @@ export function createCallViewModel$(
|
||||
),
|
||||
null,
|
||||
),
|
||||
allConnections$,
|
||||
participantCount$: participantCount$,
|
||||
handsRaised$: handsRaised$,
|
||||
reactions$: reactions$,
|
||||
|
||||
@@ -39,6 +39,7 @@ import { constant } from "../../Behavior";
|
||||
import { ConnectionManagerData } from "../remoteMembers/ConnectionManager";
|
||||
import { ConnectionState, type Connection } from "../remoteMembers/Connection";
|
||||
import { type Publisher } from "./Publisher";
|
||||
import { type LocalTransportWithSFUConfig } from "./LocalTransport";
|
||||
|
||||
const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
|
||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||
@@ -212,10 +213,11 @@ describe("LocalMembership", () => {
|
||||
|
||||
it("throws error on missing RTC config error", () => {
|
||||
withTestScheduler(({ scope, hot, expectObservable }) => {
|
||||
const localTransport$ = scope.behavior<null | LivekitTransport>(
|
||||
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
|
||||
null,
|
||||
);
|
||||
const localTransport$ =
|
||||
scope.behavior<null | LocalTransportWithSFUConfig>(
|
||||
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
|
||||
null,
|
||||
);
|
||||
|
||||
// we do not need any connection data since we want to fail before reaching that.
|
||||
const mockConnectionManager = {
|
||||
@@ -243,11 +245,23 @@ describe("LocalMembership", () => {
|
||||
});
|
||||
|
||||
const aTransport = {
|
||||
livekit_service_url: "a",
|
||||
} as LivekitTransport;
|
||||
transport: {
|
||||
livekit_service_url: "a",
|
||||
} as LivekitTransport,
|
||||
sfuConfig: {
|
||||
url: "sfu-url",
|
||||
jwt: "sfu-token",
|
||||
},
|
||||
} as LocalTransportWithSFUConfig;
|
||||
const bTransport = {
|
||||
livekit_service_url: "b",
|
||||
} as LivekitTransport;
|
||||
transport: {
|
||||
livekit_service_url: "b",
|
||||
} as LivekitTransport,
|
||||
sfuConfig: {
|
||||
url: "sfu-url",
|
||||
jwt: "sfu-token",
|
||||
},
|
||||
} as LocalTransportWithSFUConfig;
|
||||
|
||||
const connectionTransportAConnected = {
|
||||
livekitRoom: mockLivekitRoom({
|
||||
@@ -391,7 +405,8 @@ describe("LocalMembership", () => {
|
||||
const scope = new ObservableScope();
|
||||
|
||||
const connectionManagerData = new ConnectionManagerData();
|
||||
const localTransport$ = new BehaviorSubject<null | LivekitTransport>(null);
|
||||
const localTransport$ =
|
||||
new BehaviorSubject<null | LocalTransportWithSFUConfig>(null);
|
||||
const connectionManagerData$ = new BehaviorSubject(
|
||||
new Epoch(connectionManagerData),
|
||||
);
|
||||
@@ -468,7 +483,7 @@ describe("LocalMembership", () => {
|
||||
});
|
||||
|
||||
(
|
||||
connectionManagerData2.getConnectionForTransport(aTransport)!
|
||||
connectionManagerData2.getConnectionForTransport(aTransport.transport)!
|
||||
.state$ as BehaviorSubject<ConnectionState>
|
||||
).next(ConnectionState.LivekitConnected);
|
||||
expect(localMembership.localMemberState$.value).toStrictEqual({
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
} from "../remoteMembers/Connection.ts";
|
||||
import { type HomeserverConnected } from "./HomeserverConnected.ts";
|
||||
import { and$ } from "../../../utils/observable.ts";
|
||||
import { type LocalTransportWithSFUConfig } from "./LocalTransport.ts";
|
||||
|
||||
export enum TransportState {
|
||||
/** Not even a transport is available to the LocalMembership */
|
||||
@@ -126,7 +127,7 @@ interface Props {
|
||||
createPublisherFactory: (connection: Connection) => Publisher;
|
||||
joinMatrixRTC: (transport: LivekitTransport) => void;
|
||||
homeserverConnected: HomeserverConnected;
|
||||
localTransport$: Behavior<LivekitTransport | null>;
|
||||
localTransport$: Behavior<LocalTransportWithSFUConfig | null>;
|
||||
matrixRTCSession: Pick<
|
||||
MatrixRTCSession,
|
||||
"updateCallIntent" | "leaveRoomSession"
|
||||
@@ -234,7 +235,9 @@ export const createLocalMembership$ = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
return connectionData.getConnectionForTransport(localTransport);
|
||||
return connectionData.getConnectionForTransport(
|
||||
localTransport.transport,
|
||||
);
|
||||
}),
|
||||
tap((connection) => {
|
||||
logger.info(
|
||||
@@ -533,7 +536,7 @@ export const createLocalMembership$ = ({
|
||||
if (!shouldConnect) return;
|
||||
|
||||
try {
|
||||
joinMatrixRTC(transport);
|
||||
joinMatrixRTC(transport.transport);
|
||||
} catch (error) {
|
||||
logger.error("Error entering RTC session", error);
|
||||
if (error instanceof Error)
|
||||
|
||||
@@ -19,7 +19,7 @@ import { BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import { mockConfig, flushPromises, ownMemberMock } from "../../../utils/test";
|
||||
import { createLocalTransport$ } from "./LocalTransport";
|
||||
import { createLocalTransport$, JwtEndpointVersion } from "./LocalTransport";
|
||||
import { constant } from "../../Behavior";
|
||||
import { Epoch, ObservableScope } from "../../ObservableScope";
|
||||
import {
|
||||
@@ -58,7 +58,7 @@ describe("LocalTransport", () => {
|
||||
getDeviceId: vi.fn(),
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
useOldJwtEndpoint$: constant(false),
|
||||
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
await flushPromises();
|
||||
@@ -98,7 +98,7 @@ describe("LocalTransport", () => {
|
||||
getDeviceId: vi.fn(),
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
useOldJwtEndpoint$: constant(false),
|
||||
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
localTransport$.subscribe(
|
||||
@@ -140,7 +140,7 @@ describe("LocalTransport", () => {
|
||||
baseUrl: "https://lk.example.org",
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
useOldJwtEndpoint$: constant(false),
|
||||
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
|
||||
@@ -186,7 +186,7 @@ describe("LocalTransport", () => {
|
||||
baseUrl: "https://lk.example.org",
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
useOldJwtEndpoint$: constant(false),
|
||||
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("LocalTransport", () => {
|
||||
scope,
|
||||
roomId: "!example_room_id",
|
||||
useOldestMember$: constant(false),
|
||||
useOldJwtEndpoint$: constant(false),
|
||||
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
|
||||
delayId$: constant(null),
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
@@ -333,7 +333,7 @@ describe("LocalTransport", () => {
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
roomId: "!example_room_id",
|
||||
useOldestMember$: constant(false),
|
||||
useOldJwtEndpoint$: constant(false),
|
||||
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
|
||||
delayId$: constant(null),
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
|
||||
@@ -31,9 +31,11 @@ import { Config } from "../../../config/Config.ts";
|
||||
import {
|
||||
FailToGetOpenIdToken,
|
||||
MatrixRTCTransportMissingError,
|
||||
NoMatrix2AuthorizationService,
|
||||
} from "../../../utils/errors.ts";
|
||||
import {
|
||||
getSFUConfigWithOpenID,
|
||||
type SFUConfig,
|
||||
type OpenIDClientParts,
|
||||
} from "../../../livekit/openIDSFU.ts";
|
||||
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
||||
@@ -57,10 +59,25 @@ interface Props {
|
||||
OpenIDClientParts;
|
||||
roomId: string;
|
||||
useOldestMember$: Behavior<boolean>;
|
||||
useOldJwtEndpoint$: Behavior<boolean>;
|
||||
forceJwtEndpoint$: Behavior<JwtEndpointVersion>;
|
||||
delayId$: Behavior<string | null>;
|
||||
}
|
||||
|
||||
export enum JwtEndpointVersion {
|
||||
Legacy = "legacy",
|
||||
Matrix_2_0 = "matrix_2_0",
|
||||
}
|
||||
|
||||
export interface LocalTransportWithSFUConfig {
|
||||
transport: LivekitTransport;
|
||||
sfuConfig: SFUConfig;
|
||||
}
|
||||
export function isLocalTransportWithSFUConfig(
|
||||
obj: LivekitTransport | LocalTransportWithSFUConfig,
|
||||
): obj is LocalTransportWithSFUConfig {
|
||||
return "transport" in obj && "sfuConfig" in obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is responsible for managing the local transport.
|
||||
* "Which transport is the local member going to use"
|
||||
@@ -81,22 +98,40 @@ export const createLocalTransport$ = ({
|
||||
client,
|
||||
roomId,
|
||||
useOldestMember$,
|
||||
useOldJwtEndpoint$,
|
||||
forceJwtEndpoint$,
|
||||
delayId$,
|
||||
}: Props): Behavior<LivekitTransport | null> => {
|
||||
}: Props): Behavior<LocalTransportWithSFUConfig | null> => {
|
||||
/**
|
||||
* The transport over which we should be actively publishing our media.
|
||||
* undefined when not joined.
|
||||
*/
|
||||
const oldestMemberTransport$ = scope.behavior(
|
||||
combineLatest([memberships$, useOldJwtEndpoint$]).pipe(
|
||||
map(([memberships, forceOldJwtEndpoint]) => {
|
||||
combineLatest([memberships$]).pipe(
|
||||
map(([memberships]) => {
|
||||
const oldestMember = memberships.value[0];
|
||||
const transport = oldestMember?.getTransport(memberships.value[0]);
|
||||
if (!transport) return null;
|
||||
return transport;
|
||||
}),
|
||||
first((t) => t != null && isLivekitTransport(t)),
|
||||
switchMap((transport) => {
|
||||
// Get the open jwt token to connect to the sfu
|
||||
const computeLocalTransportWithSFUConfig =
|
||||
async (): Promise<LocalTransportWithSFUConfig> => {
|
||||
return {
|
||||
transport,
|
||||
sfuConfig: await getSFUConfigWithOpenID(
|
||||
client,
|
||||
ownMembershipIdentity,
|
||||
transport.livekit_service_url,
|
||||
roomId,
|
||||
{ forceJwtEndpoint: JwtEndpointVersion.Legacy },
|
||||
logger,
|
||||
),
|
||||
};
|
||||
};
|
||||
return from(computeLocalTransportWithSFUConfig());
|
||||
}),
|
||||
),
|
||||
null,
|
||||
);
|
||||
@@ -108,19 +143,29 @@ export const createLocalTransport$ = ({
|
||||
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
|
||||
*/
|
||||
const preferredTransport$ = scope.behavior(
|
||||
combineLatest([customLivekitUrl.value$, delayId$, useOldJwtEndpoint$]).pipe(
|
||||
switchMap(([customUrl, delayId, forceOldJwtEndpoint]) =>
|
||||
from(
|
||||
// preferredTransport$ (used for multi sfu) needs to know if we are using the old or new
|
||||
// jwt endpoint (`get_token` vs `sfu/get`) based on that the jwt endpoint will compute the rtcBackendIdentity
|
||||
// differently. (sha(`${userId}|${deviceId}|${memberId}`) vs `${userId}|${deviceId}|${memberId}`)
|
||||
// When using sticky events (we need to use the new endpoint).
|
||||
combineLatest([customLivekitUrl.value$, delayId$, forceJwtEndpoint$]).pipe(
|
||||
switchMap(([customUrl, delayId, forceEndpoint]) => {
|
||||
logger.info(
|
||||
"Creating preferred transport based on: ",
|
||||
customUrl,
|
||||
delayId,
|
||||
forceEndpoint,
|
||||
);
|
||||
return from(
|
||||
makeTransport(
|
||||
client,
|
||||
ownMembershipIdentity,
|
||||
roomId,
|
||||
customUrl,
|
||||
forceOldJwtEndpoint,
|
||||
forceEndpoint,
|
||||
delayId ?? undefined,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
null,
|
||||
);
|
||||
@@ -139,7 +184,9 @@ export const createLocalTransport$ = ({
|
||||
? (oldestMemberTransport ?? preferredTransport)
|
||||
: preferredTransport,
|
||||
),
|
||||
distinctUntilChanged((t1, t2) => areLivekitTransportsEqual(t1, t2)),
|
||||
distinctUntilChanged((t1, t2) =>
|
||||
areLivekitTransportsEqual(t1?.transport ?? null, t2?.transport ?? null),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -161,7 +208,10 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
||||
* @param membership The membership identity of the user.
|
||||
* @param roomId The ID of the room to be connected to.
|
||||
* @param urlFromDevSettings Override URL provided by the user's local config.
|
||||
* @param forceOldJwtEndpoint Whether to force the old JWT endpoint (not hashing the backendIdentity).
|
||||
* @param forceJwtEndpoint Whether to force a specific JWT endpoint
|
||||
* - `Legacy` / `Matrix_2_0`
|
||||
* - `get_token` / `sfu/get`
|
||||
* - not hashing / hashing the backendIdentity
|
||||
* @param delayId the delay id passed to the jwt service.
|
||||
*
|
||||
* @returns A fully validated transport config.
|
||||
@@ -176,26 +226,33 @@ async function makeTransport(
|
||||
membership: CallMembershipIdentityParts,
|
||||
roomId: string,
|
||||
urlFromDevSettings: string | null,
|
||||
forceOldJwtEndpoint: boolean,
|
||||
forceJwtEndpoint: JwtEndpointVersion,
|
||||
delayId?: string,
|
||||
): Promise<LivekitTransport> {
|
||||
): Promise<LocalTransportWithSFUConfig> {
|
||||
logger.trace("Searching for a preferred transport");
|
||||
|
||||
async function doOpenIdAndJWTFromUrl(url: string): Promise<LivekitTransport> {
|
||||
const { livekitAlias } = await getSFUConfigWithOpenID(
|
||||
async function doOpenIdAndJWTFromUrl(
|
||||
url: string,
|
||||
): Promise<LocalTransportWithSFUConfig> {
|
||||
const sfuConfig = await getSFUConfigWithOpenID(
|
||||
client,
|
||||
membership,
|
||||
url,
|
||||
forceOldJwtEndpoint,
|
||||
roomId,
|
||||
client.baseUrl,
|
||||
delayId,
|
||||
{
|
||||
forceJwtEndpoint: forceJwtEndpoint,
|
||||
delayEndpointBaseUrl: client.baseUrl,
|
||||
delayId,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
return {
|
||||
type: "livekit",
|
||||
livekit_service_url: url,
|
||||
livekit_alias: livekitAlias,
|
||||
transport: {
|
||||
type: "livekit",
|
||||
livekit_service_url: url,
|
||||
livekit_alias: sfuConfig.livekitAlias,
|
||||
},
|
||||
sfuConfig,
|
||||
};
|
||||
}
|
||||
// We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
||||
@@ -217,7 +274,7 @@ async function makeTransport(
|
||||
|
||||
async function getFirstUsableTransport(
|
||||
transports: Transport[],
|
||||
): Promise<LivekitTransport | null> {
|
||||
): Promise<LocalTransportWithSFUConfig | null> {
|
||||
for (const potentialTransport of transports) {
|
||||
if (isLivekitTransportConfig(potentialTransport)) {
|
||||
try {
|
||||
@@ -226,8 +283,11 @@ async function makeTransport(
|
||||
potentialTransport.livekit_service_url,
|
||||
);
|
||||
} catch (ex) {
|
||||
// Explictly throw these
|
||||
if (ex instanceof FailToGetOpenIdToken) {
|
||||
// Explictly throw these
|
||||
throw ex;
|
||||
}
|
||||
if (ex instanceof NoMatrix2AuthorizationService) {
|
||||
throw ex;
|
||||
}
|
||||
logger.debug(
|
||||
|
||||
@@ -114,6 +114,7 @@ function setupRemoteConnection(): Connection {
|
||||
client: client,
|
||||
transport: livekitFocus,
|
||||
scope: testScope,
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
livekitRoomFactory: () => fakeLivekitRoom,
|
||||
};
|
||||
|
||||
@@ -138,7 +139,7 @@ function setupRemoteConnection(): Connection {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return new Connection(opts, logger, ownMemberMock);
|
||||
return new Connection(opts, logger);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -155,9 +156,10 @@ describe("Start connection states", () => {
|
||||
client: client,
|
||||
transport: livekitFocus,
|
||||
scope: testScope,
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
livekitRoomFactory: () => fakeLivekitRoom,
|
||||
};
|
||||
const connection = new Connection(opts, logger, ownMemberMock);
|
||||
const connection = new Connection(opts, logger);
|
||||
|
||||
expect(connection.state$.getValue()).toEqual("Initialized");
|
||||
});
|
||||
@@ -170,10 +172,11 @@ describe("Start connection states", () => {
|
||||
client: client,
|
||||
transport: livekitFocus,
|
||||
scope: testScope,
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
livekitRoomFactory: () => fakeLivekitRoom,
|
||||
};
|
||||
|
||||
const connection = new Connection(opts, logger, ownMemberMock);
|
||||
const connection = new Connection(opts, logger);
|
||||
|
||||
const capturedStates: (ConnectionState | Error)[] = [];
|
||||
const s = connection.state$.subscribe((value) => {
|
||||
@@ -220,10 +223,11 @@ describe("Start connection states", () => {
|
||||
client: client,
|
||||
transport: livekitFocus,
|
||||
scope: testScope,
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
livekitRoomFactory: () => fakeLivekitRoom,
|
||||
};
|
||||
|
||||
const connection = new Connection(opts, logger, ownMemberMock);
|
||||
const connection = new Connection(opts, logger);
|
||||
|
||||
const capturedStates: (ConnectionState | Error)[] = [];
|
||||
const s = connection.state$.subscribe((value) => {
|
||||
@@ -277,10 +281,11 @@ describe("Start connection states", () => {
|
||||
client: client,
|
||||
transport: livekitFocus,
|
||||
scope: testScope,
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
livekitRoomFactory: () => fakeLivekitRoom,
|
||||
};
|
||||
|
||||
const connection = new Connection(opts, logger, ownMemberMock);
|
||||
const connection = new Connection(opts, logger);
|
||||
|
||||
const capturedStates: (ConnectionState | Error)[] = [];
|
||||
const s = connection.state$.subscribe((value) => {
|
||||
|
||||
@@ -33,10 +33,21 @@ import {
|
||||
SFURoomCreationRestrictedError,
|
||||
UnknownCallError,
|
||||
} from "../../../utils/errors.ts";
|
||||
import { type JwtEndpointVersion } from "../localMember/LocalTransport.ts";
|
||||
|
||||
export interface ConnectionOpts {
|
||||
/** Whether we always try to connect to this connection via the legacy jwt endpoint. (no hash identity) */
|
||||
forceOldJwtEndpoint?: boolean;
|
||||
/**
|
||||
* 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;
|
||||
/** The media transport to connect to. */
|
||||
transport: LivekitTransport;
|
||||
/** The Matrix client to use for OpenID and SFU config requests. */
|
||||
@@ -132,8 +143,10 @@ export class Connection {
|
||||
try {
|
||||
this._state$.next(ConnectionState.FetchingConfig);
|
||||
// We should already have this information after creating the localTransport.
|
||||
// It would probably be better to forward this here.
|
||||
const { url, jwt } = await this.getSFUConfigWithOpenID();
|
||||
// only call getSFUConfigWithOpenID for connections where we do not have a token yet. (existingJwtTokenData === undefined)
|
||||
const { url, jwt } =
|
||||
this.existingSFUConfig ??
|
||||
(await this.getSFUConfigForRemoteConnection());
|
||||
// If we were stopped while fetching the config, don't proceed to connect
|
||||
if (this.stopped) return;
|
||||
|
||||
@@ -189,17 +202,16 @@ export class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
protected async getSFUConfigWithOpenID(): Promise<SFUConfig> {
|
||||
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
|
||||
return await getSFUConfigWithOpenID(
|
||||
this.client,
|
||||
this.ownMembershipIdentity,
|
||||
this.transport.livekit_service_url,
|
||||
this.forceOldJwtEndpoint,
|
||||
this.transport.livekit_alias,
|
||||
// For the remote members we intentionally do not pass a delayEndpointBaseUrl.
|
||||
undefined,
|
||||
// and no delayId.
|
||||
undefined,
|
||||
// dont pass any custom opts for the subscribe only connections
|
||||
{},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
@@ -222,7 +234,8 @@ export class Connection {
|
||||
|
||||
private readonly client: OpenIDClientParts;
|
||||
private readonly logger: Logger;
|
||||
private readonly forceOldJwtEndpoint: boolean;
|
||||
private readonly ownMembershipIdentity: CallMembershipIdentityParts;
|
||||
private readonly existingSFUConfig?: SFUConfig;
|
||||
/**
|
||||
* Creates a new connection to a matrix RTC LiveKit backend.
|
||||
*
|
||||
@@ -230,12 +243,9 @@ export class Connection {
|
||||
*
|
||||
* @param logger - The logger to use.
|
||||
*/
|
||||
public constructor(
|
||||
opts: ConnectionOpts,
|
||||
logger: Logger,
|
||||
private ownMembershipIdentity: CallMembershipIdentityParts,
|
||||
) {
|
||||
this.forceOldJwtEndpoint = opts.forceOldJwtEndpoint ?? false;
|
||||
public constructor(opts: ConnectionOpts, logger: Logger) {
|
||||
this.ownMembershipIdentity = opts.ownMembershipIdentity;
|
||||
this.existingSFUConfig = opts.existingSFUConfig;
|
||||
this.logger = logger.getChild("[Connection]");
|
||||
this.logger.info(
|
||||
`Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`,
|
||||
|
||||
@@ -20,7 +20,10 @@ import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc/LivekitTransp
|
||||
|
||||
import { type ObservableScope } from "../../ObservableScope.ts";
|
||||
import { Connection } from "./Connection.ts";
|
||||
import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
||||
import type {
|
||||
OpenIDClientParts,
|
||||
SFUConfig,
|
||||
} from "../../../livekit/openIDSFU.ts";
|
||||
import type { MediaDevices } from "../../MediaDevices.ts";
|
||||
import type { Behavior } from "../../Behavior.ts";
|
||||
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||
@@ -29,11 +32,11 @@ import { defaultLiveKitOptions } from "../../../livekit/options.ts";
|
||||
// TODO evaluate if this should be done like the Publisher Factory
|
||||
export interface ConnectionFactory {
|
||||
createConnection(
|
||||
transport: LivekitTransport,
|
||||
scope: ObservableScope,
|
||||
transport: LivekitTransport,
|
||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||
logger: Logger,
|
||||
forceOldJwtEndpoint?: boolean,
|
||||
sfuConfig?: SFUConfig,
|
||||
): Connection;
|
||||
}
|
||||
|
||||
@@ -83,30 +86,30 @@ export class ECConnectionFactory implements ConnectionFactory {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param transport The transport to use for this connection.
|
||||
* @param scope The observable scope (used for clean-up)
|
||||
* @param transport The transport to use for this connection.
|
||||
* @param ownMembershipIdentity required to connect (using the jwt service) with the SFU.
|
||||
* @param logger The logger instance to use for this connection.
|
||||
* @param forceOldJwtEndpoint Use the old JWT endpoint independent of what the sfu supports.
|
||||
* @param sfuConfig optional config in case we already have a token for this connection.
|
||||
* @returns
|
||||
*/
|
||||
public createConnection(
|
||||
transport: LivekitTransport,
|
||||
scope: ObservableScope,
|
||||
transport: LivekitTransport,
|
||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||
logger: Logger,
|
||||
forceOldJwtEndpoint?: boolean,
|
||||
sfuConfig?: SFUConfig,
|
||||
): Connection {
|
||||
return new Connection(
|
||||
{
|
||||
existingSFUConfig: sfuConfig,
|
||||
transport,
|
||||
client: this.client,
|
||||
scope: scope,
|
||||
livekitRoomFactory: this.livekitRoomFactory,
|
||||
forceOldJwtEndpoint,
|
||||
ownMembershipIdentity,
|
||||
},
|
||||
logger,
|
||||
ownMembershipIdentity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,17 @@ import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type RemoteParticipant } from "livekit-client";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
|
||||
import { constant, type Behavior } from "../../Behavior.ts";
|
||||
import { type Behavior } from "../../Behavior.ts";
|
||||
import { type Connection } from "./Connection.ts";
|
||||
import { Epoch, type ObservableScope } from "../../ObservableScope.ts";
|
||||
import { generateItemsWithEpoch } from "../../../utils/observable.ts";
|
||||
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
|
||||
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
||||
import {
|
||||
isLocalTransportWithSFUConfig,
|
||||
type LocalTransportWithSFUConfig,
|
||||
} from "../localMember/LocalTransport.ts";
|
||||
import { type SFUConfig } from "../../../livekit/openIDSFU.ts";
|
||||
|
||||
export class ConnectionManagerData {
|
||||
private readonly store: Map<
|
||||
@@ -66,9 +71,9 @@ export class ConnectionManagerData {
|
||||
interface Props {
|
||||
scope: ObservableScope;
|
||||
connectionFactory: ConnectionFactory;
|
||||
localTransport$: Behavior<LivekitTransport | null>;
|
||||
localTransport$: Behavior<LocalTransportWithSFUConfig | null>;
|
||||
remoteTransports$: Behavior<Epoch<LivekitTransport[]>>;
|
||||
forceOldJwtEndpointForLocalTransport$?: Behavior<boolean>;
|
||||
|
||||
logger: Logger;
|
||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||
}
|
||||
@@ -87,7 +92,7 @@ export interface IConnectionManager {
|
||||
* @param props.remoteTransports$ - All other transports. The connection manager will create connections for each transport. (deduplicated with localTransport$)
|
||||
* @param props.ownMembershipIdentity - The own membership identity to use.
|
||||
* @param props.logger - The logger to use.
|
||||
* @param props.forceOldJwtEndpointForLocalTransport$ - Use the old JWT endpoint independent of what the sfu supports. Only applies for localTransport$.
|
||||
|
||||
*
|
||||
* Each of these behaviors can be interpreted as subscribed list of transports.
|
||||
*
|
||||
@@ -103,7 +108,6 @@ export function createConnectionManager$({
|
||||
connectionFactory,
|
||||
localTransport$,
|
||||
remoteTransports$,
|
||||
forceOldJwtEndpointForLocalTransport$ = constant(false),
|
||||
logger: parentLogger,
|
||||
ownMembershipIdentity,
|
||||
}: Props): IConnectionManager {
|
||||
@@ -118,42 +122,35 @@ export function createConnectionManager$({
|
||||
* It is build based on the list of subscribed transports (`transportsSubscriptions$`).
|
||||
* externally this is modified via `registerTransports()`.
|
||||
*/
|
||||
const transportsWithJwtTag$ = scope.behavior(
|
||||
combineLatest([
|
||||
remoteTransports$,
|
||||
localTransport$,
|
||||
forceOldJwtEndpointForLocalTransport$,
|
||||
]).pipe(
|
||||
// combine local and remote transports into one transport array
|
||||
const localAndRemoteTransports$: Behavior<
|
||||
Epoch<(LivekitTransport | LocalTransportWithSFUConfig)[]>
|
||||
> = scope.behavior(
|
||||
combineLatest([remoteTransports$, localTransport$]).pipe(
|
||||
// Combine local and remote transports into one transport array
|
||||
// and set the forceOldJwtEndpoint property on the local transport
|
||||
map(
|
||||
([
|
||||
remoteTransports,
|
||||
localTransport,
|
||||
forceOldJwtEndpointForLocalTransport,
|
||||
]) => {
|
||||
let localTransportAsArray: (LivekitTransport & {
|
||||
forceOldJwtEndpoint: boolean;
|
||||
})[] = [];
|
||||
if (localTransport) {
|
||||
localTransportAsArray = [
|
||||
{
|
||||
...localTransport,
|
||||
forceOldJwtEndpoint: forceOldJwtEndpointForLocalTransport,
|
||||
},
|
||||
];
|
||||
}
|
||||
return new Epoch(
|
||||
removeDuplicateTransports([
|
||||
...localTransportAsArray,
|
||||
...remoteTransports.value,
|
||||
]) as (LivekitTransport & {
|
||||
forceOldJwtEndpoint?: boolean;
|
||||
})[],
|
||||
remoteTransports.epoch,
|
||||
);
|
||||
},
|
||||
),
|
||||
map(([remoteTransports, localTransport]) => {
|
||||
let localTransportAsArray: LocalTransportWithSFUConfig[] = [];
|
||||
if (localTransport) {
|
||||
localTransportAsArray = [localTransport];
|
||||
}
|
||||
const dedupedRemote = removeDuplicateTransports(remoteTransports.value);
|
||||
const remoteWithoutLocal = dedupedRemote.filter(
|
||||
(transport) =>
|
||||
!localTransportAsArray.find((l) =>
|
||||
areLivekitTransportsEqual(l.transport, transport),
|
||||
),
|
||||
);
|
||||
logger.debug(
|
||||
"remoteWithoutLocal",
|
||||
remoteWithoutLocal,
|
||||
"localTransportAsArray",
|
||||
localTransportAsArray,
|
||||
);
|
||||
return new Epoch(
|
||||
[...localTransportAsArray, ...remoteWithoutLocal],
|
||||
remoteTransports.epoch,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -161,33 +158,51 @@ export function createConnectionManager$({
|
||||
* Connections for each transport in use by one or more session members.
|
||||
*/
|
||||
const connections$ = scope.behavior(
|
||||
transportsWithJwtTag$.pipe(
|
||||
localAndRemoteTransports$.pipe(
|
||||
generateItemsWithEpoch(
|
||||
function* (transports) {
|
||||
for (const transport of transports)
|
||||
yield {
|
||||
keys: [
|
||||
transport.livekit_service_url,
|
||||
transport.livekit_alias,
|
||||
transport.forceOldJwtEndpoint,
|
||||
],
|
||||
data: undefined,
|
||||
};
|
||||
for (const transportWithOrWithoutSfuConfig of transports) {
|
||||
if (
|
||||
isLocalTransportWithSFUConfig(transportWithOrWithoutSfuConfig)
|
||||
) {
|
||||
// This is the local transport only the `LocalTransportWithSFUConfig` has a `sfuConfig` field
|
||||
const { transport, sfuConfig } = transportWithOrWithoutSfuConfig;
|
||||
yield {
|
||||
keys: [
|
||||
transport.livekit_service_url,
|
||||
transport.livekit_alias,
|
||||
sfuConfig,
|
||||
],
|
||||
data: undefined,
|
||||
};
|
||||
} else {
|
||||
const transport = transportWithOrWithoutSfuConfig;
|
||||
yield {
|
||||
keys: [
|
||||
transport.livekit_service_url,
|
||||
transport.livekit_alias,
|
||||
undefined as undefined | SFUConfig,
|
||||
],
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
(scope, _data$, serviceUrl, alias, forceOldJwtEndpoint) => {
|
||||
(scope, _data$, serviceUrl, alias, sfuConfig) => {
|
||||
logger.debug(
|
||||
`Creating connection to ${serviceUrl} (${alias}, forceOldJwtEndpoint: ${forceOldJwtEndpoint})`,
|
||||
`Creating connection to ${serviceUrl} (${alias}, withSfuConfig (local connection?): ${JSON.stringify(sfuConfig) ?? "no config->remote connection"})`,
|
||||
);
|
||||
|
||||
const connection = connectionFactory.createConnection(
|
||||
scope,
|
||||
{
|
||||
type: "livekit",
|
||||
livekit_service_url: serviceUrl,
|
||||
livekit_alias: alias,
|
||||
},
|
||||
scope,
|
||||
ownMembershipIdentity,
|
||||
logger,
|
||||
forceOldJwtEndpoint,
|
||||
sfuConfig,
|
||||
);
|
||||
// Start the connection immediately
|
||||
// Use connection state to track connection progress
|
||||
|
||||
@@ -77,8 +77,8 @@ describe("ECConnectionFactory - Audio inputs options", () => {
|
||||
noise,
|
||||
);
|
||||
ecConnectionFactory.createConnection(
|
||||
exampleTransport,
|
||||
testScope,
|
||||
exampleTransport,
|
||||
ownMemberMock,
|
||||
logger,
|
||||
);
|
||||
@@ -123,8 +123,8 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
||||
false,
|
||||
);
|
||||
ecConnectionFactory.createConnection(
|
||||
exampleTransport,
|
||||
testScope,
|
||||
exampleTransport,
|
||||
ownMemberMock,
|
||||
logger,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user