mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-30 19:39:22 +00:00
Refactor local transport
- use the new domain logic to discover the transport - then try to authenticate - Also fix the bug in multi sfu where active$ not updated on delayId change
This commit is contained in:
@@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk";
|
||||||
retryNetworkOperation,
|
|
||||||
type IOpenIDToken,
|
|
||||||
type MatrixClient,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
@@ -70,6 +66,7 @@ export type OpenIDClientParts = Pick<
|
|||||||
MatrixClient,
|
MatrixClient,
|
||||||
"getOpenIdToken" | "getDeviceId"
|
"getOpenIdToken" | "getDeviceId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a bearer token from the homeserver and then use it to authenticate
|
* Gets a bearer token from the homeserver and then use it to authenticate
|
||||||
* to the matrix RTC backend in order to get acces to the SFU.
|
* to the matrix RTC backend in order to get acces to the SFU.
|
||||||
@@ -113,9 +110,6 @@ export async function getSFUConfigWithOpenID(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
logger?.debug("Got openID token", openIdToken);
|
logger?.debug("Got openID token", openIdToken);
|
||||||
|
|
||||||
logger?.info(`Trying to get JWT for focus ${serviceUrl}...`);
|
|
||||||
|
|
||||||
let sfuConfig: { url: string; jwt: string } | undefined;
|
let sfuConfig: { url: string; jwt: string } | undefined;
|
||||||
|
|
||||||
const tryBothJwtEndpoints = opts?.forceJwtEndpoint === undefined; // This is for SFUs where we do not publish.
|
const tryBothJwtEndpoints = opts?.forceJwtEndpoint === undefined; // This is for SFUs where we do not publish.
|
||||||
@@ -127,7 +121,10 @@ export async function getSFUConfigWithOpenID(
|
|||||||
// if we can use both or if we are forced to use the new one.
|
// if we can use both or if we are forced to use the new one.
|
||||||
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
|
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
|
||||||
try {
|
try {
|
||||||
sfuConfig = await getLiveKitJWTWithDelayDelegation(
|
logger?.info(
|
||||||
|
`Trying to get JWT with delegation for focus ${serviceUrl}...`,
|
||||||
|
);
|
||||||
|
const sfuConfig = await getLiveKitJWTWithDelayDelegation(
|
||||||
membership,
|
membership,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -135,33 +132,36 @@ export async function getSFUConfigWithOpenID(
|
|||||||
opts?.delayEndpointBaseUrl,
|
opts?.delayEndpointBaseUrl,
|
||||||
opts?.delayId,
|
opts?.delayId,
|
||||||
);
|
);
|
||||||
logger?.info(`Got JWT from call's active focus URL.`);
|
|
||||||
|
return extractFullConfigFromToken(sfuConfig);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e);
|
logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e);
|
||||||
if (e instanceof NotSupportedError) {
|
// Make this throw a hard error in case we force the matrix2.0 endpoint.
|
||||||
logger?.warn(
|
if (forceMatrix2Jwt) throw new NoMatrix2AuthorizationService(e as Error);
|
||||||
`Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`,
|
|
||||||
e,
|
// if (e instanceof NotSupportedError) {
|
||||||
);
|
// logger?.warn(
|
||||||
sfuConfig = undefined;
|
// `Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported`,
|
||||||
} else {
|
// e,
|
||||||
logger?.warn(
|
// );
|
||||||
`Failed fetching jwt with matrix 2.0 endpoint other issues ->`,
|
// } else {
|
||||||
`(not going to try with legacy endpoint: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`,
|
// logger?.warn(
|
||||||
e,
|
// `Failed fetching jwt with matrix 2.0 endpoint other issues ->`,
|
||||||
);
|
// `(not going to try with legacy endpoint: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`,
|
||||||
// Make this throw a hard error in case we force the matrix2.0 endpoint.
|
// e,
|
||||||
if (forceMatrix2Jwt)
|
// );
|
||||||
throw new NoMatrix2AuthorizationService(e as Error);
|
// // NEVER get bejond this point if we forceMatrix2 and it failed!
|
||||||
// NEVER get bejond this point if we forceMatrix2 and it failed!
|
// }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED
|
// DEPRECATED
|
||||||
// here we either have a sfuConfig or we alredy exited because of `if (forceMatrix2) throw ...`
|
// here we either have a sfuConfig or we already exited because of `if (forceMatrix2) throw ...`
|
||||||
// The only case we can get into this condition is, if `forceMatrix2` is `false`
|
// The only case we can get into this condition is, if `forceMatrix2` is `false`
|
||||||
if (sfuConfig === undefined) {
|
try {
|
||||||
|
logger?.info(
|
||||||
|
`Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`,
|
||||||
|
);
|
||||||
sfuConfig = await getLiveKitJWT(
|
sfuConfig = await getLiveKitJWT(
|
||||||
membership.deviceId,
|
membership.deviceId,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -169,15 +169,19 @@ export async function getSFUConfigWithOpenID(
|
|||||||
openIdToken,
|
openIdToken,
|
||||||
);
|
);
|
||||||
logger?.info(`Got JWT from call's active focus URL.`);
|
logger?.info(`Got JWT from call's active focus URL.`);
|
||||||
|
return extractFullConfigFromToken(sfuConfig);
|
||||||
|
} catch (ex) {
|
||||||
|
throw new FailToGetOpenIdToken(
|
||||||
|
ex instanceof Error ? ex : new Error(`Unknown error ${ex}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!sfuConfig) {
|
function extractFullConfigFromToken(sfuConfig: {
|
||||||
throw new Error("No `sfuConfig` after trying with old and new endpoints");
|
url: string;
|
||||||
}
|
jwt: string;
|
||||||
|
}): SFUConfig {
|
||||||
// Pull the details from the JWT
|
|
||||||
const [, payloadStr] = sfuConfig.jwt.split(".");
|
const [, payloadStr] = sfuConfig.jwt.split(".");
|
||||||
// TODO: Prefer Uint8Array.fromBase64 when widely available
|
|
||||||
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
||||||
return {
|
return {
|
||||||
jwt: sfuConfig.jwt,
|
jwt: sfuConfig.jwt,
|
||||||
@@ -189,16 +193,15 @@ export async function getSFUConfigWithOpenID(
|
|||||||
livekitIdentity: payload.sub,
|
livekitIdentity: payload.sub,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const RETRIES = 4;
|
|
||||||
async function getLiveKitJWT(
|
async function getLiveKitJWT(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
matrixRoomId: string,
|
matrixRoomId: string,
|
||||||
openIDToken: IOpenIDToken,
|
openIDToken: IOpenIDToken,
|
||||||
): Promise<{ url: string; jwt: string }> {
|
): Promise<{ url: string; jwt: string }> {
|
||||||
let res: Response | undefined;
|
const res = await doNetworkOperationWithRetry(async () => {
|
||||||
await retryNetworkOperation(RETRIES, async () => {
|
return await fetch(livekitServiceURL + "/sfu/get", {
|
||||||
res = await fetch(livekitServiceURL + "/sfu/get", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -211,11 +214,7 @@ async function getLiveKitJWT(
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (!res) {
|
|
||||||
throw new Error(
|
|
||||||
`Network error while connecting to jwt service after ${RETRIES} retries`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("SFU Config fetch failed with status code " + res.status);
|
throw new Error("SFU Config fetch failed with status code " + res.status);
|
||||||
}
|
}
|
||||||
@@ -262,10 +261,8 @@ export async function getLiveKitJWTWithDelayDelegation(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let res: Response | undefined;
|
const res = await doNetworkOperationWithRetry(async () => {
|
||||||
|
return await fetch(livekitServiceURL + "/get_token", {
|
||||||
await retryNetworkOperation(RETRIES, async () => {
|
|
||||||
res = await fetch(livekitServiceURL + "/get_token", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -274,11 +271,6 @@ export async function getLiveKitJWTWithDelayDelegation(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
throw new Error(
|
|
||||||
`Network error while connecting to jwt service after ${RETRIES} retries`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = "SFU Config fetch failed with status code " + res.status;
|
const msg = "SFU Config fetch failed with status code " + res.status;
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ describe("LocalTransport", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.fails(
|
it(
|
||||||
"should not update advertised transport on delayID changes, but active should update",
|
"should not update advertised transport on delayID changes, but active should update",
|
||||||
async () => {
|
async () => {
|
||||||
// For simplicity, we'll just use the config livekit
|
// For simplicity, we'll just use the config livekit
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
isLivekitTransportConfig,
|
isLivekitTransportConfig,
|
||||||
type Transport,
|
|
||||||
type LivekitTransportConfig,
|
type LivekitTransportConfig,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { MatrixError, type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
|
combineLatest,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
first,
|
first,
|
||||||
from,
|
from,
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
} from "../../../livekit/openIDSFU.ts";
|
} from "../../../livekit/openIDSFU.ts";
|
||||||
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
||||||
import { customLivekitUrl } from "../../../settings/settings.ts";
|
import { customLivekitUrl } from "../../../settings/settings.ts";
|
||||||
|
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[LocalTransport]");
|
const logger = rootLogger.getChild("[LocalTransport]");
|
||||||
|
|
||||||
@@ -137,91 +138,75 @@ export const createLocalTransport$ = ({
|
|||||||
forceJwtEndpoint,
|
forceJwtEndpoint,
|
||||||
delayId$,
|
delayId$,
|
||||||
}: Props): LocalTransport => {
|
}: Props): LocalTransport => {
|
||||||
/**
|
// The LiveKit transport in use by the oldest RTC membership. `null` when the
|
||||||
* The LiveKit transport in use by the oldest RTC membership. `null` when the
|
// oldest member has no such transport.
|
||||||
* oldest member has no such transport.
|
const oldestMemberTransport$ = observerOldestMembership$(scope, memberships$);
|
||||||
*/
|
|
||||||
const oldestMemberTransport$ = scope.behavior<LivekitTransportConfig | null>(
|
const transportDiscovery = new RtcTransportAutoDiscovery({
|
||||||
memberships$.pipe(
|
client: client,
|
||||||
map((memberships) => {
|
resolvedConfig: Config.get(),
|
||||||
const oldestMember = memberships.value[0];
|
wellKnownFetcher: AutoDiscovery.getRawClientConfig.bind(AutoDiscovery),
|
||||||
if (oldestMember === undefined) {
|
logger: logger,
|
||||||
logger.info("Oldest member: not found");
|
});
|
||||||
return null;
|
|
||||||
}
|
// Get the preferred transport from the current deployement.
|
||||||
const transport = oldestMember.getTransport(oldestMember);
|
const discoveredTransport$ = from(
|
||||||
if (transport === undefined) {
|
transportDiscovery.discoverPreferredTransport(),
|
||||||
logger.warn(
|
|
||||||
`Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has no transport`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isLivekitTransportConfig(transport)) {
|
|
||||||
logger.warn(
|
|
||||||
`Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has invalid transport`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
"Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has valid transport",
|
|
||||||
);
|
|
||||||
return transport;
|
|
||||||
}),
|
|
||||||
distinctUntilChanged(areLivekitTransportsEqual),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
const preferredConfig$ = customLivekitUrl.value$
|
||||||
* The transport that we would personally prefer to publish on (if not for the
|
.pipe(
|
||||||
* transport preferences of others, perhaps). `null` until fetched and
|
startWith(null),
|
||||||
* validated.
|
switchMap((customUrl) => {
|
||||||
*
|
if (customUrl) {
|
||||||
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
|
return of({
|
||||||
*/
|
type: "livekit",
|
||||||
const preferredTransport$ =
|
livekit_service_url: customUrl,
|
||||||
scope.behavior<LocalTransportWithSFUConfig | null>(
|
} as LivekitTransportConfig);
|
||||||
// preferredTransport$ (used for multi sfu) needs to know if we are using the old or new
|
} else {
|
||||||
// jwt endpoint (`get_token` vs `sfu/get`) based on that the jwt endpoint will compute the rtcBackendIdentity
|
return discoveredTransport$;
|
||||||
// differently. (sha(`${userId}|${deviceId}|${memberId}`) vs `${userId}|${deviceId}|${memberId}`)
|
}
|
||||||
// When using sticky events (we need to use the new endpoint).
|
}),
|
||||||
customLivekitUrl.value$.pipe(
|
)
|
||||||
switchMap((customUrl) =>
|
.pipe(
|
||||||
startWith<LocalTransportWithSFUConfig | null>(null)(
|
map((config) => {
|
||||||
// Fetch the SFU config, and repeat this asynchronously for every
|
if (!config) {
|
||||||
// change in delay ID.
|
// Bubbled up from the preferredConfig$ observable.
|
||||||
delayId$.pipe(
|
throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
|
||||||
switchMap(async (delayId) => {
|
}
|
||||||
logger.info(
|
return config;
|
||||||
"Creating preferred transport based on: ",
|
}),
|
||||||
"customUrl: ",
|
distinctUntilChanged(areLivekitTransportsEqual),
|
||||||
customUrl,
|
|
||||||
"delayId: ",
|
|
||||||
delayId,
|
|
||||||
"forceJwtEndpoint: ",
|
|
||||||
forceJwtEndpoint,
|
|
||||||
);
|
|
||||||
return makeTransport(
|
|
||||||
client,
|
|
||||||
ownMembershipIdentity,
|
|
||||||
roomId,
|
|
||||||
customUrl,
|
|
||||||
forceJwtEndpoint,
|
|
||||||
delayId ?? undefined,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
// We deliberately hide any changes to the SFU config because we
|
|
||||||
// do not actually want the app to reconnect whenever the JWT
|
|
||||||
// token changes due to us delegating a new delayed event. The
|
|
||||||
// initial SFU config for the transport is all the app needs.
|
|
||||||
distinctUntilChanged((prev, next) =>
|
|
||||||
areLivekitTransportsEqual(prev.transport, next.transport),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const preferredTransport$ = combineLatest([preferredConfig$, delayId$]).pipe(
|
||||||
|
switchMap(async ([transport, delayId]) => {
|
||||||
|
try {
|
||||||
|
return await doOpenIdAndJWTFromUrl(
|
||||||
|
transport.livekit_service_url,
|
||||||
|
forceJwtEndpoint,
|
||||||
|
ownMembershipIdentity,
|
||||||
|
roomId,
|
||||||
|
client,
|
||||||
|
delayId ?? undefined,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
e instanceof FailToGetOpenIdToken ||
|
||||||
|
e instanceof NoMatrix2AuthorizationService
|
||||||
|
) {
|
||||||
|
// rethrow as is
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// Catch others and rethrow as FailToGetOpenIdToken that has user friendly message.
|
||||||
|
logger.error("Failed to get JWT from preferred transport", e);
|
||||||
|
throw new FailToGetOpenIdToken(
|
||||||
|
e instanceof Error ? e : new Error(String(e)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (useOldestMember) {
|
if (useOldestMember) {
|
||||||
// --- Oldest member mode ---
|
// --- Oldest member mode ---
|
||||||
return {
|
return {
|
||||||
@@ -232,7 +217,7 @@ export const createLocalTransport$ = ({
|
|||||||
advertised$: scope.behavior(
|
advertised$: scope.behavior(
|
||||||
merge(
|
merge(
|
||||||
oldestMemberTransport$,
|
oldestMemberTransport$,
|
||||||
preferredTransport$.pipe(map((t) => t?.transport ?? null)),
|
preferredTransport$.pipe(map((t) => t.transport)),
|
||||||
).pipe(
|
).pipe(
|
||||||
first((t) => t !== null),
|
first((t) => t !== null),
|
||||||
tap((t) =>
|
tap((t) =>
|
||||||
@@ -268,6 +253,7 @@ export const createLocalTransport$ = ({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -277,210 +263,93 @@ export const createLocalTransport$ = ({
|
|||||||
return {
|
return {
|
||||||
advertised$: scope.behavior(
|
advertised$: scope.behavior(
|
||||||
preferredTransport$.pipe(
|
preferredTransport$.pipe(
|
||||||
map((t) => t?.transport ?? null),
|
map((t) => t.transport),
|
||||||
distinctUntilChanged(areLivekitTransportsEqual),
|
distinctUntilChanged(areLivekitTransportsEqual),
|
||||||
),
|
),
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
active$: preferredTransport$,
|
active$: scope.behavior(preferredTransport$, null),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the correct Transport for the current session, including
|
* Observes the oldest member in the room and returns the transport that it uses if it is a livekit transport.
|
||||||
* validating auth against the service to ensure it's correct.
|
* @param scope - The observable scope.
|
||||||
* Prefers in order:
|
* @param memberships$ - The observable of the call's memberships.'
|
||||||
*
|
|
||||||
|
|
||||||
* 1. The `urlFromDevSettings` value. If this cannot be validated, the function will throw.
|
|
||||||
* 2. The transports returned via the homeserver.
|
|
||||||
* 3. The transports returned via .well-known.
|
|
||||||
* 4. The transport configured in Element Call's config.
|
|
||||||
*
|
|
||||||
* @param client The authenticated Matrix client for the current user
|
|
||||||
* @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 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.
|
|
||||||
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
|
|
||||||
*/
|
*/
|
||||||
async function makeTransport(
|
function observerOldestMembership$(
|
||||||
|
scope: ObservableScope,
|
||||||
|
memberships$: Behavior<Epoch<CallMembership[]>>,
|
||||||
|
): Behavior<LivekitTransportConfig | null> {
|
||||||
|
return scope.behavior<LivekitTransportConfig | null>(
|
||||||
|
memberships$.pipe(
|
||||||
|
map((memberships) => {
|
||||||
|
const oldestMember = memberships.value[0];
|
||||||
|
if (oldestMember === undefined) {
|
||||||
|
logger.info("Oldest member: not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const transport = oldestMember.getTransport(oldestMember);
|
||||||
|
if (transport === undefined) {
|
||||||
|
logger.warn(
|
||||||
|
`Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has no transport`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isLivekitTransportConfig(transport)) {
|
||||||
|
logger.warn(
|
||||||
|
`Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has invalid transport`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has valid transport",
|
||||||
|
);
|
||||||
|
return transport;
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(areLivekitTransportsEqual),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to ensure the user can authenticate with the SFU.
|
||||||
|
//
|
||||||
|
// We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
||||||
|
// only mechanism of validation. This means we will also ask the
|
||||||
|
// homeserver for a OpenID token a few times. Since OpenID tokens are single
|
||||||
|
// use we don't want to risk any issues by re-using a token.
|
||||||
|
//
|
||||||
|
// If the OpenID request were to fail, then it's acceptable for us to fail
|
||||||
|
// this function early, as we assume the homeserver has got some problems.
|
||||||
|
async function doOpenIdAndJWTFromUrl(
|
||||||
|
url: string,
|
||||||
|
forceJwtEndpoint: JwtEndpointVersion,
|
||||||
|
membership: CallMembershipIdentityParts,
|
||||||
|
roomId: string,
|
||||||
client: Pick<
|
client: Pick<
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken"
|
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken"
|
||||||
> &
|
> &
|
||||||
OpenIDClientParts,
|
OpenIDClientParts,
|
||||||
membership: CallMembershipIdentityParts,
|
|
||||||
roomId: string,
|
|
||||||
urlFromDevSettings: string | null,
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion,
|
|
||||||
delayId?: string,
|
delayId?: string,
|
||||||
): Promise<LocalTransportWithSFUConfig> {
|
): Promise<LocalTransportWithSFUConfig> {
|
||||||
logger.trace("Searching for a preferred transport");
|
const sfuConfig = await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
async function doOpenIdAndJWTFromUrl(
|
membership,
|
||||||
url: string,
|
url,
|
||||||
): Promise<LocalTransportWithSFUConfig> {
|
roomId,
|
||||||
const sfuConfig = await getSFUConfigWithOpenID(
|
{
|
||||||
client,
|
forceJwtEndpoint: forceJwtEndpoint,
|
||||||
membership,
|
delayEndpointBaseUrl: client.baseUrl,
|
||||||
url,
|
delayId,
|
||||||
roomId,
|
},
|
||||||
{
|
logger,
|
||||||
forceJwtEndpoint: forceJwtEndpoint,
|
|
||||||
delayEndpointBaseUrl: client.baseUrl,
|
|
||||||
delayId,
|
|
||||||
},
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
transport: {
|
|
||||||
type: "livekit",
|
|
||||||
livekit_service_url: url,
|
|
||||||
},
|
|
||||||
sfuConfig,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
|
||||||
// only mechanism of valiation. This means we will also ask the
|
|
||||||
// homeserver for a OpenID token a few times. Since OpenID tokens are single
|
|
||||||
// use we don't want to risk any issues by re-using a token.
|
|
||||||
//
|
|
||||||
// If the OpenID request were to fail then it's acceptable for us to fail
|
|
||||||
// this function early, as we assume the homeserver has got some problems.
|
|
||||||
|
|
||||||
// DEVTOOL: Highest priority: Load from devtool setting
|
|
||||||
if (urlFromDevSettings !== null) {
|
|
||||||
// Validate that the SFU is up. Otherwise, we want to fail on this
|
|
||||||
// as we don't permit other SFUs.
|
|
||||||
// This will call the jwt/sfu/get endpoint to pre create the livekit room.
|
|
||||||
logger.info("Using LiveKit transport from dev tools: ", urlFromDevSettings);
|
|
||||||
return await doOpenIdAndJWTFromUrl(urlFromDevSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getFirstUsableTransport(
|
|
||||||
transports: Transport[],
|
|
||||||
): Promise<LocalTransportWithSFUConfig | null> {
|
|
||||||
for (const potentialTransport of transports) {
|
|
||||||
if (isLivekitTransportConfig(potentialTransport)) {
|
|
||||||
try {
|
|
||||||
logger.info(
|
|
||||||
`makeTransport: check transport authentication for "${potentialTransport.livekit_service_url}"`,
|
|
||||||
);
|
|
||||||
// This will call the jwt/sfu/get endpoint to pre create the livekit room.
|
|
||||||
return await doOpenIdAndJWTFromUrl(
|
|
||||||
potentialTransport.livekit_service_url,
|
|
||||||
);
|
|
||||||
} catch (ex) {
|
|
||||||
logger.debug(
|
|
||||||
`makeTransport: Could not use SFU service "${potentialTransport.livekit_service_url}" as SFU`,
|
|
||||||
ex,
|
|
||||||
);
|
|
||||||
// Explictly throw these
|
|
||||||
if (ex instanceof FailToGetOpenIdToken) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
if (ex instanceof NoMatrix2AuthorizationService) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
`makeTransport: "${potentialTransport.livekit_service_url}" is not a valid livekit transport as SFU`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MSC4143: Attempt to fetch transports from backend.
|
|
||||||
// TODO: Workaround for an issue in the js-sdk RoomWidgetClient that
|
|
||||||
// is not yet implementing _unstable_getRTCTransports properly (via widget API new action).
|
|
||||||
// For now we just skip this call if we are in a widget.
|
|
||||||
// In widget mode the client is a `RoomWidgetClient` which has no access token (it is using the widget API).
|
|
||||||
// Could be removed once the js-sdk is fixed (https://github.com/matrix-org/matrix-js-sdk/issues/5245)
|
|
||||||
const isSPA = !!client.getAccessToken();
|
|
||||||
if (isSPA && "_unstable_getRTCTransports" in client) {
|
|
||||||
logger.info(
|
|
||||||
"makeTransport: First try to use getRTCTransports end point ...",
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
// TODO This should also check for server support?
|
|
||||||
const transportList = await client._unstable_getRTCTransports();
|
|
||||||
const selectedTransport = await getFirstUsableTransport(transportList);
|
|
||||||
if (selectedTransport) {
|
|
||||||
logger.info(
|
|
||||||
"makeTransport: ...Using backend-configured (client.getRTCTransports) SFU",
|
|
||||||
selectedTransport,
|
|
||||||
);
|
|
||||||
return selectedTransport;
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
if (ex instanceof MatrixError && ex.httpStatus === 404) {
|
|
||||||
// Expected, this is an unstable endpoint and it's not required.
|
|
||||||
// There will be expected 404 errors in the console. When we check if synapse supports the endpoint.
|
|
||||||
logger.debug(
|
|
||||||
"Matrix homeserver does not provide any RTC transports via `/rtc/transports` (will retry with well-known.)",
|
|
||||||
);
|
|
||||||
} else if (ex instanceof FailToGetOpenIdToken) {
|
|
||||||
throw ex;
|
|
||||||
} else {
|
|
||||||
// We got an error that wasn't just missing support for the feature, so log it loudly.
|
|
||||||
logger.error(
|
|
||||||
"Unexpected error fetching RTC transports from backend",
|
|
||||||
ex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`makeTransport: Trying to get transports from .well-known/matrix/client on domain ${client.getDomain()} ...`,
|
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
// Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available.
|
transport: {
|
||||||
const domain = client.getDomain();
|
type: "livekit",
|
||||||
if (domain) {
|
livekit_service_url: url,
|
||||||
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
},
|
||||||
// been fully configured and started
|
sfuConfig,
|
||||||
const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[
|
};
|
||||||
FOCI_WK_KEY
|
|
||||||
];
|
|
||||||
const selectedTransport = Array.isArray(wellKnownFoci)
|
|
||||||
? await getFirstUsableTransport(wellKnownFoci)
|
|
||||||
: null;
|
|
||||||
if (selectedTransport) {
|
|
||||||
logger.info("Using .well-known SFU", selectedTransport);
|
|
||||||
return selectedTransport;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`makeTransport: No valid transport found via backend or .well-known, falling back to config if available.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// CONFIG: Least prioritized; Load from config file
|
|
||||||
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
|
||||||
if (urlFromConf) {
|
|
||||||
try {
|
|
||||||
// This will call the jwt/sfu/get endpoint to pre create the livekit room.
|
|
||||||
logger.info("Using config SFU", urlFromConf);
|
|
||||||
return await doOpenIdAndJWTFromUrl(urlFromConf);
|
|
||||||
} catch (ex) {
|
|
||||||
if (ex instanceof FailToGetOpenIdToken) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
logger.error("Failed to validate config SFU", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we do not have returned a transport by now we throw an error
|
|
||||||
throw new MatrixRTCTransportMissingError(domain ?? "");
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user