Refactor how we aquire the jwt token for the local user. (only fetch it

once)

The local jwt token needs to be aquired via the right endpoint. The
endpoint defines how our rtcBackendIdentity is computed. Based on us
using sticky events or state events we also need to use the right
endpoint. This cannot be done generically in the connection manager. The
jwt token now is computed in the localTransport and the resolved sfu
config is passed to the connection manager.

Add JWT endpoint version and SFU config support Pin matrix-js-sdk to a
specific commit and update dev auth image tag. Propagate SFU config and
JWT endpoint choice through local transport, ConnectionManager and
Connection; add JwtEndpointVersion enum and LocalTransportWithSFUConfig
type. Add NO_MATRIX_2 auth error and locale string, thread
rtcBackendIdentity through UI props, and include related test, CSS and
minor imports updates
This commit is contained in:
Timo K
2026-01-09 13:38:26 +01:00
parent d4b06b0f9c
commit 7dbbd763b9
27 changed files with 421 additions and 192 deletions

View File

@@ -3,7 +3,7 @@ networks:
services: services:
auth-service: auth-service:
image: ghcr.io/element-hq/lk-jwt-service:latest-ci image: ghcr.io/element-hq/lk-jwt-service:pr_139
pull_policy: always pull_policy: always
hostname: auth-server hostname: auth-server
environment: environment:
@@ -25,7 +25,7 @@ services:
- ecbackend - ecbackend
auth-service-1: auth-service-1:
image: ghcr.io/element-hq/lk-jwt-service:latest-ci image: ghcr.io/element-hq/lk-jwt-service:pr_139
pull_policy: always pull_policy: always
hostname: auth-server-1 hostname: auth-server-1
environment: environment:

View File

@@ -116,6 +116,7 @@
"matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).", "matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
"membership_manager": "Membership Manager Error", "membership_manager": "Membership Manager Error",
"membership_manager_description": "The Membership Manager had to shut down. This is caused by many consequtive failed network requests.", "membership_manager_description": "The Membership Manager had to shut down. This is caused by many consequtive failed network requests.",
"no_matrix_2_authorization_service": "Your authorization service for you media server (SFU) is not on the newest version",
"open_elsewhere": "Opened in another tab", "open_elsewhere": "Opened in another tab",
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.", "open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
"room_creation_restricted": "Failed to create call", "room_creation_restricted": "Failed to create call",

View File

@@ -104,7 +104,7 @@
"livekit-client": "^2.13.0", "livekit-client": "^2.13.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"loglevel": "^1.9.1", "loglevel": "^1.9.1",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "matrix-org/matrix-js-sdk#ff5fab7722e54be2d69a380cd6182ecb262d7859",
"matrix-widget-api": "^1.14.0", "matrix-widget-api": "^1.14.0",
"node-stdlib-browser": "^1.3.1", "node-stdlib-browser": "^1.3.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",

View File

@@ -20,6 +20,7 @@ interface Props {
audio?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats; audio?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
video?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats; video?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
focusUrl?: string; focusUrl?: string;
rtcBackendIdentity?: string;
} }
const extractDomain = (url: string): string => { const extractDomain = (url: string): string => {
@@ -37,6 +38,7 @@ export const RTCConnectionStats: FC<Props> = ({
audio, audio,
video, video,
focusUrl, focusUrl,
rtcBackendIdentity,
...rest ...rest
}) => { }) => {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
@@ -71,6 +73,9 @@ export const RTCConnectionStats: FC<Props> = ({
</pre> </pre>
</div> </div>
</Modal> </Modal>
<Text as="span" size="xs" title="rtcBackendIdentity">
rtcBackendIdentity:{rtcBackendIdentity}
</Text>
{focusUrl && ( {focusUrl && (
<div> <div>
<Text as="span" size="xs" title="focusURL"> <Text as="span" size="xs" title="focusURL">

View File

@@ -46,7 +46,6 @@ describe("getSFUConfigWithOpenID", () => {
matrixClient, matrixClient,
ownMemberMock, ownMemberMock,
"https://sfu.example.org", "https://sfu.example.org",
false,
"!example_room_id", "!example_room_id",
); );
expect(config).toEqual({ expect(config).toEqual({
@@ -70,7 +69,6 @@ describe("getSFUConfigWithOpenID", () => {
matrixClient, matrixClient,
ownMemberMock, ownMemberMock,
"https://sfu.example.org", "https://sfu.example.org",
false,
"!example_room_id", "!example_room_id",
); );
} catch (ex) { } catch (ex) {
@@ -101,10 +99,11 @@ describe("getSFUConfigWithOpenID", () => {
matrixClient, matrixClient,
ownMemberMock, ownMemberMock,
"https://sfu.example.org", "https://sfu.example.org",
false,
"!example_room_id", "!example_room_id",
"https://matrix.homeserverserver.org", {
"mock_delay_id", delayEndpointBaseUrl: "https://matrix.homeserverserver.org",
delayId: "mock_delay_id",
},
); );
} catch (ex) { } catch (ex) {
expect((ex as Error).message).toEqual( expect((ex as Error).message).toEqual(
@@ -154,10 +153,11 @@ describe("getSFUConfigWithOpenID", () => {
matrixClient, matrixClient,
ownMemberMock, ownMemberMock,
"https://sfu.example.org", "https://sfu.example.org",
false,
"!example_room_id", "!example_room_id",
"https://matrix.homeserverserver.org", {
"mock_delay_id", delayEndpointBaseUrl: "https://matrix.homeserverserver.org",
delayId: "mock_delay_id",
},
); );
} catch (ex) { } catch (ex) {
expect((ex as Error).message).toEqual( expect((ex as Error).message).toEqual(
@@ -203,7 +203,6 @@ describe("getSFUConfigWithOpenID", () => {
matrixClient, matrixClient,
ownMemberMock, ownMemberMock,
"https://sfu.example.org", "https://sfu.example.org",
false,
"!example_room_id", "!example_room_id",
); );
expect(config).toEqual({ expect(config).toEqual({

View File

@@ -13,9 +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 { FailToGetOpenIdToken } from "../utils/errors"; import {
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 { JwtEndpointVersion } from "../state/CallViewModel/localMember/LocalTransport";
/** /**
* Configuration and access tokens provided by the SFU on successful authentication. * Configuration and access tokens provided by the SFU on successful authentication.
@@ -73,14 +77,15 @@ export type OpenIDClientParts = Pick<
* @param client The Matrix client * @param client The Matrix client
* @param membership Our own membership identity parts used to send to jwt service. * @param membership Our own membership identity parts used to send to jwt service.
* @param serviceUrl The URL of the livekit SFU service * @param serviceUrl The URL of the livekit SFU service
* @param forceOldJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination * @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases.
* @param opts Additional options to modify which endpoint with which data will be used to aquire the jwt token.
* @param opts.forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination
* instead of a hash. * instead of a hash.
* This function by default uses whatever is possible with the current jwt service installed next to the SFU. * This function by default uses whatever is possible with the current jwt service installed next to the SFU.
* For remote connections this does not matter, since we will not publish there we can rely on the newest option. * For remote connections this does not matter, since we will not publish there we can rely on the newest option.
* For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events. * For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events.
* @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases. * @param opts.delayEndpointBaseUrl The URL of the matrix homeserver.
* @param delayEndpointBaseUrl The URL of the matrix homeserver. * @param opts.delayId The delay id used for the jwt service to manage.
* @param delayId The delay id used for the jwt service to manage.
* @param logger optional logger. * @param logger optional logger.
* @returns Object containing the token information * @returns Object containing the token information
* @throws FailToGetOpenIdToken * @throws FailToGetOpenIdToken
@@ -89,10 +94,12 @@ export async function getSFUConfigWithOpenID(
client: OpenIDClientParts, client: OpenIDClientParts,
membership: CallMembershipIdentityParts, membership: CallMembershipIdentityParts,
serviceUrl: string, serviceUrl: string,
forceOldJwtEndpoint: boolean,
roomId: string, roomId: string,
delayEndpointBaseUrl?: string, opts?: {
delayId?: string, forceJwtEndpoint?: JwtEndpointVersion;
delayEndpointBaseUrl?: string;
delayId?: string;
},
logger?: Logger, logger?: Logger,
): Promise<SFUConfig> { ): Promise<SFUConfig> {
let openIdToken: IOpenIDToken; let openIdToken: IOpenIDToken;
@@ -113,15 +120,21 @@ export async function getSFUConfigWithOpenID(
// If forceOldJwtEndpoint is set we indicate that we do not want to try the new endpoint, // If forceOldJwtEndpoint is set we indicate that we do not want to try the new endpoint,
// since we are not sending the new matrix2.0 sticky events (no hashed identity in the event) // since we are not sending the new matrix2.0 sticky events (no hashed identity in the event)
if (forceOldJwtEndpoint === false) { if (
// we do not force anything. Try with new first (remote connections)
!opts?.forceJwtEndpoint ||
// we do force the matrix2.0 endpoint
(opts?.forceJwtEndpoint &&
opts?.forceJwtEndpoint === JwtEndpointVersion.Matrix_2_0)
) {
try { try {
sfuConfig = await getLiveKitJWTWithDelayDelegation( sfuConfig = await getLiveKitJWTWithDelayDelegation(
membership, membership,
serviceUrl, serviceUrl,
roomId, roomId,
openIdToken, openIdToken,
delayEndpointBaseUrl, opts?.delayEndpointBaseUrl,
delayId, opts?.delayId,
); );
logger?.info(`Got JWT from call's active focus URL.`); logger?.info(`Got JWT from call's active focus URL.`);
} catch (e) { } catch (e) {
@@ -137,12 +150,16 @@ export async function getSFUConfigWithOpenID(
`(not going to try with legacy endpoint: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`, `(not going to try with legacy endpoint: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`,
e, e,
); );
// Make this throw a hard error in case we force the matrix2.0 endpoint.
if (opts?.forceJwtEndpoint === JwtEndpointVersion.Matrix_2_0)
throw new NoMatrix2AuthorizationService(e as Error);
} }
} }
} }
// DEPRECATED // DEPRECATED
// Either forceOldJwtEndpoint = true or getLiveKitJWTWithDelayDelegation throws -> reset sfuConfig = undefined // here we either have a sfuConfig or we alredy exited becuause of `if (opts?.forceEndpoint === MatrixRTCMode.Matrix_2_0) throw e;`
// The only case we can get into this if is, if `opts?.forceEndpoint !== MatrixRTCMode.Matrix_2_0`
if (sfuConfig === undefined) { if (sfuConfig === undefined) {
sfuConfig = await getLiveKitJWT( sfuConfig = await getLiveKitJWT(
membership.deviceId, membership.deviceId,
@@ -235,7 +252,7 @@ export async function getLiveKitJWTWithDelayDelegation(
let bodyDalayParts = {}; let bodyDalayParts = {};
// Also check for empty string // Also check for empty string
if (delayId && delayEndpointBaseUrl && false) { if (delayId && delayEndpointBaseUrl) {
const delayTimeoutMs = const delayTimeoutMs =
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000; Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000;
bodyDalayParts = { bodyDalayParts = {

View File

@@ -113,10 +113,8 @@ const logger = rootLogger.getChild("[InCallView]");
const maxTapDurationMs = 400; const maxTapDurationMs = 400;
export interface ActiveCallProps extends Omit< export interface ActiveCallProps
InCallViewProps, extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
"vm" | "livekitRoom" | "connState"
> {
e2eeSystem: EncryptionSystem; e2eeSystem: EncryptionSystem;
// TODO refactor those reasons into an enum // TODO refactor those reasons into an enum
onLeft: ( onLeft: (
@@ -798,6 +796,8 @@ export const InCallView: FC<InCallViewProps> = ({
</div> </div>
); );
const allConnections = useBehavior(vm.allConnections$);
return ( return (
<div <div
className={styles.inRoom} className={styles.inRoom}
@@ -836,8 +836,14 @@ export const InCallView: FC<InCallViewProps> = ({
onDismiss={closeSettings} onDismiss={closeSettings}
tab={settingsTab} tab={settingsTab}
onTabChange={setSettingsTab} onTabChange={setSettingsTab}
// TODO expose correct data to setttings modal livekitRooms={allConnections
livekitRooms={[]} .getConnections()
.map((connectionItem) => ({
room: connectionItem.livekitRoom,
// TODO compute is local or tag it in the livekit room items already
isLocal: undefined,
url: connectionItem.transport.livekit_service_url,
}))}
/> />
</> </>
)} )}

View File

@@ -8,3 +8,14 @@ Please see LICENSE in the repository root for full details.
pre { pre {
font-size: var(--font-size-micro); font-size: var(--font-size-micro);
} }
.livekit_room_box {
border: 3px solid var(--cpd-color-bg-subtle-secondary);
border-radius: var(--cpd-space-8x);
padding: var(--cpd-space-4x);
margin-bottom: var(--cpd-space-4x);
margin-top: var(--cpd-space-4x);
li {
font-size: var(--font-size-micro);
}
}

View File

@@ -29,6 +29,7 @@ import {
Label, Label,
RadioControl, RadioControl,
} from "@vector-im/compound-web"; } from "@vector-im/compound-web";
import { type Room as LivekitRoom } from "livekit-client";
import { FieldRow, InputField } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
import { import {
@@ -42,7 +43,6 @@ import {
customLivekitUrl as customLivekitUrlSetting, customLivekitUrl as customLivekitUrlSetting,
MatrixRTCMode, MatrixRTCMode,
} from "./settings"; } from "./settings";
import type { Room as LivekitRoom } from "livekit-client";
import styles from "./DeveloperSettingsTab.module.css"; import styles from "./DeveloperSettingsTab.module.css";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
@@ -304,12 +304,12 @@ export const DeveloperSettingsTab: FC<Props> = ({
</InlineField> </InlineField>
</Form> </Form>
{livekitRooms?.map((livekitRoom) => ( {livekitRooms?.map((livekitRoom) => (
<> <div className={styles.livekit_room_box}>
<h3> <h4>
{t("developer_mode.livekit_sfu", { {t("developer_mode.livekit_sfu", {
url: livekitRoom.url || "unknown", url: livekitRoom.url || "unknown",
})} })}
</h3> </h4>
{livekitRoom.isLocal && <p>ws-url: {localSfuUrl?.href}</p>} {livekitRoom.isLocal && <p>ws-url: {localSfuUrl?.href}</p>}
<p> <p>
{t("developer_mode.livekit_server_info")}( {t("developer_mode.livekit_server_info")}(
@@ -321,7 +321,19 @@ export const DeveloperSettingsTab: FC<Props> = ({
: "undefined"} : "undefined"}
{livekitRoom.room.metadata} {livekitRoom.room.metadata}
</pre> </pre>
</> <p>Local Participant</p>
<pre className={styles.pre}>
{livekitRoom.room.localParticipant.identity}
</pre>
<p>Remote Participants</p>
<ul>
{Array.from(livekitRoom.room.remoteParticipants.keys()).map(
(id) => (
<li key={id}>{id}</li>
),
)}
</ul>
</div>
))} ))}
<p>{t("developer_mode.environment_variables")}</p> <p>{t("developer_mode.environment_variables")}</p>
<pre>{JSON.stringify(env, null, 2)}</pre> <pre>{JSON.stringify(env, null, 2)}</pre>

View File

@@ -108,13 +108,19 @@ import {
enterRTCSession, enterRTCSession,
TransportState, TransportState,
} from "./localMember/LocalMember.ts"; } from "./localMember/LocalMember.ts";
import { createLocalTransport$ } from "./localMember/LocalTransport.ts"; import {
createLocalTransport$,
JwtEndpointVersion,
} from "./localMember/LocalTransport.ts";
import { import {
createMemberships$, createMemberships$,
membershipsAndTransports$, membershipsAndTransports$,
} from "../SessionBehaviors.ts"; } from "../SessionBehaviors.ts";
import { ECConnectionFactory } from "./remoteMembers/ConnectionFactory.ts"; import { ECConnectionFactory } from "./remoteMembers/ConnectionFactory.ts";
import { createConnectionManager$ } from "./remoteMembers/ConnectionManager.ts"; import {
type ConnectionManagerData,
createConnectionManager$,
} from "./remoteMembers/ConnectionManager.ts";
import { import {
createMatrixLivekitMembers$, createMatrixLivekitMembers$,
type TaggedParticipant, type TaggedParticipant,
@@ -263,6 +269,7 @@ export interface CallViewModel {
* multiple devices. * multiple devices.
*/ */
participantCount$: Behavior<number>; participantCount$: Behavior<number>;
allConnections$: Behavior<ConnectionManagerData>;
/** Participants sorted by livekit room so they can be used in the audio rendering */ /** Participants sorted by livekit room so they can be used in the audio rendering */
livekitRoomItems$: Behavior<LivekitRoomItem[]>; livekitRoomItems$: Behavior<LivekitRoomItem[]>;
userMedia$: Behavior<UserMedia[]>; userMedia$: Behavior<UserMedia[]>;
@@ -428,14 +435,6 @@ export function createCallViewModel$(
memberId: `${userId}:${deviceId}`, memberId: `${userId}:${deviceId}`,
}; };
const useOldJwtEndpoint$ = scope.behavior(
matrixRTCMode$.pipe(
map(
(v) => v === MatrixRTCMode.Legacy || v === MatrixRTCMode.Compatibility,
),
),
);
const localTransport$ = createLocalTransport$({ const localTransport$ = createLocalTransport$({
scope: scope, scope: scope,
memberships$: memberships$, memberships$: memberships$,
@@ -451,7 +450,15 @@ export function createCallViewModel$(
matrixRTCSession.delayId ?? null, matrixRTCSession.delayId ?? null,
), ),
roomId: matrixRoom.roomId, roomId: matrixRoom.roomId,
useOldJwtEndpoint$, forceJwtEndpoint$: scope.behavior(
matrixRTCMode$.pipe(
map((v) =>
v === MatrixRTCMode.Matrix_2_0
? JwtEndpointVersion.Matrix_2_0
: JwtEndpointVersion.Legacy,
),
),
),
useOldestMember$: scope.behavior( useOldestMember$: scope.behavior(
matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Legacy)), matrixRTCMode$.pipe(map((v) => v === MatrixRTCMode.Legacy)),
), ),
@@ -483,7 +490,6 @@ export function createCallViewModel$(
), ),
), ),
remoteTransports$: membershipsAndTransports.transports$, remoteTransports$: membershipsAndTransports.transports$,
forceOldJwtEndpointForLocalTransport$: useOldJwtEndpoint$,
logger: logger, logger: logger,
ownMembershipIdentity, ownMembershipIdentity,
}); });
@@ -628,6 +634,9 @@ export function createCallViewModel$(
), ),
); );
const allConnections$ = scope.behavior(
connectionManager.connectionManagerData$.pipe(map((d) => d.value)),
);
const livekitRoomItems$ = scope.behavior( const livekitRoomItems$ = scope.behavior(
matrixLivekitMembers$.pipe( matrixLivekitMembers$.pipe(
switchMap((members) => { switchMap((members) => {
@@ -724,6 +733,7 @@ export function createCallViewModel$(
userId, userId,
participant satisfies TaggedParticipant as TaggedParticipant, // Widen the type safely participant satisfies TaggedParticipant as TaggedParticipant, // Widen the type safely
connection$, connection$,
membership$.value,
], ],
data: undefined, data: undefined,
}; };
@@ -742,7 +752,14 @@ export function createCallViewModel$(
// const participantId = membership$.value?.identity; // const participantId = membership$.value?.identity;
for (let dup = 0; dup < 1 + duplicateTiles; dup++) { for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
yield { yield {
keys: [dup, userMediaId, userId, participant, connection$], keys: [
dup,
userMediaId,
userId,
participant,
connection$,
membership$.value,
],
data: undefined, data: undefined,
}; };
} }
@@ -756,6 +773,7 @@ export function createCallViewModel$(
userId, userId,
participant, participant,
connection$, connection$,
membership,
) => { ) => {
const livekitRoom$ = scope.behavior( const livekitRoom$ = scope.behavior(
connection$.pipe(map((c) => c?.livekitRoom)), connection$.pipe(map((c) => c?.livekitRoom)),
@@ -773,6 +791,7 @@ export function createCallViewModel$(
scope, scope,
`${participantId}:${dup}`, `${participantId}:${dup}`,
userId, userId,
membership,
participant, participant,
options.encryptionSystem, options.encryptionSystem,
livekitRoom$, livekitRoom$,
@@ -1523,6 +1542,7 @@ export function createCallViewModel$(
), ),
null, null,
), ),
allConnections$,
participantCount$: participantCount$, participantCount$: participantCount$,
handsRaised$: handsRaised$, handsRaised$: handsRaised$,
reactions$: reactions$, reactions$: reactions$,

View File

@@ -39,6 +39,7 @@ import { constant } from "../../Behavior";
import { ConnectionManagerData } from "../remoteMembers/ConnectionManager"; import { ConnectionManagerData } from "../remoteMembers/ConnectionManager";
import { ConnectionState, type Connection } from "../remoteMembers/Connection"; import { ConnectionState, type Connection } from "../remoteMembers/Connection";
import { type Publisher } from "./Publisher"; import { type Publisher } from "./Publisher";
import { type LocalTransportWithSFUConfig } from "./LocalTransport";
const MATRIX_RTC_MODE = MatrixRTCMode.Legacy; const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
@@ -212,10 +213,11 @@ 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$ =
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")), scope.behavior<null | LocalTransportWithSFUConfig>(
null, hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
); null,
);
// we do not need any connection data since we want to fail before reaching that. // we do not need any connection data since we want to fail before reaching that.
const mockConnectionManager = { const mockConnectionManager = {
@@ -243,11 +245,23 @@ describe("LocalMembership", () => {
}); });
const aTransport = { const aTransport = {
livekit_service_url: "a", transport: {
} as LivekitTransport; livekit_service_url: "a",
} as LivekitTransport,
sfuConfig: {
url: "sfu-url",
jwt: "sfu-token",
},
} as LocalTransportWithSFUConfig;
const bTransport = { const bTransport = {
livekit_service_url: "b", transport: {
} as LivekitTransport; livekit_service_url: "b",
} as LivekitTransport,
sfuConfig: {
url: "sfu-url",
jwt: "sfu-token",
},
} as LocalTransportWithSFUConfig;
const connectionTransportAConnected = { const connectionTransportAConnected = {
livekitRoom: mockLivekitRoom({ livekitRoom: mockLivekitRoom({
@@ -391,7 +405,8 @@ describe("LocalMembership", () => {
const scope = new ObservableScope(); const scope = new ObservableScope();
const connectionManagerData = new ConnectionManagerData(); const connectionManagerData = new ConnectionManagerData();
const localTransport$ = new BehaviorSubject<null | LivekitTransport>(null); const localTransport$ =
new BehaviorSubject<null | LocalTransportWithSFUConfig>(null);
const connectionManagerData$ = new BehaviorSubject( const connectionManagerData$ = new BehaviorSubject(
new Epoch(connectionManagerData), new Epoch(connectionManagerData),
); );
@@ -468,7 +483,7 @@ describe("LocalMembership", () => {
}); });
( (
connectionManagerData2.getConnectionForTransport(aTransport)! connectionManagerData2.getConnectionForTransport(aTransport.transport)!
.state$ as BehaviorSubject<ConnectionState> .state$ as BehaviorSubject<ConnectionState>
).next(ConnectionState.LivekitConnected); ).next(ConnectionState.LivekitConnected);
expect(localMembership.localMemberState$.value).toStrictEqual({ expect(localMembership.localMemberState$.value).toStrictEqual({

View File

@@ -61,6 +61,7 @@ import {
} from "../remoteMembers/Connection.ts"; } from "../remoteMembers/Connection.ts";
import { type HomeserverConnected } from "./HomeserverConnected.ts"; import { type HomeserverConnected } from "./HomeserverConnected.ts";
import { and$ } from "../../../utils/observable.ts"; import { and$ } from "../../../utils/observable.ts";
import { type LocalTransportWithSFUConfig } from "./LocalTransport.ts";
export enum TransportState { export enum TransportState {
/** Not even a transport is available to the LocalMembership */ /** Not even a transport is available to the LocalMembership */
@@ -126,7 +127,7 @@ interface Props {
createPublisherFactory: (connection: Connection) => Publisher; createPublisherFactory: (connection: Connection) => Publisher;
joinMatrixRTC: (transport: LivekitTransport) => void; joinMatrixRTC: (transport: LivekitTransport) => void;
homeserverConnected: HomeserverConnected; homeserverConnected: HomeserverConnected;
localTransport$: Behavior<LivekitTransport | null>; localTransport$: Behavior<LocalTransportWithSFUConfig | null>;
matrixRTCSession: Pick< matrixRTCSession: Pick<
MatrixRTCSession, MatrixRTCSession,
"updateCallIntent" | "leaveRoomSession" "updateCallIntent" | "leaveRoomSession"
@@ -234,7 +235,9 @@ export const createLocalMembership$ = ({
return null; return null;
} }
return connectionData.getConnectionForTransport(localTransport); return connectionData.getConnectionForTransport(
localTransport.transport,
);
}), }),
tap((connection) => { tap((connection) => {
logger.info( logger.info(
@@ -533,7 +536,7 @@ export const createLocalMembership$ = ({
if (!shouldConnect) return; if (!shouldConnect) return;
try { try {
joinMatrixRTC(transport); joinMatrixRTC(transport.transport);
} catch (error) { } catch (error) {
logger.error("Error entering RTC session", error); logger.error("Error entering RTC session", error);
if (error instanceof Error) if (error instanceof Error)

View File

@@ -19,7 +19,7 @@ import { BehaviorSubject, lastValueFrom } from "rxjs";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { mockConfig, flushPromises, ownMemberMock } from "../../../utils/test"; import { mockConfig, flushPromises, ownMemberMock } from "../../../utils/test";
import { createLocalTransport$ } from "./LocalTransport"; import { createLocalTransport$, JwtEndpointVersion } from "./LocalTransport";
import { constant } from "../../Behavior"; import { constant } from "../../Behavior";
import { Epoch, ObservableScope } from "../../ObservableScope"; import { Epoch, ObservableScope } from "../../ObservableScope";
import { import {
@@ -58,7 +58,7 @@ describe("LocalTransport", () => {
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false), forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"), delayId$: constant("delay_id_mock"),
}); });
await flushPromises(); await flushPromises();
@@ -98,7 +98,7 @@ describe("LocalTransport", () => {
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false), forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"), delayId$: constant("delay_id_mock"),
}); });
localTransport$.subscribe( localTransport$.subscribe(
@@ -140,7 +140,7 @@ describe("LocalTransport", () => {
baseUrl: "https://lk.example.org", baseUrl: "https://lk.example.org",
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false), forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"), delayId$: constant("delay_id_mock"),
}); });
@@ -186,7 +186,7 @@ describe("LocalTransport", () => {
baseUrl: "https://lk.example.org", baseUrl: "https://lk.example.org",
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false), forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"), delayId$: constant("delay_id_mock"),
}); });
@@ -216,7 +216,7 @@ describe("LocalTransport", () => {
scope, scope,
roomId: "!example_room_id", roomId: "!example_room_id",
useOldestMember$: constant(false), useOldestMember$: constant(false),
useOldJwtEndpoint$: constant(false), forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant(null), delayId$: constant(null),
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
@@ -333,7 +333,7 @@ describe("LocalTransport", () => {
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
roomId: "!example_room_id", roomId: "!example_room_id",
useOldestMember$: constant(false), useOldestMember$: constant(false),
useOldJwtEndpoint$: constant(false), forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant(null), delayId$: constant(null),
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {

View File

@@ -31,9 +31,11 @@ import { Config } from "../../../config/Config.ts";
import { import {
FailToGetOpenIdToken, FailToGetOpenIdToken,
MatrixRTCTransportMissingError, MatrixRTCTransportMissingError,
NoMatrix2AuthorizationService,
} from "../../../utils/errors.ts"; } from "../../../utils/errors.ts";
import { import {
getSFUConfigWithOpenID, getSFUConfigWithOpenID,
type SFUConfig,
type OpenIDClientParts, type OpenIDClientParts,
} from "../../../livekit/openIDSFU.ts"; } from "../../../livekit/openIDSFU.ts";
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts"; import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
@@ -57,10 +59,25 @@ interface Props {
OpenIDClientParts; OpenIDClientParts;
roomId: string; roomId: string;
useOldestMember$: Behavior<boolean>; useOldestMember$: Behavior<boolean>;
useOldJwtEndpoint$: Behavior<boolean>; forceJwtEndpoint$: Behavior<JwtEndpointVersion>;
delayId$: Behavior<string | null>; delayId$: Behavior<string | null>;
} }
export enum JwtEndpointVersion {
Legacy = "legacy",
Matrix_2_0 = "matrix_2_0",
}
export interface LocalTransportWithSFUConfig {
transport: LivekitTransport;
sfuConfig: SFUConfig;
}
export function isLocalTransportWithSFUConfig(
obj: LivekitTransport | LocalTransportWithSFUConfig,
): obj is LocalTransportWithSFUConfig {
return "transport" in obj && "sfuConfig" in obj;
}
/** /**
* This class is responsible for managing the local transport. * This class is responsible for managing the local transport.
* "Which transport is the local member going to use" * "Which transport is the local member going to use"
@@ -81,22 +98,40 @@ export const createLocalTransport$ = ({
client, client,
roomId, roomId,
useOldestMember$, useOldestMember$,
useOldJwtEndpoint$, forceJwtEndpoint$,
delayId$, delayId$,
}: Props): Behavior<LivekitTransport | null> => { }: Props): Behavior<LocalTransportWithSFUConfig | null> => {
/** /**
* The transport over which we should be actively publishing our media. * The transport over which we should be actively publishing our media.
* undefined when not joined. * undefined when not joined.
*/ */
const oldestMemberTransport$ = scope.behavior( const oldestMemberTransport$ = scope.behavior(
combineLatest([memberships$, useOldJwtEndpoint$]).pipe( combineLatest([memberships$]).pipe(
map(([memberships, forceOldJwtEndpoint]) => { map(([memberships]) => {
const oldestMember = memberships.value[0]; const oldestMember = memberships.value[0];
const transport = oldestMember?.getTransport(memberships.value[0]); const transport = oldestMember?.getTransport(memberships.value[0]);
if (!transport) return null; if (!transport) return null;
return transport; return transport;
}), }),
first((t) => t != null && isLivekitTransport(t)), first((t) => t != null && isLivekitTransport(t)),
switchMap((transport) => {
// Get the open jwt token to connect to the sfu
const computeLocalTransportWithSFUConfig =
async (): Promise<LocalTransportWithSFUConfig> => {
return {
transport,
sfuConfig: await getSFUConfigWithOpenID(
client,
ownMembershipIdentity,
transport.livekit_service_url,
roomId,
{ forceJwtEndpoint: JwtEndpointVersion.Legacy },
logger,
),
};
};
return from(computeLocalTransportWithSFUConfig());
}),
), ),
null, null,
); );
@@ -108,19 +143,29 @@ export const createLocalTransport$ = ({
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken * @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
*/ */
const preferredTransport$ = scope.behavior( const preferredTransport$ = scope.behavior(
combineLatest([customLivekitUrl.value$, delayId$, useOldJwtEndpoint$]).pipe( // preferredTransport$ (used for multi sfu) needs to know if we are using the old or new
switchMap(([customUrl, delayId, forceOldJwtEndpoint]) => // jwt endpoint (`get_token` vs `sfu/get`) based on that the jwt endpoint will compute the rtcBackendIdentity
from( // differently. (sha(`${userId}|${deviceId}|${memberId}`) vs `${userId}|${deviceId}|${memberId}`)
// When using sticky events (we need to use the new endpoint).
combineLatest([customLivekitUrl.value$, delayId$, forceJwtEndpoint$]).pipe(
switchMap(([customUrl, delayId, forceEndpoint]) => {
logger.info(
"Creating preferred transport based on: ",
customUrl,
delayId,
forceEndpoint,
);
return from(
makeTransport( makeTransport(
client, client,
ownMembershipIdentity, ownMembershipIdentity,
roomId, roomId,
customUrl, customUrl,
forceOldJwtEndpoint, forceEndpoint,
delayId ?? undefined, delayId ?? undefined,
), ),
), );
), }),
), ),
null, null,
); );
@@ -139,7 +184,9 @@ export const createLocalTransport$ = ({
? (oldestMemberTransport ?? preferredTransport) ? (oldestMemberTransport ?? preferredTransport)
: preferredTransport, : preferredTransport,
), ),
distinctUntilChanged((t1, t2) => areLivekitTransportsEqual(t1, t2)), distinctUntilChanged((t1, t2) =>
areLivekitTransportsEqual(t1?.transport ?? null, t2?.transport ?? null),
),
), ),
); );
}; };
@@ -161,7 +208,10 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
* @param membership The membership identity of the user. * @param membership The membership identity of the user.
* @param roomId The ID of the room to be connected to. * @param roomId The ID of the room to be connected to.
* @param urlFromDevSettings Override URL provided by the user's local config. * @param urlFromDevSettings Override URL provided by the user's local config.
* @param forceOldJwtEndpoint Whether to force the old JWT endpoint (not hashing the backendIdentity). * @param forceJwtEndpoint Whether to force a specific JWT endpoint
* - `Legacy` / `Matrix_2_0`
* - `get_token` / `sfu/get`
* - not hashing / hashing the backendIdentity
* @param delayId the delay id passed to the jwt service. * @param delayId the delay id passed to the jwt service.
* *
* @returns A fully validated transport config. * @returns A fully validated transport config.
@@ -176,26 +226,33 @@ async function makeTransport(
membership: CallMembershipIdentityParts, membership: CallMembershipIdentityParts,
roomId: string, roomId: string,
urlFromDevSettings: string | null, urlFromDevSettings: string | null,
forceOldJwtEndpoint: boolean, forceJwtEndpoint: JwtEndpointVersion,
delayId?: string, delayId?: string,
): Promise<LivekitTransport> { ): Promise<LocalTransportWithSFUConfig> {
logger.trace("Searching for a preferred transport"); logger.trace("Searching for a preferred transport");
async function doOpenIdAndJWTFromUrl(url: string): Promise<LivekitTransport> { async function doOpenIdAndJWTFromUrl(
const { livekitAlias } = await getSFUConfigWithOpenID( url: string,
): Promise<LocalTransportWithSFUConfig> {
const sfuConfig = await getSFUConfigWithOpenID(
client, client,
membership, membership,
url, url,
forceOldJwtEndpoint,
roomId, roomId,
client.baseUrl, {
delayId, forceJwtEndpoint: forceJwtEndpoint,
delayEndpointBaseUrl: client.baseUrl,
delayId,
},
logger, logger,
); );
return { return {
type: "livekit", transport: {
livekit_service_url: url, type: "livekit",
livekit_alias: livekitAlias, livekit_service_url: url,
livekit_alias: sfuConfig.livekitAlias,
},
sfuConfig,
}; };
} }
// We will call `getSFUConfigWithOpenID` once per transport here as it's our // We will call `getSFUConfigWithOpenID` once per transport here as it's our
@@ -217,7 +274,7 @@ async function makeTransport(
async function getFirstUsableTransport( async function getFirstUsableTransport(
transports: Transport[], transports: Transport[],
): Promise<LivekitTransport | null> { ): Promise<LocalTransportWithSFUConfig | null> {
for (const potentialTransport of transports) { for (const potentialTransport of transports) {
if (isLivekitTransportConfig(potentialTransport)) { if (isLivekitTransportConfig(potentialTransport)) {
try { try {
@@ -226,8 +283,11 @@ async function makeTransport(
potentialTransport.livekit_service_url, potentialTransport.livekit_service_url,
); );
} catch (ex) { } catch (ex) {
// Explictly throw these
if (ex instanceof FailToGetOpenIdToken) { if (ex instanceof FailToGetOpenIdToken) {
// Explictly throw these throw ex;
}
if (ex instanceof NoMatrix2AuthorizationService) {
throw ex; throw ex;
} }
logger.debug( logger.debug(

View File

@@ -114,6 +114,7 @@ function setupRemoteConnection(): Connection {
client: client, client: client,
transport: livekitFocus, transport: livekitFocus,
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
}; };
@@ -138,7 +139,7 @@ function setupRemoteConnection(): Connection {
return Promise.resolve(); return Promise.resolve();
}); });
return new Connection(opts, logger, ownMemberMock); return new Connection(opts, logger);
} }
afterEach(() => { afterEach(() => {
@@ -155,9 +156,10 @@ describe("Start connection states", () => {
client: client, client: client,
transport: livekitFocus, transport: livekitFocus,
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
}; };
const connection = new Connection(opts, logger, ownMemberMock); const connection = new Connection(opts, logger);
expect(connection.state$.getValue()).toEqual("Initialized"); expect(connection.state$.getValue()).toEqual("Initialized");
}); });
@@ -170,10 +172,11 @@ describe("Start connection states", () => {
client: client, client: client,
transport: livekitFocus, transport: livekitFocus,
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
}; };
const connection = new Connection(opts, logger, ownMemberMock); const connection = new Connection(opts, logger);
const capturedStates: (ConnectionState | Error)[] = []; const capturedStates: (ConnectionState | Error)[] = [];
const s = connection.state$.subscribe((value) => { const s = connection.state$.subscribe((value) => {
@@ -220,10 +223,11 @@ describe("Start connection states", () => {
client: client, client: client,
transport: livekitFocus, transport: livekitFocus,
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
}; };
const connection = new Connection(opts, logger, ownMemberMock); const connection = new Connection(opts, logger);
const capturedStates: (ConnectionState | Error)[] = []; const capturedStates: (ConnectionState | Error)[] = [];
const s = connection.state$.subscribe((value) => { const s = connection.state$.subscribe((value) => {
@@ -277,10 +281,11 @@ describe("Start connection states", () => {
client: client, client: client,
transport: livekitFocus, transport: livekitFocus,
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
}; };
const connection = new Connection(opts, logger, ownMemberMock); const connection = new Connection(opts, logger);
const capturedStates: (ConnectionState | Error)[] = []; const capturedStates: (ConnectionState | Error)[] = [];
const s = connection.state$.subscribe((value) => { const s = connection.state$.subscribe((value) => {

View File

@@ -33,10 +33,21 @@ import {
SFURoomCreationRestrictedError, SFURoomCreationRestrictedError,
UnknownCallError, UnknownCallError,
} from "../../../utils/errors.ts"; } from "../../../utils/errors.ts";
import { type JwtEndpointVersion } from "../localMember/LocalTransport.ts";
export interface ConnectionOpts { export interface ConnectionOpts {
/** Whether we always try to connect to this connection via the legacy jwt endpoint. (no hash identity) */ /**
forceOldJwtEndpoint?: boolean; * For the local transport we already do know the jwt token and url. We can reuse it.
* On top the local transport will send additional data to the jwt server to use delayed event delegation.
*/
existingSFUConfig?: SFUConfig;
/**
* For local connections that use the oldest member pattern. here we have not prefetched the sfuConfig
* and hence we need to let the connection do the jwt token fetching.
*/
forceJwtEndpoint?: JwtEndpointVersion;
/** The identity parts to use on this connection */
ownMembershipIdentity: CallMembershipIdentityParts;
/** The media transport to connect to. */ /** The media transport to connect to. */
transport: LivekitTransport; transport: LivekitTransport;
/** The Matrix client to use for OpenID and SFU config requests. */ /** The Matrix client to use for OpenID and SFU config requests. */
@@ -132,8 +143,10 @@ export class Connection {
try { try {
this._state$.next(ConnectionState.FetchingConfig); this._state$.next(ConnectionState.FetchingConfig);
// We should already have this information after creating the localTransport. // We should already have this information after creating the localTransport.
// It would probably be better to forward this here. // only call getSFUConfigWithOpenID for connections where we do not have a token yet. (existingJwtTokenData === undefined)
const { url, jwt } = await this.getSFUConfigWithOpenID(); const { url, jwt } =
this.existingSFUConfig ??
(await this.getSFUConfigForRemoteConnection());
// If we were stopped while fetching the config, don't proceed to connect // If we were stopped while fetching the config, don't proceed to connect
if (this.stopped) return; if (this.stopped) return;
@@ -189,17 +202,16 @@ export class Connection {
} }
} }
protected async getSFUConfigWithOpenID(): Promise<SFUConfig> { protected async getSFUConfigForRemoteConnection(): Promise<SFUConfig> {
// This will only be called for sfu's where we do not publish ourselves.
// For the local connection we will use the existingJwtTokenData
return await getSFUConfigWithOpenID( return await getSFUConfigWithOpenID(
this.client, this.client,
this.ownMembershipIdentity, this.ownMembershipIdentity,
this.transport.livekit_service_url, this.transport.livekit_service_url,
this.forceOldJwtEndpoint,
this.transport.livekit_alias, this.transport.livekit_alias,
// For the remote members we intentionally do not pass a delayEndpointBaseUrl. // dont pass any custom opts for the subscribe only connections
undefined, {},
// and no delayId.
undefined,
this.logger, this.logger,
); );
} }
@@ -222,7 +234,8 @@ export class Connection {
private readonly client: OpenIDClientParts; private readonly client: OpenIDClientParts;
private readonly logger: Logger; private readonly logger: Logger;
private readonly forceOldJwtEndpoint: boolean; private readonly ownMembershipIdentity: CallMembershipIdentityParts;
private readonly existingSFUConfig?: SFUConfig;
/** /**
* Creates a new connection to a matrix RTC LiveKit backend. * Creates a new connection to a matrix RTC LiveKit backend.
* *
@@ -230,12 +243,9 @@ export class Connection {
* *
* @param logger - The logger to use. * @param logger - The logger to use.
*/ */
public constructor( public constructor(opts: ConnectionOpts, logger: Logger) {
opts: ConnectionOpts, this.ownMembershipIdentity = opts.ownMembershipIdentity;
logger: Logger, this.existingSFUConfig = opts.existingSFUConfig;
private ownMembershipIdentity: CallMembershipIdentityParts,
) {
this.forceOldJwtEndpoint = opts.forceOldJwtEndpoint ?? false;
this.logger = logger.getChild("[Connection]"); this.logger = logger.getChild("[Connection]");
this.logger.info( this.logger.info(
`Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`, `Creating new connection to ${opts.transport.livekit_service_url} ${opts.transport.livekit_alias}`,

View File

@@ -20,7 +20,10 @@ import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc/LivekitTransp
import { type ObservableScope } from "../../ObservableScope.ts"; import { type ObservableScope } from "../../ObservableScope.ts";
import { Connection } from "./Connection.ts"; import { Connection } from "./Connection.ts";
import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import type {
OpenIDClientParts,
SFUConfig,
} from "../../../livekit/openIDSFU.ts";
import type { MediaDevices } from "../../MediaDevices.ts"; import type { MediaDevices } from "../../MediaDevices.ts";
import type { Behavior } from "../../Behavior.ts"; import type { Behavior } from "../../Behavior.ts";
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
@@ -29,11 +32,11 @@ import { defaultLiveKitOptions } from "../../../livekit/options.ts";
// TODO evaluate if this should be done like the Publisher Factory // TODO evaluate if this should be done like the Publisher Factory
export interface ConnectionFactory { export interface ConnectionFactory {
createConnection( createConnection(
transport: LivekitTransport,
scope: ObservableScope, scope: ObservableScope,
transport: LivekitTransport,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
logger: Logger, logger: Logger,
forceOldJwtEndpoint?: boolean, sfuConfig?: SFUConfig,
): Connection; ): Connection;
} }
@@ -83,30 +86,30 @@ export class ECConnectionFactory implements ConnectionFactory {
/** /**
* *
* @param transport The transport to use for this connection.
* @param scope The observable scope (used for clean-up) * @param scope The observable scope (used for clean-up)
* @param transport The transport to use for this connection.
* @param ownMembershipIdentity required to connect (using the jwt service) with the SFU. * @param ownMembershipIdentity required to connect (using the jwt service) with the SFU.
* @param logger The logger instance to use for this connection. * @param logger The logger instance to use for this connection.
* @param forceOldJwtEndpoint Use the old JWT endpoint independent of what the sfu supports. * @param sfuConfig optional config in case we already have a token for this connection.
* @returns * @returns
*/ */
public createConnection( public createConnection(
transport: LivekitTransport,
scope: ObservableScope, scope: ObservableScope,
transport: LivekitTransport,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
logger: Logger, logger: Logger,
forceOldJwtEndpoint?: boolean, sfuConfig?: SFUConfig,
): Connection { ): Connection {
return new Connection( return new Connection(
{ {
existingSFUConfig: sfuConfig,
transport, transport,
client: this.client, client: this.client,
scope: scope, scope: scope,
livekitRoomFactory: this.livekitRoomFactory, livekitRoomFactory: this.livekitRoomFactory,
forceOldJwtEndpoint, ownMembershipIdentity,
}, },
logger, logger,
ownMembershipIdentity,
); );
} }
} }

View File

@@ -12,12 +12,17 @@ import { type Logger } from "matrix-js-sdk/lib/logger";
import { type RemoteParticipant } from "livekit-client"; import { type RemoteParticipant } from "livekit-client";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import { constant, type Behavior } from "../../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
import { type Connection } from "./Connection.ts"; import { 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 { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";
import {
isLocalTransportWithSFUConfig,
type LocalTransportWithSFUConfig,
} from "../localMember/LocalTransport.ts";
import { type SFUConfig } from "../../../livekit/openIDSFU.ts";
export class ConnectionManagerData { export class ConnectionManagerData {
private readonly store: Map< private readonly store: Map<
@@ -66,9 +71,9 @@ export class ConnectionManagerData {
interface Props { interface Props {
scope: ObservableScope; scope: ObservableScope;
connectionFactory: ConnectionFactory; connectionFactory: ConnectionFactory;
localTransport$: Behavior<LivekitTransport | null>; localTransport$: Behavior<LocalTransportWithSFUConfig | null>;
remoteTransports$: Behavior<Epoch<LivekitTransport[]>>; remoteTransports$: Behavior<Epoch<LivekitTransport[]>>;
forceOldJwtEndpointForLocalTransport$?: Behavior<boolean>;
logger: Logger; logger: Logger;
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
} }
@@ -87,7 +92,7 @@ export interface IConnectionManager {
* @param props.remoteTransports$ - All other transports. The connection manager will create connections for each transport. (deduplicated with localTransport$) * @param props.remoteTransports$ - All other transports. The connection manager will create connections for each transport. (deduplicated with localTransport$)
* @param props.ownMembershipIdentity - The own membership identity to use. * @param props.ownMembershipIdentity - The own membership identity to use.
* @param props.logger - The logger to use. * @param props.logger - The logger to use.
* @param props.forceOldJwtEndpointForLocalTransport$ - Use the old JWT endpoint independent of what the sfu supports. Only applies for localTransport$.
* *
* Each of these behaviors can be interpreted as subscribed list of transports. * Each of these behaviors can be interpreted as subscribed list of transports.
* *
@@ -103,7 +108,6 @@ export function createConnectionManager$({
connectionFactory, connectionFactory,
localTransport$, localTransport$,
remoteTransports$, remoteTransports$,
forceOldJwtEndpointForLocalTransport$ = constant(false),
logger: parentLogger, logger: parentLogger,
ownMembershipIdentity, ownMembershipIdentity,
}: Props): IConnectionManager { }: Props): IConnectionManager {
@@ -118,42 +122,35 @@ export function createConnectionManager$({
* It is build based on the list of subscribed transports (`transportsSubscriptions$`). * It is build based on the list of subscribed transports (`transportsSubscriptions$`).
* externally this is modified via `registerTransports()`. * externally this is modified via `registerTransports()`.
*/ */
const transportsWithJwtTag$ = scope.behavior( const localAndRemoteTransports$: Behavior<
combineLatest([ Epoch<(LivekitTransport | LocalTransportWithSFUConfig)[]>
remoteTransports$, > = scope.behavior(
localTransport$, combineLatest([remoteTransports$, localTransport$]).pipe(
forceOldJwtEndpointForLocalTransport$, // Combine local and remote transports into one transport array
]).pipe(
// combine local and remote transports into one transport array
// and set the forceOldJwtEndpoint property on the local transport // and set the forceOldJwtEndpoint property on the local transport
map( map(([remoteTransports, localTransport]) => {
([ let localTransportAsArray: LocalTransportWithSFUConfig[] = [];
remoteTransports, if (localTransport) {
localTransport, localTransportAsArray = [localTransport];
forceOldJwtEndpointForLocalTransport, }
]) => { const dedupedRemote = removeDuplicateTransports(remoteTransports.value);
let localTransportAsArray: (LivekitTransport & { const remoteWithoutLocal = dedupedRemote.filter(
forceOldJwtEndpoint: boolean; (transport) =>
})[] = []; !localTransportAsArray.find((l) =>
if (localTransport) { areLivekitTransportsEqual(l.transport, transport),
localTransportAsArray = [ ),
{ );
...localTransport, logger.debug(
forceOldJwtEndpoint: forceOldJwtEndpointForLocalTransport, "remoteWithoutLocal",
}, remoteWithoutLocal,
]; "localTransportAsArray",
} localTransportAsArray,
return new Epoch( );
removeDuplicateTransports([ return new Epoch(
...localTransportAsArray, [...localTransportAsArray, ...remoteWithoutLocal],
...remoteTransports.value, remoteTransports.epoch,
]) as (LivekitTransport & { );
forceOldJwtEndpoint?: boolean; }),
})[],
remoteTransports.epoch,
);
},
),
), ),
); );
@@ -161,33 +158,51 @@ export function createConnectionManager$({
* Connections for each transport in use by one or more session members. * Connections for each transport in use by one or more session members.
*/ */
const connections$ = scope.behavior( const connections$ = scope.behavior(
transportsWithJwtTag$.pipe( localAndRemoteTransports$.pipe(
generateItemsWithEpoch( generateItemsWithEpoch(
function* (transports) { function* (transports) {
for (const transport of transports) for (const transportWithOrWithoutSfuConfig of transports) {
yield { if (
keys: [ isLocalTransportWithSFUConfig(transportWithOrWithoutSfuConfig)
transport.livekit_service_url, ) {
transport.livekit_alias, // This is the local transport only the `LocalTransportWithSFUConfig` has a `sfuConfig` field
transport.forceOldJwtEndpoint, const { transport, sfuConfig } = transportWithOrWithoutSfuConfig;
], yield {
data: undefined, keys: [
}; transport.livekit_service_url,
transport.livekit_alias,
sfuConfig,
],
data: undefined,
};
} else {
const transport = transportWithOrWithoutSfuConfig;
yield {
keys: [
transport.livekit_service_url,
transport.livekit_alias,
undefined as undefined | SFUConfig,
],
data: undefined,
};
}
}
}, },
(scope, _data$, serviceUrl, alias, forceOldJwtEndpoint) => { (scope, _data$, serviceUrl, alias, sfuConfig) => {
logger.debug( logger.debug(
`Creating connection to ${serviceUrl} (${alias}, forceOldJwtEndpoint: ${forceOldJwtEndpoint})`, `Creating connection to ${serviceUrl} (${alias}, withSfuConfig (local connection?): ${JSON.stringify(sfuConfig) ?? "no config->remote connection"})`,
); );
const connection = connectionFactory.createConnection( const connection = connectionFactory.createConnection(
scope,
{ {
type: "livekit", type: "livekit",
livekit_service_url: serviceUrl, livekit_service_url: serviceUrl,
livekit_alias: alias, livekit_alias: alias,
}, },
scope,
ownMembershipIdentity, ownMembershipIdentity,
logger, logger,
forceOldJwtEndpoint, sfuConfig,
); );
// Start the connection immediately // Start the connection immediately
// Use connection state to track connection progress // Use connection state to track connection progress

View File

@@ -77,8 +77,8 @@ describe("ECConnectionFactory - Audio inputs options", () => {
noise, noise,
); );
ecConnectionFactory.createConnection( ecConnectionFactory.createConnection(
exampleTransport,
testScope, testScope,
exampleTransport,
ownMemberMock, ownMemberMock,
logger, logger,
); );
@@ -123,8 +123,8 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
false, false,
); );
ecConnectionFactory.createConnection( ecConnectionFactory.createConnection(
exampleTransport,
testScope, testScope,
exampleTransport,
ownMemberMock, ownMemberMock,
logger, logger,
); );

View File

@@ -44,6 +44,7 @@ import {
throttleTime, throttleTime,
distinctUntilChanged, distinctUntilChanged,
} from "rxjs"; } from "rxjs";
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
import { alwaysShowSelf } from "../settings/settings"; import { alwaysShowSelf } from "../settings/settings";
import { showConnectionStats } from "../settings/settings"; import { showConnectionStats } from "../settings/settings";
@@ -257,6 +258,7 @@ abstract class BaseMediaViewModel {
* The Matrix user to which this media belongs. * The Matrix user to which this media belongs.
*/ */
public readonly userId: string, public readonly userId: string,
public readonly rtcMembership: CallMembership,
// We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through // We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through
// livekit. // livekit.
protected readonly participant$: Observable< protected readonly participant$: Observable<
@@ -402,10 +404,13 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
*/ */
public readonly cropVideo$: Behavior<boolean> = this._cropVideo$; public readonly cropVideo$: Behavior<boolean> = this._cropVideo$;
public readonly rtcBackendIdentity = this.rtcMembership.rtcBackendIdentity;
public constructor( public constructor(
scope: ObservableScope, scope: ObservableScope,
id: string, id: string,
userId: string, userId: string,
rtcMembership: CallMembership,
participant$: Observable<LocalParticipant | RemoteParticipant | null>, participant$: Observable<LocalParticipant | RemoteParticipant | null>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>, livekitRoom$: Behavior<LivekitRoom | undefined>,
@@ -419,6 +424,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
scope, scope,
id, id,
userId, userId,
rtcMembership,
participant$, participant$,
encryptionSystem, encryptionSystem,
Track.Source.Microphone, Track.Source.Microphone,
@@ -544,6 +550,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
scope: ObservableScope, scope: ObservableScope,
id: string, id: string,
userId: string, userId: string,
rtcMembership: CallMembership,
participant$: Behavior<LocalParticipant | null>, participant$: Behavior<LocalParticipant | null>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>, livekitRoom$: Behavior<LivekitRoom | undefined>,
@@ -558,6 +565,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
scope, scope,
id, id,
userId, userId,
rtcMembership,
participant$, participant$,
encryptionSystem, encryptionSystem,
livekitRoom$, livekitRoom$,
@@ -671,6 +679,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
scope: ObservableScope, scope: ObservableScope,
id: string, id: string,
userId: string, userId: string,
rtcMembership: CallMembership,
participant$: Observable<RemoteParticipant | null>, participant$: Observable<RemoteParticipant | null>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>, livekitRoom$: Behavior<LivekitRoom | undefined>,
@@ -685,6 +694,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
scope, scope,
id, id,
userId, userId,
rtcMembership,
participant$, participant$,
encryptionSystem, encryptionSystem,
livekitRoom$, livekitRoom$,
@@ -772,6 +782,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
scope: ObservableScope, scope: ObservableScope,
id: string, id: string,
userId: string, userId: string,
rtcMembership: CallMembership,
participant$: Observable<LocalParticipant | RemoteParticipant>, participant$: Observable<LocalParticipant | RemoteParticipant>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>, livekitRoom$: Behavior<LivekitRoom | undefined>,
@@ -785,6 +796,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
scope, scope,
id, id,
userId, userId,
rtcMembership,
participant$, participant$,
encryptionSystem, encryptionSystem,
Track.Source.ScreenShareAudio, Track.Source.ScreenShareAudio,

View File

@@ -10,6 +10,7 @@ import {
type RemoteParticipant, type RemoteParticipant,
type Room as LivekitRoom, type Room as LivekitRoom,
} from "livekit-client"; } from "livekit-client";
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc/CallMembership";
import { type ObservableScope } from "./ObservableScope.ts"; import { type ObservableScope } from "./ObservableScope.ts";
import { ScreenShareViewModel } from "./MediaViewModel.ts"; import { ScreenShareViewModel } from "./MediaViewModel.ts";
@@ -28,6 +29,7 @@ export class ScreenShare {
private readonly scope: ObservableScope, private readonly scope: ObservableScope,
id: string, id: string,
userId: string, userId: string,
rtcMember: CallMembership,
participant: LocalParticipant | RemoteParticipant, participant: LocalParticipant | RemoteParticipant,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>, livekitRoom$: Behavior<LivekitRoom | undefined>,
@@ -40,6 +42,7 @@ export class ScreenShare {
this.scope, this.scope,
id, id,
userId, userId,
rtcMember,
of(participant), of(participant),
encryptionSystem, encryptionSystem,
livekitRoom$, livekitRoom$,

View File

@@ -13,6 +13,7 @@ import {
type Room as LivekitRoom, type Room as LivekitRoom,
} from "livekit-client"; } from "livekit-client";
import { observeParticipantEvents } from "@livekit/components-core"; import { observeParticipantEvents } from "@livekit/components-core";
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc/CallMembership";
import { type ObservableScope } from "./ObservableScope.ts"; import { type ObservableScope } from "./ObservableScope.ts";
import { import {
@@ -75,6 +76,7 @@ export class UserMedia {
this.scope, this.scope,
this.id, this.id,
this.userId, this.userId,
this.rtcMembership,
this.participant.value$, this.participant.value$,
this.encryptionSystem, this.encryptionSystem,
this.livekitRoom$, this.livekitRoom$,
@@ -89,6 +91,7 @@ export class UserMedia {
this.scope, this.scope,
this.id, this.id,
this.userId, this.userId,
this.rtcMembership,
this.participant.value$, this.participant.value$,
this.encryptionSystem, this.encryptionSystem,
this.livekitRoom$, this.livekitRoom$,
@@ -140,6 +143,7 @@ export class UserMedia {
scope, scope,
`${this.id}:${key}`, `${this.id}:${key}`,
this.userId, this.userId,
this.rtcMembership,
p, p,
this.encryptionSystem, this.encryptionSystem,
this.livekitRoom$, this.livekitRoom$,
@@ -191,6 +195,8 @@ export class UserMedia {
private readonly scope: ObservableScope, private readonly scope: ObservableScope,
public readonly id: string, public readonly id: string,
private readonly userId: string, private readonly userId: string,
// TODO evaluate if this should just be the rtcBackendIdentity
private readonly rtcMembership: CallMembership,
private readonly participant: TaggedParticipant, private readonly participant: TaggedParticipant,
private readonly encryptionSystem: EncryptionSystem, private readonly encryptionSystem: EncryptionSystem,
private readonly livekitRoom$: Behavior<LivekitRoom | undefined>, private readonly livekitRoom$: Behavior<LivekitRoom | undefined>,

View File

@@ -113,6 +113,7 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
}, },
[vm], [vm],
); );
const rtcBackendIdentity = vm.rtcBackendIdentity;
const handRaised = useBehavior(vm.handRaised$); const handRaised = useBehavior(vm.handRaised$);
const reaction = useBehavior(vm.reaction$); const reaction = useBehavior(vm.reaction$);
@@ -200,6 +201,7 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
focusUrl={focusUrl} focusUrl={focusUrl}
audioStreamStats={audioStreamStats} audioStreamStats={audioStreamStats}
videoStreamStats={videoStreamStats} videoStreamStats={videoStreamStats}
rtcBackendIdentity={rtcBackendIdentity}
{...props} {...props}
/> />
); );

View File

@@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof animated.div> {
waitingForMedia?: boolean; waitingForMedia?: boolean;
audioStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats; audioStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
videoStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats; videoStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
rtcBackendIdentity?: string;
// The focus url, mainly for debugging purposes // The focus url, mainly for debugging purposes
focusUrl?: string; focusUrl?: string;
} }
@@ -74,6 +75,7 @@ export const MediaView: FC<Props> = ({
waitingForMedia, waitingForMedia,
audioStreamStats, audioStreamStats,
videoStreamStats, videoStreamStats,
rtcBackendIdentity,
focusUrl, focusUrl,
...props ...props
}) => { }) => {
@@ -135,11 +137,14 @@ export const MediaView: FC<Props> = ({
</div> </div>
)} )}
{(audioStreamStats || videoStreamStats) && ( {(audioStreamStats || videoStreamStats) && (
<RTCConnectionStats <>
audio={audioStreamStats} <RTCConnectionStats
video={videoStreamStats} audio={audioStreamStats}
focusUrl={focusUrl} video={videoStreamStats}
/> focusUrl={focusUrl}
rtcBackendIdentity={rtcBackendIdentity}
/>
</>
)} )}
{/* TODO: Bring this back once encryption status is less broken */} {/* TODO: Bring this back once encryption status is less broken */}
{/*encryptionStatus !== EncryptionStatus.Okay && ( {/*encryptionStatus !== EncryptionStatus.Okay && (

View File

@@ -19,6 +19,7 @@ export enum ErrorCode {
INSUFFICIENT_CAPACITY_ERROR = "INSUFFICIENT_CAPACITY_ERROR", INSUFFICIENT_CAPACITY_ERROR = "INSUFFICIENT_CAPACITY_ERROR",
E2EE_NOT_SUPPORTED = "E2EE_NOT_SUPPORTED", E2EE_NOT_SUPPORTED = "E2EE_NOT_SUPPORTED",
OPEN_ID_ERROR = "OPEN_ID_ERROR", OPEN_ID_ERROR = "OPEN_ID_ERROR",
NO_MATRIX_2_AUTHORIZATION_SERVICE = "NO_MATRIX_2_0_AUTHORIZATION_SERVICE",
SFU_ERROR = "SFU_ERROR", SFU_ERROR = "SFU_ERROR",
UNKNOWN_ERROR = "UNKNOWN_ERROR", UNKNOWN_ERROR = "UNKNOWN_ERROR",
} }
@@ -171,6 +172,23 @@ export class FailToGetOpenIdToken extends ElementCallError {
} }
} }
export class NoMatrix2AuthorizationService extends ElementCallError {
/**
* Creates an instance of NoMatrix2_0AuthorizationService.
* @param error - The underlying error that caused the failure.
*/
public constructor(error: Error) {
super(
t("error.generic"),
ErrorCode.NO_MATRIX_2_AUTHORIZATION_SERVICE,
ErrorCategory.CONFIGURATION_ISSUE,
t("error.no_matrix_2_authorization_service"),
// Properly set it as a cause for a better reporting on sentry
error,
);
}
}
/** /**
* Error indicating a failure to start publishing on a LiveKit connection. * Error indicating a failure to start publishing on a LiveKit connection.
*/ */

View File

@@ -334,6 +334,7 @@ export function createLocalMedia(
testScope(), testScope(),
"local", "local",
member.userId, member.userId,
rtcMember,
constant(localParticipant), constant(localParticipant),
{ {
kind: E2eeType.PER_PARTICIPANT, kind: E2eeType.PER_PARTICIPANT,
@@ -379,6 +380,7 @@ export function createRemoteMedia(
testScope(), testScope(),
"remote", "remote",
member.userId, member.userId,
rtcMember,
constant(participant), constant(participant),
{ {
kind: E2eeType.PER_PARTICIPANT, kind: E2eeType.PER_PARTICIPANT,

View File

@@ -8246,7 +8246,7 @@ __metadata:
livekit-client: "npm:^2.13.0" livekit-client: "npm:^2.13.0"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.21"
loglevel: "npm:^1.9.1" loglevel: "npm:^1.9.1"
matrix-js-sdk: "matrix-org/matrix-js-sdk#develop" matrix-js-sdk: "matrix-org/matrix-js-sdk#ff5fab7722e54be2d69a380cd6182ecb262d7859"
matrix-widget-api: "npm:^1.14.0" matrix-widget-api: "npm:^1.14.0"
node-stdlib-browser: "npm:^1.3.1" node-stdlib-browser: "npm:^1.3.1"
normalize.css: "npm:^8.0.1" normalize.css: "npm:^8.0.1"
@@ -11333,9 +11333,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"matrix-js-sdk@matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@portal:/Users/timo/Projects/matrix-js-sdk::locator=element-call%40workspace%3A.":
version: 39.4.0 version: 0.0.0-use.local
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=4b89fb23c54aaf7826bd127d8fa21cc7bb87688f" resolution: "matrix-js-sdk@portal:/Users/timo/Projects/matrix-js-sdk::locator=element-call%40workspace%3A."
dependencies: dependencies:
"@babel/runtime": "npm:^7.12.5" "@babel/runtime": "npm:^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^16.0.0"
@@ -11351,9 +11351,8 @@ __metadata:
sdp-transform: "npm:^3.0.0" sdp-transform: "npm:^3.0.0"
unhomoglyph: "npm:^1.0.6" unhomoglyph: "npm:^1.0.6"
uuid: "npm:13" uuid: "npm:13"
checksum: 10c0/bc7443bf67822e9bc7b8e531b4e61e6ebac41c2fd8047ac0567456c264ae0d1911fbef6e437d312a3adeead86cd5e7134944e3fd73d28002777618bc0ebaa1ca
languageName: node languageName: node
linkType: hard linkType: soft
"matrix-widget-api@npm:^1.14.0": "matrix-widget-api@npm:^1.14.0":
version: 1.15.0 version: 1.15.0