From 83e1396bdd199711cdbc0d1c74118304bdb28aa0 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 12 Oct 2026 10:30:52 +0200 Subject: [PATCH] Use the unstable LiveKit transport type UnstableLivekitTransport covers every shape a homeserver may advertise while MSC4195 evolves: a LiveKit SFU `url`, a `livekit_service_url`, or both. Switching to it means a service URL is no longer guaranteed, so the remaining code that relies on one checks for it explicitly: delegation probing skips transports without one, the connection map keys transports by whichever URL they carry, and the OpenID flow rejects transports without a service URL until the client-server API flow lands. --- src/RTCConnectionStats.tsx | 4 +- src/livekit/MatrixAudioRenderer.tsx | 4 +- src/room/CallEventAudioRenderer.test.tsx | 4 +- src/room/InCallView.tsx | 7 +++- src/state/CallViewModel/CallViewModel.test.ts | 4 +- src/state/CallViewModel/CallViewModel.ts | 8 ++-- .../localMember/LocalMember.test.ts | 8 ++-- .../CallViewModel/localMember/LocalMember.ts | 30 +++++++-------- .../localMember/LocalTransport.ts | 6 +-- .../RtcTransportAutoDiscovery.test.ts | 6 +-- .../localMember/RtcTransportAutoDiscovery.ts | 12 +++--- .../remoteMembers/Connection.test.ts | 34 ++++++++--------- .../CallViewModel/remoteMembers/Connection.ts | 8 ++-- .../remoteMembers/ConnectionFactory.ts | 6 +-- .../remoteMembers/ConnectionMap.test.ts | 24 +++++++----- .../remoteMembers/ConnectionMap.ts | 37 +++++++++++-------- .../MatrixLivekitMembers.test.ts | 14 ++++--- .../remoteMembers/MatrixLivekitMembers.ts | 11 ++++-- .../remoteMembers/integration.test.ts | 8 ++-- src/state/SessionBehaviors.ts | 27 ++++++++------ src/state/media/MemberMediaViewModel.ts | 6 +-- src/tile/GridTile.tsx | 4 +- src/tile/MediaView.tsx | 4 +- src/tile/SpotlightTile.tsx | 4 +- src/utils/test.ts | 4 +- 25 files changed, 157 insertions(+), 127 deletions(-) diff --git a/src/RTCConnectionStats.tsx b/src/RTCConnectionStats.tsx index 90fefb656..a19cdd977 100644 --- a/src/RTCConnectionStats.tsx +++ b/src/RTCConnectionStats.tsx @@ -16,12 +16,12 @@ import classNames from "classnames"; import { Modal } from "./Modal"; import styles from "./RTCConnectionStats.module.css"; import mediaViewStyles from "../src/tile/MediaView.module.css"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; interface Props { audio?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats; video?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats; - transport?: LivekitTransport; + transport?: UnstableLivekitTransport; rtcBackendIdentity?: string; } diff --git a/src/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx index c9c4bbb36..7f5e8b8fa 100644 --- a/src/livekit/MatrixAudioRenderer.tsx +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -19,10 +19,10 @@ import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { useEarpieceAudioConfig } from "../MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; import * as controls from "../controls"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; export interface MatrixAudioRendererProps { - transport: LivekitTransport; + transport: UnstableLivekitTransport; livekitRoom: LivekitRoom; /** * The list of participant identities to render audio for. diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 9ab64dfd6..1d8fd33d2 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -17,7 +17,7 @@ import { import { act } from "react"; import { type RoomMember } from "matrix-js-sdk"; import { - type LivekitTransport, + type UnstableLivekitTransport, type CallMembership, } from "matrix-js-sdk/lib/matrixrtc"; @@ -46,7 +46,7 @@ vitest.mock("../useAudioContext"); vitest.mock("../soundUtils"); vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({ ...(await importOriginal()), - makeTransport: (): [LivekitTransport] => [exampleTransport], + makeTransport: (): [UnstableLivekitTransport] => [exampleTransport], })); afterEach(() => { diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 8b66eb160..7f35c3887 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -699,7 +699,12 @@ export const InCallView: FC = ({ livekitAlias: connectionItem.livekitAlias, // TODO compute is local or tag it in the livekit room items already isLocal: undefined, - url: connectionItem.transport.livekit_service_url, + url: + // It should be relatively clear to the user which kind of URL + // this is, based on the scheme (ws:// vs. http://) + "url" in connectionItem.transport + ? connectionItem.transport.url + : connectionItem.transport.livekit_service_url, }))} /> diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index c7783d152..1b1c38d4a 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -31,7 +31,7 @@ import { type CallMembership, type IRTCNotificationContent, MatrixRTCSessionEvent, - type LivekitTransport, + type UnstableLivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; import { deepCompare } from "matrix-js-sdk/lib/utils"; @@ -97,7 +97,7 @@ vi.mock( "../state/CallViewModel/localMember/localTransport", async (importOriginal) => ({ ...(await importOriginal()), - makeTransport: async (): Promise => + makeTransport: async (): Promise => Promise.resolve(exampleTransport), }), ); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index a005c05ed..d6791c995 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -46,7 +46,7 @@ import { import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { MembershipManagerEvent, - type LivekitTransport, + type UnstableLivekitTransport, type MatrixRTCSession, type RTCCallIntent, type RTCNotificationType, @@ -217,7 +217,7 @@ export interface CallViewModelOptions { */ windowSize$: Behavior<{ width: number; height: number }>; /** Optional value overriding the local transport, for testing purposes. */ - localTransport?: LivekitTransport; + localTransport?: UnstableLivekitTransport; /** Optional value overriding the connection factory, for testing purposes. */ connectionFactory?: ConnectionFactory; /** The version & compatibility mode of MatrixRTC that we should use. */ @@ -278,7 +278,7 @@ interface LayoutScanState { export type LivekitRoomItem = { livekitRoom: LivekitRoom; participants: string[]; - transport: LivekitTransport; + transport: UnstableLivekitTransport; }; /** @@ -619,7 +619,7 @@ export function createCallViewModel$( ), muteStates, joinMatrixRTC: ( - transport: LivekitTransport, + transport: UnstableLivekitTransport, delayedLeaveTimings: ResolvedDelayedLeaveTimings, ) => { return enterRTCSession( diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 543cada8c..437cdd20e 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { Status as RTCMemberStatus, - type LivekitTransport, + type UnstableLivekitTransport, type MatrixRTCSession, } from "matrix-js-sdk/lib/matrixrtc"; import { @@ -127,7 +127,7 @@ const mockedClient = { }; describe("enterRTCSession", () => { - const transport: LivekitTransport = { + const transport: UnstableLivekitTransport = { livekit_service_url: "http://my-livekit-service-url.com", type: "livekit", }; @@ -306,7 +306,7 @@ describe("LocalMembership", () => { const mockTransport = { livekit_service_url: "a", - } as LivekitTransport; + } as UnstableLivekitTransport; const connectionTransportAConnected = { livekitRoom: mockLivekitRoom({ @@ -459,7 +459,7 @@ describe("LocalMembership", () => { it("tracks livekit state correctly", async () => { const scope = new ObservableScope(); const connectionMap = new ConnectionMap(); - const localTransport$ = new Subject(); + const localTransport$ = new Subject(); const connectionMap$ = new BehaviorSubject(new Epoch(connectionMap)); const publishers: Publisher[] = []; diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 482627ee2..1b1e8aae4 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -18,7 +18,7 @@ import { import { observeParticipantEvents } from "@livekit/components-core"; import { Status as RTCSessionStatus, - type LivekitTransport, + type UnstableLivekitTransport, type MatrixRTCSession, type RTCCallIntent, type RTCNotificationType, @@ -141,13 +141,13 @@ interface Props { connectionMap$: Behavior>; createPublisherFactory: (connection: Connection) => Publisher; joinMatrixRTC: ( - transport: LivekitTransport, + transport: UnstableLivekitTransport, delayedLeaveTimings: ResolvedDelayedLeaveTimings, ) => void; homeserverConnected: HomeserverConnected; roomId: string; ownMembershipIdentity: CallMembershipIdentityParts; - localTransport$: Observable; + localTransport$: Observable; client: ClientOpenIDParts; matrixRTCSession: Pick< MatrixRTCSession, @@ -307,15 +307,16 @@ export const createLocalMembership$ = ({ const joinParams$ = scope.behavior( localTransport$.pipe( switchMap(async (transport) => { - const transportSupportsDelegation = checkDelegationSupport( - transport.livekit_service_url + "/delegate_delayed_leave", - `transport ${transport.livekit_service_url}`, - ); + const transportSupportsDelegation = + "livekit_service_url" in transport && + (await checkDelegationSupport( + transport.livekit_service_url + "/delegate_delayed_leave", + `transport ${transport.livekit_service_url}`, + )); return { transport, delegationSupported: - (await homeserverSupportsDelegation) || - (await transportSupportsDelegation), + transportSupportsDelegation || (await homeserverSupportsDelegation), }; }), ), @@ -715,6 +716,8 @@ export const createLocalMembership$ = ({ async ([joinParams, delayId]) => { if (joinParams?.delegationSupported && delayId !== null) { try { + if (!("livekit_service_url" in joinParams.transport)) + throw new Error("Transport has no livekit_service_url"); // 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. @@ -731,10 +734,7 @@ export const createLocalMembership$ = ({ }); } catch (e) { // TODO: Surface this to the user as a service interruption? - logger.error( - `Failed to delegate leave to ${joinParams.transport.livekit_service_url}`, - e, - ); + logger.error(`Failed to delegate leave`, e); } } }, @@ -967,7 +967,7 @@ interface EnterRTCSessionOptions { * * @param rtcSession - The MatrixRTCSession to join. * @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 * Matrix RTC mode to use. `delayedLeaveTimings`: The preferred timings for * delayed leave events. `sendNotificationType`: Whether and what kind of @@ -978,7 +978,7 @@ interface EnterRTCSessionOptions { export function enterRTCSession( rtcSession: MatrixRTCSession, ownMembershipIdentity: CallMembershipIdentityParts, - transport: LivekitTransport, + transport: UnstableLivekitTransport, { encryptMedia, matrixRTCMode, diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 891f70ccf..f2572bf30 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -5,7 +5,7 @@ SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 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 { logger as rootLogger } from "matrix-js-sdk/lib/logger"; @@ -20,7 +20,7 @@ import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; */ export async function getLocalTransport( client: Pick, -): Promise { +): Promise { const discovery = new RtcTransportAutoDiscovery({ client, resolvedConfig: Config.get(), @@ -29,7 +29,7 @@ export async function getLocalTransport( const customUrl = customLivekitUrl.value$.value; // Respect the user's custom URL, if set - const transport: LivekitTransport | null = customUrl + const transport: UnstableLivekitTransport | null = customUrl ? { type: "livekit", livekit_service_url: customUrl } : await discovery.discoverPreferredTransport(); diff --git a/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts index 61df88a73..d3f246c97 100644 --- a/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts +++ b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.test.ts @@ -16,7 +16,7 @@ import { import { MatrixError } from "matrix-js-sdk"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { - type LivekitTransport, + type UnstableLivekitTransport, type Transport, } from "matrix-js-sdk/lib/matrixrtc"; @@ -28,12 +28,12 @@ import { type DiscoveryClient = RtcTransportAutoDiscoveryProps["client"]; -const backendTransport: LivekitTransport = { +const backendTransport: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://backend.example.org", }; -const configTransport: LivekitTransport = { +const configTransport: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://config.example.org", }; diff --git a/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts index 58a5f1b19..a5bedcd80 100644 --- a/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts +++ b/src/state/CallViewModel/localMember/RtcTransportAutoDiscovery.ts @@ -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. */ import { - isLivekitTransport, - type LivekitTransport, + isUnstableLivekitTransport, + type UnstableLivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; import { type MatrixClient } from "matrix-js-sdk"; import { type Logger } from "matrix-js-sdk/lib/logger"; @@ -40,7 +40,7 @@ export class RtcTransportAutoDiscovery { this.logger = logger.getChild("[RtcTransportAutoDiscovery]"); } - public async discoverPreferredTransport(): Promise { + public async discoverPreferredTransport(): Promise { // 1) backend transports const backendTransport = await this.tryBackendTransports(); 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. * @private */ - private async tryBackendTransports(): Promise { + private async tryBackendTransports(): Promise { const client = this.client; // MSC4143: Attempt to fetch transports from backend. this.logger.info("First try to use getRTCTransports end point ..."); @@ -75,7 +75,7 @@ export class RtcTransportAutoDiscovery { const transportList = await doNetworkOperationWithRetry(async () => client._unstable_getRTCTransports(), ); - const first = transportList.find(isLivekitTransport); + const first = transportList.find(isUnstableLivekitTransport); if (first) { return first; } else { @@ -90,7 +90,7 @@ export class RtcTransportAutoDiscovery { return null; } - private tryConfigTransport(): LivekitTransport | null { + private tryConfigTransport(): UnstableLivekitTransport | null { const url = this.resolvedConfig.livekit?.livekit_service_url; if (url) { return { diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 5afd79dec..98a1aed87 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -29,7 +29,7 @@ import fetchMock from "fetch-mock"; import EventEmitter from "events"; import { type IOpenIDToken } from "matrix-js-sdk"; 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 { Connection, @@ -54,8 +54,9 @@ let fakeLocalParticipant: MockedObject; const ROOM_ID = "!roomID:example.org"; const SLOT_ID = "m.call#ROOM"; -const livekitFocus: LivekitTransport = { - livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", +const livekitServiceUrl = "https://matrix-rtc.example.org/livekit/jwt"; +const livekitFocus: UnstableLivekitTransport = { + livekit_service_url: livekitServiceUrl, type: "livekit", }; @@ -122,7 +123,7 @@ function setupRemoteConnection(): Connection { livekitRoomFactory: () => fakeLivekitRoom, }; - fetchMock.post(`${livekitFocus.livekit_service_url}/get_token`, () => { + fetchMock.post(`${livekitServiceUrl}/get_token`, () => { return { status: 200, body: { @@ -250,19 +251,16 @@ describe("Start connection states", () => { const deferredSFU = Promise.withResolvers(); // mock the /get_token call - fetchMock.post( - `${livekitFocus.livekit_service_url}/get_token`, - async () => { - await deferredSFU.promise; - return { - status: 500, - body: { - errcode: "M_LOOKUP_FAILED", - error: "Failed to look up user info from homeserver", - }, - }; - }, - ); + fetchMock.post(`${livekitServiceUrl}/get_token`, async () => { + await deferredSFU.promise; + return { + status: 500, + body: { + errcode: "M_LOOKUP_FAILED", + error: "Failed to look up user info from homeserver", + }, + }; + }); connection.start().catch(() => { // expected to throw @@ -317,7 +315,7 @@ describe("Start connection states", () => { const deferredSFU = Promise.withResolvers(); // mock the /get_token call - fetchMock.post(`${livekitFocus.livekit_service_url}/get_token`, () => { + fetchMock.post(`${livekitServiceUrl}/get_token`, () => { return { status: 200, body: { diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index 15b0559cb..575ddfad6 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -23,7 +23,7 @@ import { type Track, type TrackPublication, } from "livekit-client"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; @@ -50,7 +50,7 @@ export interface ConnectionOpts { /** Whether we want to publish or only subscribe on this connection. */ role: "publisher" | "subscriber"; /** The media transport to connect to. */ - transport: LivekitTransport; + transport: UnstableLivekitTransport; /** The Matrix client to use for OpenID and SFU config requests. */ client: ClientOpenIDParts; /** The ID of the Matrix room in which the session takes place. */ @@ -97,7 +97,7 @@ export class Connection { /** * The media transport to connect to. */ - public readonly transport: LivekitTransport; + public readonly transport: UnstableLivekitTransport; public readonly livekitRoom: LivekitRoom; @@ -377,6 +377,8 @@ export class Connection { } protected async getSFUConfig(): Promise { + if (!("livekit_service_url" in this.transport)) + throw new Error("Transport has no livekit_service_url"); return await getSFUConfig({ client: this.client, membership: this.ownMembershipIdentity, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts index b062e8d20..b2a4833d7 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts @@ -16,7 +16,7 @@ import { logger, type Logger } from "matrix-js-sdk/lib/logger"; // imported as inline to support worker when loaded from a cdn (cross domain) import E2EEWorker from "livekit-client/e2ee-worker?worker&inline"; 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 { Connection } from "./Connection.ts"; @@ -42,7 +42,7 @@ export interface ConnectionFactory { createConnection( scope: ObservableScope, role: "publisher" | "subscriber", - transport: LivekitTransport, + transport: UnstableLivekitTransport, ownMembershipIdentity: CallMembershipIdentityParts, logger: Logger, ): Connection; @@ -103,7 +103,7 @@ export class ECConnectionFactory implements ConnectionFactory { public createConnection( scope: ObservableScope, role: "publisher" | "subscriber", - transport: LivekitTransport, + transport: UnstableLivekitTransport, ownMembershipIdentity: CallMembershipIdentityParts, logger: Logger, ): Connection { diff --git a/src/state/CallViewModel/remoteMembers/ConnectionMap.test.ts b/src/state/CallViewModel/remoteMembers/ConnectionMap.test.ts index e0f7add1c..dba1cb0be 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionMap.test.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionMap.test.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { BehaviorSubject } 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 { logger } from "matrix-js-sdk/lib/logger"; @@ -21,18 +21,18 @@ import { type Behavior } from "../../Behavior.ts"; // Some test constants -const TRANSPORT_1: LivekitTransport = { +const TRANSPORT_1: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://lk.example.org", }; -const TRANSPORT_2: LivekitTransport = { +const TRANSPORT_2: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://lk.sample.com", }; function findConnection( - transport: LivekitTransport, + transport: UnstableLivekitTransport, connections: Connection[], ): Connection | undefined { return connections.find((c) => @@ -53,7 +53,11 @@ beforeEach(() => { vi.mocked(fakeConnectionFactory).createConnection = vi .fn() .mockImplementation( - (scope: ObservableScope, _role: unknown, transport: LivekitTransport) => { + ( + scope: ObservableScope, + _role: unknown, + transport: UnstableLivekitTransport, + ) => { const mockConnection = { transport, remoteParticipants$: new BehaviorSubject([]), @@ -224,15 +228,17 @@ describe("connectionMap$", () => { // Used in test to control fake connections' remoteParticipants$ streams let fakeRemoteParticipantsStreams: Map>; - function keyForTransport(transport: LivekitTransport): string { - return `${transport.livekit_service_url}`; + function keyForTransport(transport: UnstableLivekitTransport): string { + return "livekit_service_url" in transport + ? transport.livekit_service_url + : transport.url; } beforeEach(() => { fakeRemoteParticipantsStreams = new Map(); function getRemoteParticipantsFor( - transport: LivekitTransport, + transport: UnstableLivekitTransport, ): Behavior { return ( fakeRemoteParticipantsStreams.get(keyForTransport(transport)) ?? @@ -247,7 +253,7 @@ describe("connectionMap$", () => { ( scope: ObservableScope, _role: unknown, - transport: LivekitTransport, + transport: UnstableLivekitTransport, ) => { const fakeRemoteParticipants$ = new BehaviorSubject< RemoteParticipant[] diff --git a/src/state/CallViewModel/remoteMembers/ConnectionMap.ts b/src/state/CallViewModel/remoteMembers/ConnectionMap.ts index 67253bf38..698d9ad97 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionMap.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionMap.ts @@ -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. */ -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, map, of, switchMap, tap } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type RemoteParticipant } from "livekit-client"; @@ -23,7 +23,17 @@ import { generateItemsWithEpoch } from "../../../utils/observable.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts"; -function removeDuplicateTransports( +/** + * The URL by which a transport is identified: its JWT service URL, or failing + * that, its SFU URL. + */ +function urlOf(transport: UnstableLivekitTransport): string { + return "livekit_service_url" in transport + ? transport.livekit_service_url + : transport.url; +} + +function removeDuplicateTransports( transports: T[], ): T[] { return transports.reduce((acc, transport) => { @@ -56,10 +66,10 @@ export class ConnectionMap { } } - private getKey(transport: LivekitTransport): string { + private getKey(transport: UnstableLivekitTransport): string { // This is enough as a key because the ConnectionMap 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}`. - return transport.livekit_service_url; + return urlOf(transport); } public getConnections(): Connection[] { @@ -67,13 +77,13 @@ export class ConnectionMap { } public getConnectionForTransport( - transport: LivekitTransport, + transport: UnstableLivekitTransport, ): Connection | null { return this.map.get(this.getKey(transport))?.connection ?? null; } public getParticipantsForTransport( - transport: LivekitTransport, + transport: UnstableLivekitTransport, ): RemoteParticipant[] { const existing = this.map.get(this.getKey(transport)); if (existing) { @@ -86,8 +96,8 @@ export class ConnectionMap { interface Props { scope: ObservableScope; connectionFactory: ConnectionFactory; - localTransport: LivekitTransport; - remoteTransports$: Behavior>; + localTransport: UnstableLivekitTransport; + remoteTransports$: Behavior>; logger: Logger; ownMembershipIdentity: CallMembershipIdentityParts; @@ -118,7 +128,7 @@ export function createConnectionMap$({ * All transports currently represented in the connection map. This list does * not include duplicate transports. */ - const localAndRemoteTransports$: Behavior> = + const localAndRemoteTransports$: Behavior> = scope.behavior( remoteTransports$.pipe( mapEpoch((remoteTransports) => @@ -142,17 +152,14 @@ export function createConnectionMap$({ const role = areLivekitTransportsEqual(transport, localTransport) ? ("publisher" as const) : ("subscriber" as const); - yield { - keys: [role, transport.livekit_service_url], - data: undefined, - }; + yield { keys: [role, urlOf(transport)], data: transport }; } }, - (scope, _data$, role, serviceUrl) => { + (scope, transport$, role, _url) => { const connection = connectionFactory.createConnection( scope, role, - { type: "livekit", livekit_service_url: serviceUrl }, + transport$.value, ownMembershipIdentity, logger, ); diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts index 42548a79a..af98b2342 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts @@ -9,7 +9,7 @@ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { logger } from "matrix-js-sdk/lib/logger"; import { type CallMembership, - type LivekitTransport, + type UnstableLivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs"; @@ -38,12 +38,12 @@ let testScope: ObservableScope; const fallbackMemberId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; -const transportA: LivekitTransport = { +const transportA: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://lk.example.org", }; -const transportB: LivekitTransport = { +const transportB: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://lk.sample.com", }; @@ -111,7 +111,9 @@ test("should signal participant not yet connected to livekit", async () => { function createEpochedMemberships$(m$: Observable): { memberships$: Observable>; membershipsWithTransport$: Observable< - Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> + Epoch< + { membership: CallMembership; transport?: UnstableLivekitTransport }[] + > >; } { const memberships$ = m$.pipe(trackEpoch()); @@ -122,7 +124,9 @@ function createEpochedMemberships$(m$: Observable): { return { membership: m, transport: - tr?.type === "livekit" ? (tr as LivekitTransport) : undefined, + tr?.type === "livekit" + ? (tr as UnstableLivekitTransport) + : undefined, }; }); }), diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 8c5f21d77..46735bb2f 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -8,10 +8,11 @@ Please see LICENSE in the repository root for full details. import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; import { type CallMembership, - type LivekitTransport, + type UnstableLivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, filter, map } from "rxjs"; import { logger } from "matrix-js-sdk/lib/logger"; +import { deepCompare } from "matrix-js-sdk/lib/utils"; import { type Behavior } from "../../Behavior"; import { type ConnectionMap } from "./ConnectionMap"; @@ -60,7 +61,9 @@ export interface RemoteMatrixLivekitMember extends MatrixLivekitMember { interface Props { scope: ObservableScope; membershipsWithTransport$: Behavior< - Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> + Epoch< + { membership: CallMembership; transport?: UnstableLivekitTransport }[] + > >; connectionMap$: Behavior>; localUser: { deviceId: string; userId: string }; @@ -162,12 +165,12 @@ export function createRemoteMatrixLivekitMembers$({ // TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$) // TODO add this to the JS-SDK -export function areLivekitTransportsEqual( +export function areLivekitTransportsEqual( t1: T | null, t2: T | null, ): boolean { if (t1 && t2) { - return t1.livekit_service_url === t2.livekit_service_url; + return deepCompare(t1, t2); } return !t1 && !t2; } diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index df3ff8dc3..1a965cc02 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -10,7 +10,7 @@ import { BehaviorSubject } from "rxjs"; import { type Room as LivekitRoom } from "livekit-client"; import EventEmitter from "events"; 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 { @@ -152,7 +152,7 @@ test("bob, carl, then bob joining no tracks yet", () => { a: expect.toSatisfy((co) => areLivekitTransportsEqual( co.transport, - bobMembership.transports[0]! as LivekitTransport, + bobMembership.transports[0]! as UnstableLivekitTransport, ), ), }); @@ -189,7 +189,7 @@ test("bob, carl, then bob joining no tracks yet", () => { expect( areLivekitTransportsEqual( connection.transport, - carlMembership.transports[0]! as LivekitTransport, + carlMembership.transports[0]! as UnstableLivekitTransport, ), ).toBe(true); return true; @@ -219,7 +219,7 @@ test("bob, carl, then bob joining no tracks yet", () => { expect( areLivekitTransportsEqual( connection.transport, - daveMembership.transports[0]! as LivekitTransport, + daveMembership.transports[0]! as UnstableLivekitTransport, ), ).toBe(true); return true; diff --git a/src/state/SessionBehaviors.ts b/src/state/SessionBehaviors.ts index 4ca1c5861..435e77021 100644 --- a/src/state/SessionBehaviors.ts +++ b/src/state/SessionBehaviors.ts @@ -7,10 +7,10 @@ Please see LICENSE in the repository root for full details. import { type CallMembership, - type LivekitTransport, + type UnstableLivekitTransport, type MatrixRTCSession, MatrixRTCSessionEvent, - isLivekitTransport, + isUnstableLivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; import { fromEvent } from "rxjs"; @@ -31,15 +31,17 @@ export const membershipsAndTransports$ = ( memberships$: Behavior>, ): { membershipsWithTransport$: Behavior< - Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> + Epoch< + { membership: CallMembership; transport?: UnstableLivekitTransport }[] + > >; - transports$: Behavior>; + transports$: Behavior>; } => { const membershipsWithTransport$: Behavior< Epoch< { membership: CallMembership; - transport: LivekitTransport | undefined; + transport: UnstableLivekitTransport | undefined; }[] > > = scope.behavior( @@ -49,18 +51,21 @@ export const membershipsAndTransports$ = ( const transport = membership.getTransport(); return { membership, - transport: isLivekitTransport(transport) ? transport : undefined, + transport: isUnstableLivekitTransport(transport) + ? transport + : undefined, }; }); }), ), ); - const transports$: Behavior> = scope.behavior( - membershipsWithTransport$.pipe( - mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))), - ), - ); + const transports$: Behavior> = + scope.behavior( + membershipsWithTransport$.pipe( + mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))), + ), + ); return { membershipsWithTransport$, diff --git a/src/state/media/MemberMediaViewModel.ts b/src/state/media/MemberMediaViewModel.ts index 4891809e2..e57dcc636 100644 --- a/src/state/media/MemberMediaViewModel.ts +++ b/src/state/media/MemberMediaViewModel.ts @@ -40,7 +40,7 @@ import { E2eeType } from "../../e2ee/e2eeType"; import { observeInboundRtpStreamStats$ } from "./observeRtpStreamStats"; import { type UserMediaViewModel } from "./UserMediaViewModel"; import { type ScreenShareViewModel } from "./ScreenShareViewModel"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; // TODO: Encryption status is kinda broken and thus unused right now. Remove? export enum EncryptionStatus { @@ -63,7 +63,7 @@ export interface BaseMemberMediaViewModel extends BaseMediaViewModel { * The LiveKit transport on which this member should be publishing. Exposed * for debugging. */ - transport$: Behavior; + transport$: Behavior; /** * Whether there should be a warning that this media is unencrypted. */ @@ -76,7 +76,7 @@ export interface MemberMediaInputs extends BaseMediaViewModel { livekitRoom$: Behavior; audioSource: AudioSource; videoSource: VideoSource; - transport$: Behavior; + transport$: Behavior; encryptionSystem: EncryptionSystem; } diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index b83d5f5ea..b0625a4d7 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -51,7 +51,7 @@ import { type RemoteUserMediaViewModel } from "../state/media/RemoteUserMediaVie import { type UserMediaViewModel } from "../state/media/UserMediaViewModel"; import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel"; import { RingingStatus } from "./RingingStatus"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; interface TileProps { ref?: Ref; @@ -104,7 +104,7 @@ interface UserMediaTileProps extends TileProps { playbackMuted: boolean; waitingForMedia?: boolean; primaryButton?: ReactNode; - transport: LivekitTransport | undefined; + transport: UnstableLivekitTransport | undefined; } /** diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index 7b22764d5..6776317df 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -33,7 +33,7 @@ import { ReactionIndicator } from "../reactions/ReactionIndicator"; import { RTCConnectionStats } from "../RTCConnectionStats"; import videoPlaceholder from "../graphics/video-placeholder.gif"; import { autoVideoFit } from "../utils/videoFit"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; interface Props extends ComponentProps { className?: string; @@ -70,7 +70,7 @@ interface Props extends ComponentProps { /** * The LiveKit transport, mainly for debugging purposes. */ - transport?: LivekitTransport; + transport?: UnstableLivekitTransport; /** * Called whenever the aspect ratio of the video content becomes known or * otherwise changes. diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 46cab2566..344eed00e 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -54,7 +54,7 @@ import { platform } from "../Platform"; import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel"; import { RingingStatus } from "./RingingStatus"; import { useRootElement } from "../RootElementContext"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; interface SpotlightItemBaseProps { ref?: Ref; @@ -75,7 +75,7 @@ interface SpotlightItemBaseProps { interface SpotlightMemberMediaItemBaseProps extends SpotlightItemBaseProps { video: TrackReferenceOrPlaceholder | undefined; unencryptedWarning: boolean; - transport: LivekitTransport | undefined; + transport: UnstableLivekitTransport | undefined; } interface SpotlightUserMediaItemBaseProps extends SpotlightMemberMediaItemBaseProps { diff --git a/src/utils/test.ts b/src/utils/test.ts index 6949c2171..c3efc6b89 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -26,7 +26,7 @@ import { import { CallMembership, type LivekitFocusSelection, - type LivekitTransport, + type UnstableLivekitTransport, type MatrixRTCSession, MatrixRTCSessionEvent, type MatrixRTCSessionEventHandlerMap, @@ -208,7 +208,7 @@ export function mockEmitter(): EmitterMock { }; } -export const exampleTransport: LivekitTransport = { +export const exampleTransport: UnstableLivekitTransport = { type: "livekit", livekit_service_url: "https://lk.example.org", };