This commit is contained in:
Robin
2026-10-03 01:27:05 +02:00
parent 0030371ea7
commit 8a06976494
23 changed files with 232 additions and 297 deletions
+6 -5
View File
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
import { type MatrixClient } from "matrix-js-sdk"; import { 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 OpenIDClientParts } from "./openID.ts";
import { extractFullConfigFromToken, type SFUConfig } from "./types.ts"; import { extractFullConfigFromToken, type SFUConfig } from "./types.ts";
import { doNetworkOperationWithRetry } from "../../utils/matrix.ts"; import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
@@ -16,8 +15,10 @@ import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
// of being hardcoded here. (the legacy flow hardcodes it as well) // of being hardcoded here. (the legacy flow hardcodes it as well)
const SLOT_ID = "m.call#ROOM"; const SLOT_ID = "m.call#ROOM";
export type ClientGetTokenParts = OpenIDClientParts & export type ClientGetTokenParts = Pick<
Pick<MatrixClient, "_unstable_getLivekitToken">; MatrixClient,
"_unstable_getLivekitToken"
>;
export async function getSFUConfig({ export async function getSFUConfig({
client, client,
@@ -35,7 +36,7 @@ export async function getSFUConfig({
url, url,
room_id: roomId, room_id: roomId,
slot_id: "m.call#ROOM", slot_id: "m.call#ROOM",
member: { id: membership.memberId }, member_id: membership.memberId,
}), }),
); );
return extractFullConfigFromToken({ url, jwt: res.jwt }); return extractFullConfigFromToken({ url, jwt: res.jwt });
@@ -64,7 +65,7 @@ export async function delegateDelayedLeave({
url, url,
room_id: roomId, room_id: roomId,
slot_id: SLOT_ID, slot_id: SLOT_ID,
member: { id: membership.memberId }, member_id: membership.memberId,
delay_id: delayId, delay_id: delayId,
}), }),
); );
+31 -40
View File
@@ -8,20 +8,16 @@ Please see LICENSE in the repository root for full details.
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";
import {
getSFUConfig as getSFUConfigWithOpenID,
type OpenIDClientParts,
} from "./openID";
import {
type ClientDelegationParts,
delegateDelayedLeave as delegateDelayedLeaveWithCSApi,
type ClientGetTokenParts,
getSFUConfig as getSFUConfigWithCSApi,
} from "./csApi";
import { type MatrixRTCMode } from "../../config/ConfigOptions";
import { MatrixError } from "matrix-js-sdk"; import { MatrixError } from "matrix-js-sdk";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import * as openID from "./openID";
import * as csApi from "./csApi";
import { type SFUConfig } from "./types"; import { type SFUConfig } from "./types";
import { NoMatrix2AuthorizationService } from "../../utils/errors";
export { ClientOpenIDParts } from "./openID";
export { ClientGetTokenParts, ClientDelegationParts } from "./csApi";
export * from "./types";
/** /**
* Checks whether an error means "this homeserver does not implement the * Checks whether an error means "this homeserver does not implement the
@@ -46,19 +42,19 @@ export async function getSFUConfig({
membership, membership,
transport, transport,
roomId, roomId,
matrixRTCMode, role,
logger, logger,
}: { }: {
client: ClientGetTokenParts & OpenIDClientParts; client: csApi.ClientGetTokenParts & openID.ClientOpenIDParts;
membership: CallMembershipIdentityParts; membership: CallMembershipIdentityParts;
transport: LivekitTransport; transport: UnstableLivekitTransport;
roomId: string; roomId: string;
matrixRTCMode: MatrixRTCMode; role: "publisher" | "subscriber";
logger: Logger; logger: Logger;
}): Promise<SFUConfig> { }): Promise<SFUConfig> {
if ("url" in transport) { if ("url" in transport) {
try { try {
return await getSFUConfigWithCSApi({ return await csApi.getSFUConfig({
client, client,
membership, membership,
url: transport.url, url: transport.url,
@@ -66,19 +62,18 @@ export async function getSFUConfig({
}); });
} catch (e) { } catch (e) {
if (isEndpointUnsupported(e)) { if (isEndpointUnsupported(e)) {
// Alias transport before checking whether we can fall back, otherwise const mayFallBack =
// TypeScript gets confused about its type role === "subscriber" && "livekit_service_url" in transport;
const transport_ = transport; if (mayFallBack) {
if ("livekit_service_url" in transport_) {
logger.warn( logger.warn(
`Homeserver does not support the MSC4195 get_token endpoint, falling back to the OpenID flow with service ${transport_.livekit_service_url}`, `Homeserver does not support the MSC4195 get_token endpoint, falling back to the OpenID flow with service ${transport.livekit_service_url}`,
); );
// Fall through // Fall through
} else { } else {
logger.error( logger.error(
`Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${transport.url}`, `Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${transport.url}`,
); );
throw e; throw new NoMatrix2AuthorizationService(e as Error);
} }
} else { } else {
throw e; throw e;
@@ -86,18 +81,14 @@ export async function getSFUConfig({
} }
} }
if ("livekit_service_url" in transport) { return await openID.getSFUConfig({
return await getSFUConfigWithOpenID(
client, client,
membership, membership,
transport.livekit_service_url, serviceUrl: transport.livekit_service_url,
roomId, roomId,
{ matrixRTCMode }, role,
logger, logger,
); });
}
throw new Error(`Unrecognized transport: ${JSON.stringify(transport)}`);
} }
export async function delegateDelayedLeave({ export async function delegateDelayedLeave({
@@ -106,20 +97,18 @@ export async function delegateDelayedLeave({
transport, transport,
roomId, roomId,
delayId, delayId,
matrixRTCMode,
logger, logger,
}: { }: {
client: ClientDelegationParts & OpenIDClientParts; client: csApi.ClientDelegationParts & openID.ClientOpenIDParts;
membership: CallMembershipIdentityParts; membership: CallMembershipIdentityParts;
transport: LivekitTransport; transport: UnstableLivekitTransport;
roomId: string; roomId: string;
delayId: string; delayId: string;
matrixRTCMode: MatrixRTCMode;
logger: Logger; logger: Logger;
}): Promise<void> { }): Promise<void> {
if ("url" in transport) { if ("url" in transport) {
try { try {
await delegateDelayedLeaveWithCSApi({ await csApi.delegateDelayedLeave({
client, client,
membership, membership,
url: transport.url, url: transport.url,
@@ -136,13 +125,15 @@ export async function delegateDelayedLeave({
} else { } else {
// This will technically cause the service to issue a new JWT token, but // This will technically cause the service to issue a new JWT token, but
// it's safe to discard. We're only interested in triggering delegation. // it's safe to discard. We're only interested in triggering delegation.
await getSFUConfigWithOpenID( await openID.getSFUConfig({
client, client,
membership, membership,
transport.livekit_service_url, serviceUrl: transport.livekit_service_url,
roomId, roomId,
{ matrixRTCMode, delayEndpointBaseUrl: client.baseUrl, delayId }, role: "publisher",
delayEndpointBaseUrl: client.baseUrl,
delayId,
logger, logger,
); });
} }
} }
+2 -2
View File
@@ -17,7 +17,7 @@ import {
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { MatrixError } from "matrix-js-sdk"; import { MatrixError } from "matrix-js-sdk";
import { getSFUConfigWithOpenID, type OpenIDClientParts } from "./openIDSFU"; import { getSFUConfigWithOpenID, type ClientOpenIDParts } from "./openIDSFU";
import { testJWTToken } from "../utils/test-fixtures"; import { testJWTToken } from "../utils/test-fixtures";
import { ownMemberMock } from "../utils/test"; import { ownMemberMock } from "../utils/test";
import { FailToGetOpenIdToken } from "../utils/errors"; import { FailToGetOpenIdToken } from "../utils/errors";
@@ -25,7 +25,7 @@ import { FailToGetOpenIdToken } from "../utils/errors";
const sfuUrl = "https://sfu.example.org"; const sfuUrl = "https://sfu.example.org";
describe("getSFUConfigWithOpenID", () => { describe("getSFUConfigWithOpenID", () => {
let matrixClient: MockedObject<OpenIDClientParts>; let matrixClient: MockedObject<ClientOpenIDParts>;
beforeEach(() => { beforeEach(() => {
fetchMock.catch(404); fetchMock.catch(404);
matrixClient = { matrixClient = {
+46 -58
View File
@@ -13,17 +13,13 @@ import {
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";
import { import { FailToGetOpenIdToken } from "../../utils/errors";
FailToGetOpenIdToken,
NoMatrix2AuthorizationService,
} from "../../utils/errors";
import { doNetworkOperationWithRetry } from "../../utils/matrix"; import { doNetworkOperationWithRetry } from "../../utils/matrix";
import { Config } from "../../config/Config"; import { Config } from "../../config/Config";
import { MatrixRTCMode } from "../../config/ConfigOptions";
import { extractFullConfigFromToken, type SFUConfig } from "./types"; import { extractFullConfigFromToken, type SFUConfig } from "./types";
// The bits we need from MatrixClient // The bits we need from MatrixClient
export type OpenIDClientParts = Pick< export type ClientOpenIDParts = Pick<
MatrixClient, MatrixClient,
"getOpenIdToken" | "getDeviceId" "getOpenIdToken" | "getDeviceId"
>; >;
@@ -47,18 +43,25 @@ export type OpenIDClientParts = Pick<
* @returns Object containing the token information * @returns Object containing the token information
* @throws FailToGetOpenIdToken * @throws FailToGetOpenIdToken
*/ */
export async function getSFUConfig( export async function getSFUConfig({
client: OpenIDClientParts, client,
membership: CallMembershipIdentityParts, membership,
serviceUrl: string, serviceUrl,
roomId: string, roomId,
opts?: { role,
matrixRTCMode?: MatrixRTCMode; delayEndpointBaseUrl,
delayId,
logger,
}: {
client: ClientOpenIDParts;
membership: CallMembershipIdentityParts;
serviceUrl: string;
roomId: string;
role: "publisher" | "subscriber";
delayEndpointBaseUrl?: string; delayEndpointBaseUrl?: string;
delayId?: string; delayId?: string;
}, logger: Logger;
logger?: Logger, }): Promise<SFUConfig> {
): Promise<SFUConfig> {
let openIdToken: IOpenIDToken; let openIdToken: IOpenIDToken;
try { try {
openIdToken = await doNetworkOperationWithRetry(async () => openIdToken = await doNetworkOperationWithRetry(async () =>
@@ -69,65 +72,50 @@ export async function getSFUConfig(
error instanceof Error ? error : new Error("Unknown error"), error instanceof Error ? error : new Error("Unknown error"),
); );
} }
logger?.debug("Got openID token", openIdToken); logger.debug("Got openID token", openIdToken);
let sfuConfig: { url: string; jwt: string } | undefined; let sfuConfig: { url: string; jwt: string } | undefined;
const tryBothJwtEndpoints = opts?.matrixRTCMode === undefined; // This is for SFUs where we do not publish. let endpoint: "default" | "legacy";
switch (role) {
case "publisher":
// When publishing a legacy transport (one with a `livekit_service_url`),
// subscribers will expect our participant identity to use the legacy
// `@user_id:device_id` format. Only the legacy JWT service endpoint
// assigns identities in this format, so we must use it.
endpoint = "legacy";
break;
case "subscriber":
// Use the default endpoint as it's more likely to allow remote access.
endpoint = "default";
break;
}
const forceMatrix2Jwt = opts?.matrixRTCMode === MatrixRTCMode.Matrix_2_0;
// We want to start using the new endpoint (with optional delay delegation)
// if we can use both or if we are forced to use the new one.
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
try { try {
logger?.info( logger.info(
`Trying to get JWT via default endpoint for focus ${serviceUrl}...`, `Trying to get JWT with ${endpoint} endpoint for focus ${serviceUrl}...`,
); );
const sfuConfig = await getLiveKitJWT( sfuConfig = await (
endpoint === "legacy" ? getLiveKitJWTLegacy : getLiveKitJWT
)(
membership, membership,
serviceUrl, serviceUrl,
roomId, roomId,
openIdToken, openIdToken,
opts?.delayEndpointBaseUrl, delayEndpointBaseUrl,
opts?.delayId, delayId,
); );
logger.info(`Got JWT from call's active focus URL.`);
return extractFullConfigFromToken(sfuConfig); return extractFullConfigFromToken(sfuConfig);
} catch (e) { } catch (e) {
logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e); logger.error(`Failed fetching jwt with ${endpoint} endpoint:`, e);
// Make this throw a hard error in case we force the matrix2.0 endpoint.
if (forceMatrix2Jwt) {
throw new NoMatrix2AuthorizationService(e as Error);
}
}
}
// DEPRECATED
// 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`
try {
logger?.info(
`Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`,
);
sfuConfig = await getLiveKitJWTLegacy(
membership.deviceId,
serviceUrl,
roomId,
openIdToken,
opts?.delayEndpointBaseUrl,
opts?.delayId,
);
logger?.info(`Got JWT from call's active focus URL.`);
return extractFullConfigFromToken(sfuConfig);
} catch (ex) {
throw new FailToGetOpenIdToken( throw new FailToGetOpenIdToken(
ex instanceof Error ? ex : new Error(`Unknown error ${ex}`), e instanceof Error ? e : new Error(`Unknown error ${e}`),
); );
} }
} }
async function getLiveKitJWTLegacy( async function getLiveKitJWTLegacy(
deviceId: string, membership: CallMembershipIdentityParts,
livekitServiceURL: string, livekitServiceURL: string,
matrixRoomId: string, matrixRoomId: string,
openIDToken: IOpenIDToken, openIDToken: IOpenIDToken,
@@ -161,7 +149,7 @@ async function getLiveKitJWTLegacy(
// However, the livekit room alias is provided as part of the JWT payload. // However, the livekit room alias is provided as part of the JWT payload.
room: matrixRoomId, room: matrixRoomId,
openid_token: openIDToken, openid_token: openIDToken,
device_id: deviceId, device_id: membership.deviceId,
...delayParts, ...delayParts,
}), }),
}); });
+2 -2
View File
@@ -17,7 +17,7 @@ import {
import { act } from "react"; import { act } from "react";
import { type RoomMember } from "matrix-js-sdk"; import { type RoomMember } from "matrix-js-sdk";
import { import {
type LivekitTransport, type UnstableLivekitTransport,
type CallMembership, type CallMembership,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
@@ -46,7 +46,7 @@ vitest.mock("../useAudioContext");
vitest.mock("../soundUtils"); vitest.mock("../soundUtils");
vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({ vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({
...(await importOriginal()), ...(await importOriginal()),
makeTransport: (): [LivekitTransport] => [exampleTransport], makeTransport: (): [UnstableLivekitTransport] => [exampleTransport],
})); }));
afterEach(() => { afterEach(() => {
@@ -31,7 +31,7 @@ import {
type CallMembership, type CallMembership,
type IRTCNotificationContent, type IRTCNotificationContent,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
type LivekitTransport, type UnstableLivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { deepCompare } from "matrix-js-sdk/lib/utils"; import { deepCompare } from "matrix-js-sdk/lib/utils";
@@ -97,7 +97,7 @@ vi.mock(
"../state/CallViewModel/localMember/localTransport", "../state/CallViewModel/localMember/localTransport",
async (importOriginal) => ({ async (importOriginal) => ({
...(await importOriginal()), ...(await importOriginal()),
makeTransport: async (): Promise<LivekitTransport> => makeTransport: async (): Promise<UnstableLivekitTransport> =>
Promise.resolve(exampleTransport), Promise.resolve(exampleTransport),
}), }),
); );
+5 -7
View File
@@ -43,7 +43,7 @@ import {
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { import {
MembershipManagerEvent, MembershipManagerEvent,
type LivekitTransport, type UnstableLivekitTransport,
type MatrixRTCSession, type MatrixRTCSession,
type RTCCallIntent, type RTCCallIntent,
type RTCNotificationType, type RTCNotificationType,
@@ -568,6 +568,8 @@ export function createCallViewModel$(
// membership advertises `${userId}:${deviceId}` as its `membershipID` // membership advertises `${userId}:${deviceId}` as its `membershipID`
// instead, so a uuid there names a member no peer can resolve. // instead, so a uuid there names a member no peer can resolve.
memberId: memberId:
// TODO: Make this conditional on the transport instead
// or else avoid using it in key messages
matrixRTCMode === MatrixRTCMode.Matrix_2_0 matrixRTCMode === MatrixRTCMode.Matrix_2_0
? uuidv4() ? uuidv4()
: `${userId}:${deviceId}`, : `${userId}:${deviceId}`,
@@ -580,7 +582,6 @@ export function createCallViewModel$(
ownMembershipIdentity, ownMembershipIdentity,
client, client,
roomId: matrixRoom.roomId, roomId: matrixRoom.roomId,
matrixRTCMode,
}), }),
); );
@@ -624,7 +625,7 @@ export function createCallViewModel$(
), ),
muteStates, muteStates,
joinMatrixRTC: ( joinMatrixRTC: (
transport: LivekitTransport, transport: UnstableLivekitTransport,
delayedLeaveTimings: ResolvedDelayedLeaveTimings, delayedLeaveTimings: ResolvedDelayedLeaveTimings,
) => { ) => {
return enterRTCSession( return enterRTCSession(
@@ -646,9 +647,7 @@ export function createCallViewModel$(
mediaDevices, mediaDevices,
muteStates, muteStates,
trackProcessorState$, trackProcessorState$,
logger.getChild( logger.getChild(`[Publisher ${JSON.stringify(connection.transport)}]`),
"[Publisher " + connection.transport.livekit_service_url + "]",
),
controlledAudioDevices, controlledAudioDevices,
); );
}, },
@@ -671,7 +670,6 @@ export function createCallViewModel$(
).pipe(map(([delayId]) => delayId ?? null)), ).pipe(map(([delayId]) => delayId ?? null)),
matrixRTCSession.delayId ?? null, matrixRTCSession.delayId ?? null,
), ),
matrixRTCMode,
logger: logger.getChild(`[${Date.now()}]`), logger: logger.getChild(`[${Date.now()}]`),
}); });
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { import {
Status as RTCMemberStatus, Status as RTCMemberStatus,
type LivekitTransport, type UnstableLivekitTransport,
type MatrixRTCSession, type MatrixRTCSession,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { import {
@@ -121,7 +121,7 @@ const mockedClient = {
}; };
describe("enterRTCSession", () => { describe("enterRTCSession", () => {
const transport: LivekitTransport = { const transport: UnstableLivekitTransport = {
livekit_service_url: "http://my-livekit-service-url.com", livekit_service_url: "http://my-livekit-service-url.com",
type: "livekit", type: "livekit",
}; };
@@ -252,7 +252,7 @@ describe("LocalMembership", () => {
it("throws error on missing RTC config error", () => { it("throws error on missing RTC config error", () => {
withTestScheduler(({ scope, hot, expectObservable }) => { withTestScheduler(({ scope, hot, expectObservable }) => {
const localTransport$ = scope.behavior<null | LivekitTransport>( const localTransport$ = scope.behavior<null | UnstableLivekitTransport>(
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")), hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
null, null,
); );
@@ -317,7 +317,7 @@ describe("LocalMembership", () => {
const mockTransportConfig = { const mockTransportConfig = {
livekit_service_url: "a", livekit_service_url: "a",
} as LivekitTransport; } as UnstableLivekitTransport;
const mockTransport = { const mockTransport = {
transport: mockTransportConfig, transport: mockTransportConfig,
@@ -17,7 +17,7 @@ import {
import { observeParticipantEvents } from "@livekit/components-core"; import { observeParticipantEvents } from "@livekit/components-core";
import { import {
Status as RTCSessionStatus, Status as RTCSessionStatus,
type LivekitTransport, type UnstableLivekitTransport,
type MatrixRTCSession, type MatrixRTCSession,
type RTCCallIntent, type RTCCallIntent,
type RTCNotificationType, type RTCNotificationType,
@@ -82,7 +82,8 @@ import { type LocalTransport } from "./LocalTransport.ts";
import { import {
type ClientDelegationParts, type ClientDelegationParts,
delegateDelayedLeave, delegateDelayedLeave,
} from "../../../livekit/auth/index.ts"; type ClientOpenIDParts,
} from "../../../livekit/auth";
export enum TransportState { export enum TransportState {
/** Not even a transport is available to the LocalMembership */ /** Not even a transport is available to the LocalMembership */
@@ -145,14 +146,14 @@ interface Props {
connectionManager: IConnectionManager; connectionManager: IConnectionManager;
createPublisherFactory: (connection: Connection) => Publisher; createPublisherFactory: (connection: Connection) => Publisher;
joinMatrixRTC: ( joinMatrixRTC: (
transport: LivekitTransport, transport: UnstableLivekitTransport,
delayedLeaveTimings: ResolvedDelayedLeaveTimings, delayedLeaveTimings: ResolvedDelayedLeaveTimings,
) => void; ) => void;
homeserverConnected: HomeserverConnected; homeserverConnected: HomeserverConnected;
roomId: string; roomId: string;
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
localTransport$: Observable<LocalTransport>; localTransport$: Observable<LocalTransport>;
client: ClientDelegationParts; client: ClientDelegationParts & ClientOpenIDParts;
matrixRTCSession: Pick<MatrixRTCSession, "updateCallIntent" | "leave">; matrixRTCSession: Pick<MatrixRTCSession, "updateCallIntent" | "leave">;
/** Whether to hide the screen-sharing button. */ /** Whether to hide the screen-sharing button. */
hideScreensharing: boolean; hideScreensharing: boolean;
@@ -160,7 +161,6 @@ interface Props {
hostBridge: HostBridge; hostBridge: HostBridge;
baseUrl: string; baseUrl: string;
delayId$: Behavior<string | null>; delayId$: Behavior<string | null>;
matrixRTCMode: MatrixRTCMode;
logger: Logger; logger: Logger;
} }
@@ -208,7 +208,6 @@ export const createLocalMembership$ = ({
hostBridge, hostBridge,
ownMembershipIdentity, ownMembershipIdentity,
delayId$, delayId$,
matrixRTCMode,
}: Props): { }: Props): {
/** /**
* This request to start audio and video tracks. * This request to start audio and video tracks.
@@ -714,7 +713,6 @@ export const createLocalMembership$ = ({
transport: joinParams.transport, transport: joinParams.transport,
roomId, roomId,
delayId, delayId,
matrixRTCMode,
logger, logger,
}); });
logger.info("Delayed leave successfully delegated"); logger.info("Delayed leave successfully delegated");
@@ -953,7 +951,7 @@ interface EnterRTCSessionOptions {
* *
* @param rtcSession - The MatrixRTCSession to join. * @param rtcSession - The MatrixRTCSession to join.
* @param ownMembershipIdentity - Options for entering the RTC session. * @param ownMembershipIdentity - Options for entering the RTC session.
* @param transport - The LivekitTransport to use for this session. * @param transport - The UnstableLivekitTransport to use for this session.
* @param options - `encryptMedia`: Whether to encrypt media. `matrixRTCMode`: The * @param options - `encryptMedia`: Whether to encrypt media. `matrixRTCMode`: The
* Matrix RTC mode to use. `delayedLeaveTimings`: The preferred timings for * Matrix RTC mode to use. `delayedLeaveTimings`: The preferred timings for
* delayed leave events. `sendNotificationType`: Whether and what kind of * delayed leave events. `sendNotificationType`: Whether and what kind of
@@ -964,7 +962,7 @@ interface EnterRTCSessionOptions {
export function enterRTCSession( export function enterRTCSession(
rtcSession: MatrixRTCSession, rtcSession: MatrixRTCSession,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
transport: LivekitTransport, transport: UnstableLivekitTransport,
{ {
encryptMedia, encryptMedia,
matrixRTCMode, matrixRTCMode,
@@ -5,9 +5,9 @@ SPDX-License-IdFentifier: 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 { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { type MatrixClient } from "matrix-js-sdk"; import { type MatrixClient } from "matrix-js-sdk";
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import { Config } from "../../../config/Config.ts"; import { Config } from "../../../config/Config.ts";
@@ -16,31 +16,31 @@ import {
MatrixRTCTransportMissingError, MatrixRTCTransportMissingError,
NoMatrix2AuthorizationService, NoMatrix2AuthorizationService,
} from "../../../utils/errors.ts"; } from "../../../utils/errors.ts";
import {
getSFUConfigWithOpenID,
type SFUConfig,
type OpenIDClientParts,
} from "../../../livekit/openIDSFU.ts";
import { customLivekitUrl } from "../../../settings/settings.ts"; import { customLivekitUrl } from "../../../settings/settings.ts";
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
import { type MatrixRTCMode } from "../../../config/ConfigOptions.ts"; import {
getSFUConfig,
type ClientGetTokenParts,
type ClientOpenIDParts,
type SFUConfig,
} from "../../../livekit/auth";
interface Props { interface Props {
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> & client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
OpenIDClientParts; ClientGetTokenParts &
ClientOpenIDParts;
// Used by the jwt service to create the livekit room and compute the livekit alias. // Used by the jwt service to create the livekit room and compute the livekit alias.
roomId: string; roomId: string;
matrixRTCMode: MatrixRTCMode;
} }
export interface LocalTransport { export interface LocalTransport {
transport: LivekitTransport; transport: UnstableLivekitTransport;
sfuConfig: SFUConfig; sfuConfig: SFUConfig;
} }
export function isLocalTransport( export function isLocalTransport(
obj: LivekitTransport | LocalTransport, obj: UnstableLivekitTransport | LocalTransport,
): obj is LocalTransport { ): obj is LocalTransport {
return "transport" in obj && "sfuConfig" in obj; return "transport" in obj && "sfuConfig" in obj;
} }
@@ -58,7 +58,6 @@ export async function getLocalTransport({
ownMembershipIdentity, ownMembershipIdentity,
client, client,
roomId, roomId,
matrixRTCMode,
}: Props): Promise<LocalTransport> { }: Props): Promise<LocalTransport> {
const logger = rootLogger.getChild("[LocalTransport]"); const logger = rootLogger.getChild("[LocalTransport]");
const discovery = new RtcTransportAutoDiscovery({ const discovery = new RtcTransportAutoDiscovery({
@@ -69,7 +68,7 @@ export async function getLocalTransport({
const customUrl = customLivekitUrl.value$.value; const customUrl = customLivekitUrl.value$.value;
// Respect the user's custom URL, if set // Respect the user's custom URL, if set
const transport: LivekitTransport | null = customUrl const transport: UnstableLivekitTransport | null = customUrl
? { type: "livekit", livekit_service_url: customUrl } ? { type: "livekit", livekit_service_url: customUrl }
: await discovery.discoverPreferredTransport(); : await discovery.discoverPreferredTransport();
@@ -77,60 +76,26 @@ export async function getLocalTransport({
throw new MatrixRTCTransportMissingError(client.getDomain() ?? ""); throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
try { try {
return await doOpenIdAndJWTFromUrl( return {
transport, transport,
matrixRTCMode, sfuConfig: await getSFUConfig({
ownMembershipIdentity,
roomId,
client, client,
membership: ownMembershipIdentity,
transport,
roomId,
role: "publisher",
logger, logger,
); }),
};
} catch (e) { } catch (e) {
logger.error( logger.error(
`Failed to authenticate to transport ${transport.livekit_service_url}`, `Failed to authenticate to transport ${JSON.stringify(transport)}`,
e, e,
); );
throw mapAuthErrorToUserFriendlyError(e); throw mapAuthErrorToUserFriendlyError(e);
} }
} }
/**
* 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.
*
* @param transport The transport to authenticate with.
* @param matrixRTCMode Whether to force the JWT endpoint to be used.
* @param membership The identity of the local member.
* @param roomId The room ID to use for the JWT.
* @param client The client to use for the OpenID token.
*
* @throws FailToGetOpenIdToken, NoMatrix2AuthorizationService
*/
async function doOpenIdAndJWTFromUrl(
transport: LivekitTransport,
matrixRTCMode: MatrixRTCMode,
membership: CallMembershipIdentityParts,
roomId: string,
client: Pick<MatrixClient, "_unstable_getRTCTransports"> & OpenIDClientParts,
logger?: Logger,
): Promise<LocalTransport> {
const sfuConfig = await getSFUConfigWithOpenID(
client,
membership,
transport.livekit_service_url,
roomId,
{ matrixRTCMode },
logger,
);
return {
transport,
sfuConfig,
};
}
function mapAuthErrorToUserFriendlyError(e: unknown): Error { function mapAuthErrorToUserFriendlyError(e: unknown): Error {
if ( if (
e instanceof FailToGetOpenIdToken || e instanceof FailToGetOpenIdToken ||
@@ -16,7 +16,7 @@ import {
import { MatrixError } from "matrix-js-sdk"; import { MatrixError } from "matrix-js-sdk";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { import {
type LivekitTransport, type UnstableLivekitTransport,
type Transport, type Transport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
@@ -28,12 +28,12 @@ import {
type DiscoveryClient = RtcTransportAutoDiscoveryProps["client"]; type DiscoveryClient = RtcTransportAutoDiscoveryProps["client"];
const backendTransport: LivekitTransport = { const backendTransport: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://backend.example.org", livekit_service_url: "https://backend.example.org",
}; };
const configTransport: LivekitTransport = { const configTransport: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://config.example.org", livekit_service_url: "https://config.example.org",
}; };
@@ -5,8 +5,8 @@ SPDX-License-IdFentifier: 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 {
isLivekitTransport, isUnstableLivekitTransport,
type LivekitTransport, type UnstableLivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { type MatrixClient } from "matrix-js-sdk"; import { type MatrixClient } from "matrix-js-sdk";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { type Logger } from "matrix-js-sdk/lib/logger";
@@ -40,7 +40,7 @@ export class RtcTransportAutoDiscovery {
this.logger = logger.getChild("[RtcTransportAutoDiscovery]"); this.logger = logger.getChild("[RtcTransportAutoDiscovery]");
} }
public async discoverPreferredTransport(): Promise<LivekitTransport | null> { public async discoverPreferredTransport(): Promise<UnstableLivekitTransport | null> {
// 1) backend transports // 1) backend transports
const backendTransport = await this.tryBackendTransports(); const backendTransport = await this.tryBackendTransports();
if (backendTransport) { if (backendTransport) {
@@ -67,7 +67,7 @@ export class RtcTransportAutoDiscovery {
* This will not throw errors, but instead just log them and return null if the expected config is not found or malformed. * This will not throw errors, but instead just log them and return null if the expected config is not found or malformed.
* @private * @private
*/ */
private async tryBackendTransports(): Promise<LivekitTransport | null> { private async tryBackendTransports(): Promise<UnstableLivekitTransport | null> {
const client = this.client; const client = this.client;
// MSC4143: Attempt to fetch transports from backend. // MSC4143: Attempt to fetch transports from backend.
this.logger.info("First try to use getRTCTransports end point ..."); this.logger.info("First try to use getRTCTransports end point ...");
@@ -75,7 +75,7 @@ export class RtcTransportAutoDiscovery {
const transportList = await doNetworkOperationWithRetry(async () => const transportList = await doNetworkOperationWithRetry(async () =>
client._unstable_getRTCTransports(), client._unstable_getRTCTransports(),
); );
const first = transportList.find(isLivekitTransport); const first = transportList.find(isUnstableLivekitTransport);
if (first) { if (first) {
return first; return first;
} else { } else {
@@ -90,7 +90,7 @@ export class RtcTransportAutoDiscovery {
return null; return null;
} }
private tryConfigTransport(): LivekitTransport | null { private tryConfigTransport(): UnstableLivekitTransport | null {
const url = this.resolvedConfig.livekit?.livekit_service_url; const url = this.resolvedConfig.livekit?.livekit_service_url;
if (url) { if (url) {
return { return {
@@ -30,7 +30,7 @@ import fetchMock from "fetch-mock";
import EventEmitter from "events"; import EventEmitter from "events";
import { type IOpenIDToken } from "matrix-js-sdk"; import { type IOpenIDToken } from "matrix-js-sdk";
import { logger, type Logger } from "matrix-js-sdk/lib/logger"; import { logger, type Logger } from "matrix-js-sdk/lib/logger";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { import {
Connection, Connection,
@@ -38,7 +38,7 @@ import {
type ConnectionOpts, type ConnectionOpts,
} from "./Connection.ts"; } from "./Connection.ts";
import { ObservableScope } from "../../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { type ClientOpenIDParts } from "../../../livekit/openIDSFU.ts";
import { import {
ElementCallError, ElementCallError,
FailToGetOpenIdToken, FailToGetOpenIdToken,
@@ -48,7 +48,7 @@ import { mockRemoteParticipant, ownMemberMock } from "../../../utils/test.ts";
let testScope: ObservableScope; let testScope: ObservableScope;
let client: MockedObject<OpenIDClientParts>; let client: MockedObject<ClientOpenIDParts>;
let fakeLivekitRoom: MockedObject<LivekitRoom>; let fakeLivekitRoom: MockedObject<LivekitRoom>;
@@ -57,14 +57,14 @@ let fakeLocalParticipant: MockedObject<LocalParticipant>;
const ROOM_ID = "!roomID:example.org"; const ROOM_ID = "!roomID:example.org";
const livekitFocus: LivekitTransport = { const livekitFocus: UnstableLivekitTransport = {
livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt",
type: "livekit", type: "livekit",
}; };
function setupTest(): void { function setupTest(): void {
testScope = new ObservableScope(); testScope = new ObservableScope();
client = vi.mocked<OpenIDClientParts>({ client = vi.mocked<ClientOpenIDParts>({
getOpenIdToken: vi.fn().mockResolvedValue({ getOpenIdToken: vi.fn().mockResolvedValue({
access_token: "rYsmGUEwNjKgJYyeNUkZseJN", access_token: "rYsmGUEwNjKgJYyeNUkZseJN",
token_type: "Bearer", token_type: "Bearer",
@@ -72,7 +72,7 @@ function setupTest(): void {
expires_in: 3600, expires_in: 3600,
}), }),
getDeviceId: vi.fn().mockReturnValue("ABCDEF"), getDeviceId: vi.fn().mockReturnValue("ABCDEF"),
} as unknown as OpenIDClientParts); } as unknown as ClientOpenIDParts);
localParticipantEventEmiter = new EventEmitter(); localParticipantEventEmiter = new EventEmitter();
@@ -22,16 +22,11 @@ import {
type Track, type Track,
type TrackPublication, type TrackPublication,
} from "livekit-client"; } from "livekit-client";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { BehaviorSubject, map } from "rxjs"; import { BehaviorSubject, map } from "rxjs";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { type Logger } from "matrix-js-sdk/lib/logger";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import {
getSFUConfigWithOpenID,
type OpenIDClientParts,
type SFUConfig,
} from "../../../livekit/openIDSFU.ts";
import { type Behavior } from "../../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
import { type ObservableScope } from "../../ObservableScope.ts"; import { type ObservableScope } from "../../ObservableScope.ts";
import { import {
@@ -42,6 +37,12 @@ import {
SFURoomCreationRestrictedError, SFURoomCreationRestrictedError,
UnknownCallError, UnknownCallError,
} from "../../../utils/errors.ts"; } from "../../../utils/errors.ts";
import {
type ClientGetTokenParts,
getSFUConfig,
type ClientOpenIDParts,
type SFUConfig,
} from "../../../livekit/auth";
export interface ConnectionOpts { export interface ConnectionOpts {
/** /**
@@ -52,9 +53,9 @@ export interface ConnectionOpts {
/** The identity parts to use on this connection */ /** The identity parts to use on this connection */
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
/** The media transport to connect to. */ /** The media transport to connect to. */
transport: LivekitTransport; transport: UnstableLivekitTransport;
/** The Matrix client to use for OpenID and SFU config requests. */ /** The Matrix client to use for OpenID and SFU config requests. */
client: OpenIDClientParts; client: ClientGetTokenParts & ClientOpenIDParts;
/** The room ID this connection is associated with. */ /** The room ID this connection is associated with. */
roomId: string; roomId: string;
/** The observable scope to use for this connection. */ /** The observable scope to use for this connection. */
@@ -107,7 +108,7 @@ export class Connection {
/** /**
* The media transport to connect to. * The media transport to connect to.
*/ */
public readonly transport: LivekitTransport; public readonly transport: UnstableLivekitTransport;
public readonly livekitRoom: LivekitRoom; public readonly livekitRoom: LivekitRoom;
@@ -137,7 +138,7 @@ export class Connection {
protected stopped = false; protected stopped = false;
// TODO: can we just keep the ConnectionOpts object instead of spreading? // TODO: can we just keep the ConnectionOpts object instead of spreading?
private readonly client: OpenIDClientParts; private readonly client: ClientGetTokenParts & ClientOpenIDParts;
private readonly roomId: string; private readonly roomId: string;
private readonly logger: Logger; private readonly logger: Logger;
private readonly ownMembershipIdentity: CallMembershipIdentityParts; private readonly ownMembershipIdentity: CallMembershipIdentityParts;
@@ -154,10 +155,10 @@ export class Connection {
this.existingSFUConfig = opts.existingSFUConfig; this.existingSFUConfig = opts.existingSFUConfig;
this.roomId = opts.roomId; this.roomId = opts.roomId;
this.logger = logger.getChild( this.logger = logger.getChild(
"[Connection " + opts.transport.livekit_service_url + "]", `[Connection ${JSON.stringify(opts.transport)}]`,
); );
this.logger.info( this.logger.info(
`constructor: ${opts.transport.livekit_service_url} roomId: ${this.roomId} withSfuConfig?: ${opts.existingSFUConfig ? JSON.stringify(opts.existingSFUConfig) : "undefined"}`, `constructor - roomId: ${this.roomId} withSfuConfig?: ${opts.existingSFUConfig ? JSON.stringify(opts.existingSFUConfig) : "undefined"}`,
); );
const { transport, client, scope } = opts; const { transport, client, scope } = opts;
@@ -311,9 +312,7 @@ export class Connection {
this.existingSFUConfig ?? this.existingSFUConfig ??
(await this.getSFUConfigForRemoteConnection()); (await this.getSFUConfigForRemoteConnection());
this.logger.debug( this.logger.debug(
"Starting Connection to: ", "Starting Connection - jwt: ",
this.transport.livekit_service_url,
"jwt: ",
jwt, jwt,
"wss: ", "wss: ",
url, url,
@@ -398,15 +397,14 @@ export class Connection {
protected async getSFUConfigForRemoteConnection(): Promise<SFUConfig> { protected async getSFUConfigForRemoteConnection(): Promise<SFUConfig> {
// This will only be called for sfu's where we do not publish ourselves. // This will only be called for sfu's where we do not publish ourselves.
// For the local connection we will use the existingJwtTokenData // For the local connection we will use the existingJwtTokenData
return await getSFUConfigWithOpenID( return await getSFUConfig({
this.client, client: this.client,
this.ownMembershipIdentity, membership: this.ownMembershipIdentity,
this.transport.livekit_service_url, transport: this.transport,
this.roomId, roomId: this.roomId,
// dont pass any custom opts for the subscribe only connections role: "subscriber",
{}, logger: this.logger,
this.logger, });
);
} }
/** /**
@@ -416,17 +414,13 @@ export class Connection {
* If the connection is already stopped, this is a no-op. * If the connection is already stopped, this is a no-op.
*/ */
public async stop(): Promise<void> { public async stop(): Promise<void> {
this.logger.debug( this.logger.debug("stop: disconnecing from lk room");
`stop: disconnecing from lk room ${this.transport.livekit_service_url}`,
);
if (this.stopped) return; if (this.stopped) return;
// Mark as stopped before disconnecting so that a connect() aborted by the // Mark as stopped before disconnecting so that a connect() aborted by the
// disconnect sees the flag and does not report the abort as an error. // disconnect sees the flag and does not report the abort as an error.
this.stopped = true; this.stopped = true;
await this.livekitRoom.disconnect(); await this.livekitRoom.disconnect();
this._state$.next(ConnectionState.Stopped); this._state$.next(ConnectionState.Stopped);
this.logger.debug( this.logger.debug("stop: DONE disconnecing from lk room");
`stop: DONE disconnecing from lk room ${this.transport.livekit_service_url}`,
);
} }
} }
@@ -16,12 +16,12 @@ import { logger, type Logger } from "matrix-js-sdk/lib/logger";
// imported as inline to support worker when loaded from a cdn (cross domain) // imported as inline to support worker when loaded from a cdn (cross domain)
import E2EEWorker from "livekit-client/e2ee-worker?worker&inline"; import E2EEWorker from "livekit-client/e2ee-worker?worker&inline";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { type ObservableScope } from "../../ObservableScope.ts"; import { type ObservableScope } from "../../ObservableScope.ts";
import { Connection } from "./Connection.ts"; import { Connection } from "./Connection.ts";
import type { import type {
OpenIDClientParts, ClientOpenIDParts,
SFUConfig, SFUConfig,
} from "../../../livekit/openIDSFU.ts"; } from "../../../livekit/openIDSFU.ts";
import type { MediaDevices } from "../../MediaDevices.ts"; import type { MediaDevices } from "../../MediaDevices.ts";
@@ -44,7 +44,7 @@ import {
export interface ConnectionFactory { export interface ConnectionFactory {
createConnection( createConnection(
scope: ObservableScope, scope: ObservableScope,
transport: LivekitTransport, transport: UnstableLivekitTransport,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
logger: Logger, logger: Logger,
sfuConfig?: SFUConfig, sfuConfig?: SFUConfig,
@@ -66,7 +66,7 @@ export class ECConnectionFactory implements ConnectionFactory {
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used. * @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
*/ */
public constructor( public constructor(
private client: OpenIDClientParts, private client: ClientOpenIDParts,
private readonly roomId: string, private readonly roomId: string,
private devices: MediaDevices, private devices: MediaDevices,
private processorState$: Behavior<ProcessorState>, private processorState$: Behavior<ProcessorState>,
@@ -103,7 +103,7 @@ export class ECConnectionFactory implements ConnectionFactory {
*/ */
public createConnection( public createConnection(
scope: ObservableScope, scope: ObservableScope,
transport: LivekitTransport, transport: UnstableLivekitTransport,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
logger: Logger, logger: Logger,
sfuConfig?: SFUConfig, sfuConfig?: SFUConfig,
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { BehaviorSubject, NEVER } from "rxjs"; import { BehaviorSubject, NEVER } from "rxjs";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { type RemoteParticipant } from "livekit-client"; import { type RemoteParticipant } from "livekit-client";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
@@ -19,17 +19,17 @@ import {
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts"; import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts";
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { areUnstableLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
import { type Behavior } from "../../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
// Some test constants // Some test constants
const TRANSPORT_1: LivekitTransport = { const TRANSPORT_1: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://lk.example.org", livekit_service_url: "https://lk.example.org",
}; };
const TRANSPORT_2: LivekitTransport = { const TRANSPORT_2: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://lk.sample.com", livekit_service_url: "https://lk.sample.com",
}; };
@@ -47,7 +47,7 @@ beforeEach(() => {
vi.mocked(fakeConnectionFactory).createConnection = vi vi.mocked(fakeConnectionFactory).createConnection = vi
.fn() .fn()
.mockImplementation( .mockImplementation(
(scope: ObservableScope, transport: LivekitTransport) => { (scope: ObservableScope, transport: UnstableLivekitTransport) => {
const mockConnection = { const mockConnection = {
transport, transport,
remoteParticipants$: new BehaviorSubject([]), remoteParticipants$: new BehaviorSubject([]),
@@ -94,13 +94,13 @@ describe("connections$ stream", () => {
).toHaveBeenCalledTimes(2); ).toHaveBeenCalledTimes(2);
const conn1 = connections.find((c) => const conn1 = connections.find((c) =>
areLivekitTransportsEqual(c.transport, TRANSPORT_1), areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_1),
); );
expect(conn1).toBeDefined(); expect(conn1).toBeDefined();
expect(conn1!.start).toHaveBeenCalled(); expect(conn1!.start).toHaveBeenCalled();
const conn2 = connections.find((c) => const conn2 = connections.find((c) =>
areLivekitTransportsEqual(c.transport, TRANSPORT_2), areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2),
); );
expect(conn2).toBeDefined(); expect(conn2).toBeDefined();
expect(conn2!.start).toHaveBeenCalled(); expect(conn2!.start).toHaveBeenCalled();
@@ -141,12 +141,12 @@ describe("connections$ stream", () => {
).toHaveBeenCalledTimes(2); ).toHaveBeenCalledTimes(2);
const conn2 = connections.find((c) => const conn2 = connections.find((c) =>
areLivekitTransportsEqual(c.transport, TRANSPORT_2), areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2),
); );
expect(conn2).toBeDefined(); expect(conn2).toBeDefined();
const conn1 = connections.find((c) => const conn1 = connections.find((c) =>
areLivekitTransportsEqual(c.transport, TRANSPORT_1), areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_1),
); );
expect(conn1).toBeDefined(); expect(conn1).toBeDefined();
expect(conn1!.start).toHaveBeenCalledOnce(); expect(conn1!.start).toHaveBeenCalledOnce();
@@ -187,7 +187,7 @@ describe("connections$ stream", () => {
expect(connections.length).toBe(1); expect(connections.length).toBe(1);
// The second connection should have been stopped has it is no longer needed. // The second connection should have been stopped has it is no longer needed.
const connection2 = allCreatedConnections.find((c) => const connection2 = allCreatedConnections.find((c) =>
areLivekitTransportsEqual(c.transport, TRANSPORT_2), areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2),
); );
expect(connection2).toBeDefined(); expect(connection2).toBeDefined();
expect(connection2!.stop).toHaveBeenCalled(); expect(connection2!.stop).toHaveBeenCalled();
@@ -225,7 +225,7 @@ describe("connectionManagerData$ stream", () => {
// Used in test to control fake connections' remoteParticipants$ streams // Used in test to control fake connections' remoteParticipants$ streams
let fakeRemoteParticipantsStreams: Map<string, Behavior<RemoteParticipant[]>>; let fakeRemoteParticipantsStreams: Map<string, Behavior<RemoteParticipant[]>>;
function keyForTransport(transport: LivekitTransport): string { function keyForTransport(transport: UnstableLivekitTransport): string {
return `${transport.livekit_service_url}`; return `${transport.livekit_service_url}`;
} }
@@ -233,7 +233,7 @@ describe("connectionManagerData$ stream", () => {
fakeRemoteParticipantsStreams = new Map(); fakeRemoteParticipantsStreams = new Map();
function getRemoteParticipantsFor( function getRemoteParticipantsFor(
transport: LivekitTransport, transport: UnstableLivekitTransport,
): Behavior<RemoteParticipant[]> { ): Behavior<RemoteParticipant[]> {
return ( return (
fakeRemoteParticipantsStreams.get(keyForTransport(transport)) ?? fakeRemoteParticipantsStreams.get(keyForTransport(transport)) ??
@@ -245,7 +245,7 @@ describe("connectionManagerData$ stream", () => {
vi.mocked(fakeConnectionFactory).createConnection = vi vi.mocked(fakeConnectionFactory).createConnection = vi
.fn() .fn()
.mockImplementation( .mockImplementation(
(scope: ObservableScope, transport: LivekitTransport) => { (scope: ObservableScope, transport: UnstableLivekitTransport) => {
const fakeRemoteParticipants$ = new BehaviorSubject< const fakeRemoteParticipants$ = new BehaviorSubject<
RemoteParticipant[] RemoteParticipant[]
>([]); >([]);
@@ -6,7 +6,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 { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { import {
combineLatest, combineLatest,
map, map,
@@ -25,7 +25,7 @@ import { type Behavior } from "../../Behavior.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
import { Epoch, type ObservableScope } from "../../ObservableScope.ts"; import { Epoch, type ObservableScope } from "../../ObservableScope.ts";
import { generateItemsWithEpoch } from "../../../utils/observable.ts"; import { generateItemsWithEpoch } from "../../../utils/observable.ts";
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { areUnstableLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";
import { import {
isLocalTransport, isLocalTransport,
@@ -56,7 +56,7 @@ export class ConnectionManagerData {
} }
} }
private getKey(transport: LivekitTransport): string { private getKey(transport: UnstableLivekitTransport): string {
// This is enough as a key because the ConnectionManager is already scoped by room. // This is enough as a key because the ConnectionManager is already scoped by room.
// We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`. // We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`.
return transport.livekit_service_url; return transport.livekit_service_url;
@@ -67,13 +67,13 @@ export class ConnectionManagerData {
} }
public getConnectionForTransport( public getConnectionForTransport(
transport: LivekitTransport, transport: UnstableLivekitTransport,
): Connection | null { ): Connection | null {
return this.store.get(this.getKey(transport))?.connection ?? null; return this.store.get(this.getKey(transport))?.connection ?? null;
} }
public getParticipantsForTransport( public getParticipantsForTransport(
transport: LivekitTransport, transport: UnstableLivekitTransport,
): RemoteParticipant[] { ): RemoteParticipant[] {
const key = this.getKey(transport); const key = this.getKey(transport);
const existing = this.store.get(key); const existing = this.store.get(key);
@@ -88,7 +88,7 @@ interface Props {
scope: ObservableScope; scope: ObservableScope;
connectionFactory: ConnectionFactory; connectionFactory: ConnectionFactory;
localTransport$: Observable<LocalTransport>; localTransport$: Observable<LocalTransport>;
remoteTransports$: Behavior<Epoch<LivekitTransport[]>>; remoteTransports$: Behavior<Epoch<UnstableLivekitTransport[]>>;
logger: Logger; logger: Logger;
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
@@ -147,7 +147,7 @@ export function createConnectionManager$({
* externally this is modified via `registerTransports()`. * externally this is modified via `registerTransports()`.
*/ */
const localAndRemoteTransports$: Behavior< const localAndRemoteTransports$: Behavior<
Epoch<(LivekitTransport | LocalTransport)[]> Epoch<(UnstableLivekitTransport | LocalTransport)[]>
> = scope.behavior( > = scope.behavior(
combineLatest([localTransportAsArray$, remoteTransports$]).pipe( combineLatest([localTransportAsArray$, remoteTransports$]).pipe(
// Combine local and remote transports into one transport array // Combine local and remote transports into one transport array
@@ -157,7 +157,7 @@ export function createConnectionManager$({
const remoteWithoutLocal = dedupedRemote.filter( const remoteWithoutLocal = dedupedRemote.filter(
(transport) => (transport) =>
!localTransportAsArray.find((l) => !localTransportAsArray.find((l) =>
areLivekitTransportsEqual(l.transport, transport), areUnstableLivekitTransportsEqual(l.transport, transport),
), ),
); );
logger.debug( logger.debug(
@@ -271,11 +271,11 @@ export function createConnectionManager$({
return { connectionManagerData$ }; return { connectionManagerData$ };
} }
function removeDuplicateTransports<T extends LivekitTransport>( function removeDuplicateTransports<T extends UnstableLivekitTransport>(
transports: T[], transports: T[],
): T[] { ): T[] {
return transports.reduce((acc, transport) => { return transports.reduce((acc, transport) => {
if (!acc.some((t) => areLivekitTransportsEqual(t, transport))) if (!acc.some((t) => areUnstableLivekitTransportsEqual(t, transport)))
acc.push(transport); acc.push(transport);
return acc; return acc;
}, [] as T[]); }, [] as T[]);
@@ -14,7 +14,7 @@ import EventEmitter from "events";
import { ObservableScope } from "../../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts";
import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import type { ClientOpenIDParts } from "../../../livekit/openIDSFU.ts";
import { import {
exampleTransport, exampleTransport,
mockMediaDevices, mockMediaDevices,
@@ -51,7 +51,7 @@ vi.mock("livekit-client", async (importOriginal) => {
}); });
let testScope: ObservableScope; let testScope: ObservableScope;
let mockClient: OpenIDClientParts; let mockClient: ClientOpenIDParts;
beforeEach(() => { beforeEach(() => {
testScope = new ObservableScope(); testScope = new ObservableScope();
@@ -9,7 +9,7 @@ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { import {
type CallMembership, type CallMembership,
type LivekitTransport, type UnstableLivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs"; import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs";
@@ -39,12 +39,12 @@ let testScope: ObservableScope;
const fallbackMemberId = (userId: string, deviceId: string): string => const fallbackMemberId = (userId: string, deviceId: string): string =>
`${userId}:${deviceId}`; `${userId}:${deviceId}`;
const transportA: LivekitTransport = { const transportA: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://lk.example.org", livekit_service_url: "https://lk.example.org",
}; };
const transportB: LivekitTransport = { const transportB: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://lk.sample.com", livekit_service_url: "https://lk.sample.com",
}; };
@@ -117,7 +117,7 @@ test("should signal participant not yet connected to livekit", async () => {
function createEpochedMemberships$(m$: Observable<CallMembership[]>): { function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
memberships$: Observable<Epoch<CallMembership[]>>; memberships$: Observable<Epoch<CallMembership[]>>;
membershipsWithTransport$: Observable< membershipsWithTransport$: Observable<
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]>
>; >;
} { } {
const memberships$ = m$.pipe(trackEpoch()); const memberships$ = m$.pipe(trackEpoch());
@@ -128,7 +128,7 @@ function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
return { return {
membership: m, membership: m,
transport: transport:
tr?.type === "livekit" ? (tr as LivekitTransport) : undefined, tr?.type === "livekit" ? (tr as UnstableLivekitTransport) : undefined,
}; };
}); });
}), }),
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; import { type LocalParticipant, type RemoteParticipant } from "livekit-client";
import { import {
type CallMembership, type CallMembership,
type LivekitTransport, type UnstableLivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { combineLatest, filter, map } from "rxjs"; import { combineLatest, filter, map } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
@@ -60,7 +60,7 @@ export interface RemoteMatrixLivekitMember extends MatrixLivekitMember {
interface Props { interface Props {
scope: ObservableScope; scope: ObservableScope;
membershipsWithTransport$: Behavior< membershipsWithTransport$: Behavior<
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]>
>; >;
connectionManager: IConnectionManager; connectionManager: IConnectionManager;
localUser: { deviceId: string; userId: string }; localUser: { deviceId: string; userId: string };
@@ -165,7 +165,7 @@ export function createRemoteMatrixLivekitMembers$({
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$) // TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
// TODO add this to the JS-SDK // TODO add this to the JS-SDK
export function areLivekitTransportsEqual<T extends LivekitTransport>( export function areUnstableLivekitTransportsEqual<T extends UnstableLivekitTransport>(
t1: T | null, t1: T | null,
t2: T | null, t2: T | null,
): boolean { ): boolean {
@@ -10,7 +10,7 @@ import { BehaviorSubject, NEVER } from "rxjs";
import { type Room as LivekitRoom } from "livekit-client"; import { type Room as LivekitRoom } from "livekit-client";
import EventEmitter from "events"; import EventEmitter from "events";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { import {
@@ -19,7 +19,7 @@ import {
trackEpoch, trackEpoch,
} from "../../ObservableScope.ts"; } from "../../ObservableScope.ts";
import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts";
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { type ClientOpenIDParts } from "../../../livekit/openIDSFU.ts";
import { import {
mockMediaDevices, mockMediaDevices,
mockRtcMembership, mockRtcMembership,
@@ -28,7 +28,7 @@ import {
} from "../../../utils/test.ts"; } from "../../../utils/test.ts";
import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { import {
areLivekitTransportsEqual, areUnstableLivekitTransportsEqual,
createRemoteMatrixLivekitMembers$, createRemoteMatrixLivekitMembers$,
type RemoteMatrixLivekitMember, type RemoteMatrixLivekitMember,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
@@ -40,7 +40,7 @@ import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts";
let testScope: ObservableScope; let testScope: ObservableScope;
let ecConnectionFactory: ECConnectionFactory; let ecConnectionFactory: ECConnectionFactory;
let mockClient: OpenIDClientParts; let mockClient: ClientOpenIDParts;
let lkRoomFactory: () => LivekitRoom; let lkRoomFactory: () => LivekitRoom;
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map(); const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
@@ -147,9 +147,9 @@ test("bob, carl, then bob joining no tracks yet", () => {
}); });
expectObservable(item.connection$).toBe("a", { expectObservable(item.connection$).toBe("a", {
a: expect.toSatisfy((co) => a: expect.toSatisfy((co) =>
areLivekitTransportsEqual( areUnstableLivekitTransportsEqual(
co.transport, co.transport,
bobMembership.transports[0]! as LivekitTransport, bobMembership.transports[0]! as UnstableLivekitTransport,
), ),
), ),
}); });
@@ -184,9 +184,9 @@ test("bob, carl, then bob joining no tracks yet", () => {
expectObservable(item.connection$).toBe("a", { expectObservable(item.connection$).toBe("a", {
a: expect.toSatisfy((connection) => { a: expect.toSatisfy((connection) => {
expect( expect(
areLivekitTransportsEqual( areUnstableLivekitTransportsEqual(
connection.transport, connection.transport,
carlMembership.transports[0]! as LivekitTransport, carlMembership.transports[0]! as UnstableLivekitTransport,
), ),
).toBe(true); ).toBe(true);
return true; return true;
@@ -214,9 +214,9 @@ test("bob, carl, then bob joining no tracks yet", () => {
expectObservable(item.connection$).toBe("a", { expectObservable(item.connection$).toBe("a", {
a: expect.toSatisfy((connection) => { a: expect.toSatisfy((connection) => {
expect( expect(
areLivekitTransportsEqual( areUnstableLivekitTransportsEqual(
connection.transport, connection.transport,
daveMembership.transports[0]! as LivekitTransport, daveMembership.transports[0]! as UnstableLivekitTransport,
), ),
).toBe(true); ).toBe(true);
return true; return true;
+7 -7
View File
@@ -7,10 +7,10 @@ Please see LICENSE in the repository root for full details.
import { import {
type CallMembership, type CallMembership,
type LivekitTransport, type UnstableLivekitTransport,
type MatrixRTCSession, type MatrixRTCSession,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
isLivekitTransport, isUnstableLivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { fromEvent } from "rxjs"; import { fromEvent } from "rxjs";
@@ -31,15 +31,15 @@ export const membershipsAndTransports$ = (
memberships$: Behavior<Epoch<CallMembership[]>>, memberships$: Behavior<Epoch<CallMembership[]>>,
): { ): {
membershipsWithTransport$: Behavior< membershipsWithTransport$: Behavior<
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]>
>; >;
transports$: Behavior<Epoch<LivekitTransport[]>>; transports$: Behavior<Epoch<UnstableLivekitTransport[]>>;
} => { } => {
const membershipsWithTransport$: Behavior< const membershipsWithTransport$: Behavior<
Epoch< Epoch<
{ {
membership: CallMembership; membership: CallMembership;
transport: LivekitTransport | undefined; transport: UnstableLivekitTransport | undefined;
}[] }[]
> >
> = scope.behavior( > = scope.behavior(
@@ -49,14 +49,14 @@ export const membershipsAndTransports$ = (
const transport = membership.getTransport(); const transport = membership.getTransport();
return { return {
membership, membership,
transport: isLivekitTransport(transport) ? transport : undefined, transport: isUnstableLivekitTransport(transport) ? transport : undefined,
}; };
}); });
}), }),
), ),
); );
const transports$: Behavior<Epoch<LivekitTransport[]>> = scope.behavior( const transports$: Behavior<Epoch<UnstableLivekitTransport[]>> = scope.behavior(
membershipsWithTransport$.pipe( membershipsWithTransport$.pipe(
mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))), mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))),
), ),
+2 -2
View File
@@ -26,7 +26,7 @@ import {
import { import {
CallMembership, CallMembership,
type LivekitFocusSelection, type LivekitFocusSelection,
type LivekitTransport, type UnstableLivekitTransport,
type MatrixRTCSession, type MatrixRTCSession,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
type MatrixRTCSessionEventHandlerMap, type MatrixRTCSessionEventHandlerMap,
@@ -209,7 +209,7 @@ export function mockEmitter<T>(): EmitterMock<T> {
}; };
} }
export const exampleTransport: LivekitTransport = { export const exampleTransport: UnstableLivekitTransport = {
type: "livekit", type: "livekit",
livekit_service_url: "https://lk.example.org", livekit_service_url: "https://lk.example.org",
}; };