Account for server name when identifying transports

This commit is contained in:
Robin
2026-10-09 01:43:25 +02:00
parent 4c96c99c91
commit 033f79975a
20 changed files with 289 additions and 143 deletions
+6
View File
@@ -29,6 +29,10 @@ export interface GetSFUConfigParams {
* The WebSocket URL of the SFU for which we wish to get an access token. * The WebSocket URL of the SFU for which we wish to get an access token.
*/ */
url: string; url: string;
/**
* The name of the homeserver to which the SFU belongs.
*/
serverName: string;
/** /**
* The ID of the Matrix room in which the session takes place. * The ID of the Matrix room in which the session takes place.
*/ */
@@ -47,12 +51,14 @@ export async function getSFUConfig({
client, client,
membership, membership,
url, url,
serverName,
roomId, roomId,
slotId, slotId,
}: GetSFUConfigParams): Promise<SFUConfig> { }: GetSFUConfigParams): Promise<SFUConfig> {
const res = await doNetworkOperationWithRetry(async () => const res = await doNetworkOperationWithRetry(async () =>
client._unstable_getLivekitToken({ client._unstable_getLivekitToken({
url, url,
server_name: serverName,
room_id: roomId, room_id: roomId,
slot_id: slotId, slot_id: slotId,
member_id: membership.memberId, member_id: membership.memberId,
+6
View File
@@ -50,6 +50,10 @@ export interface GetSFUConfigParams {
* The transport for which we wish to get an access token. * The transport for which we wish to get an access token.
*/ */
transport: UnstableLivekitTransport; transport: UnstableLivekitTransport;
/**
* The name of the homeserver to which the {@link transport} belongs.
*/
serverName: string;
/** /**
* The ID of the Matrix room in which the session takes place. * The ID of the Matrix room in which the session takes place.
*/ */
@@ -72,6 +76,7 @@ export async function getSFUConfig({
client, client,
membership, membership,
transport, transport,
serverName,
roomId, roomId,
slotId, slotId,
role, role,
@@ -83,6 +88,7 @@ export async function getSFUConfig({
client, client,
membership, membership,
url: transport.url, url: transport.url,
serverName,
roomId, roomId,
slotId, slotId,
}); });
+11
View File
@@ -5,6 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
/**
* Data identifying a transport and the homeserver to which it belongs, so
* consumers know how to reach it.
*/
export interface TransportLocator {
transport: UnstableLivekitTransport;
serverName: string;
}
/** /**
* Configuration and access tokens provided by the SFU on successful authentication. * Configuration and access tokens provided by the SFU on successful authentication.
*/ */
+1
View File
@@ -526,6 +526,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
client, client,
membership: { userId, deviceId, memberId: "" }, membership: { userId, deviceId, memberId: "" },
transport, transport,
serverName: client.getDomain()!,
roomId, roomId,
slotId: "org.example.fake-slot#room", slotId: "org.example.fake-slot#room",
role: "publisher", role: "publisher",
+2 -1
View File
@@ -163,6 +163,7 @@ import {
type RingingMediaViewModel, type RingingMediaViewModel,
} from "../media/RingingMediaViewModel.ts"; } from "../media/RingingMediaViewModel.ts";
import { type GridTileViewModel } from "../TileViewModel.ts"; import { type GridTileViewModel } from "../TileViewModel.ts";
import { type TransportLocator } from "../../livekit/auth";
//TODO //TODO
// Larger rename // Larger rename
@@ -215,7 +216,7 @@ export interface CallViewModelOptions {
*/ */
windowSize$: Behavior<{ width: number; height: number }>; windowSize$: Behavior<{ width: number; height: number }>;
/** Optional value overriding the local transport, for testing purposes. */ /** Optional value overriding the local transport, for testing purposes. */
localTransport?: UnstableLivekitTransport; localTransport?: TransportLocator;
/** Optional value overriding the connection factory, for testing purposes. */ /** Optional value overriding the connection factory, for testing purposes. */
connectionFactory?: ConnectionFactory; connectionFactory?: ConnectionFactory;
/** The version & compatibility mode of MatrixRTC that we should use. */ /** The version & compatibility mode of MatrixRTC that we should use. */
@@ -30,7 +30,7 @@ import {
type CallViewModelOptions, type CallViewModelOptions,
} from "./CallViewModel"; } from "./CallViewModel";
import { import {
exampleTransport, exampleTransportLocator,
mockConfig, mockConfig,
MockConnection, MockConnection,
mockLivekitRoom, mockLivekitRoom,
@@ -208,12 +208,13 @@ export function withCallViewModel(mode: MatrixRTCMode) {
livekitRoomFactory, livekitRoomFactory,
connectionState$, connectionState$,
windowSize$, windowSize$,
localTransport: exampleTransport, localTransport: exampleTransportLocator,
connectionFactory: { connectionFactory: {
createConnection( createConnection(
scope, scope,
role, role,
transport, transport,
serverName,
ownMembershipIdentity, ownMembershipIdentity,
logger, logger,
) { ) {
@@ -222,6 +223,7 @@ export function withCallViewModel(mode: MatrixRTCMode) {
scope, scope,
role, role,
transport, transport,
serverName,
ownMembershipIdentity, ownMembershipIdentity,
client: room.client, client: room.client,
roomId: room.roomId, roomId: room.roomId,
@@ -305,7 +305,7 @@ describe("LocalMembership", () => {
leave: vi.fn(), leave: vi.fn(),
}, },
connectionManager: mockConnectionManager, connectionManager: mockConnectionManager,
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
const expextedLog = const expextedLog =
"'not connected yet' while updating the call intent (this is expected on startup)"; "'not connected yet' while updating the call intent (this is expected on startup)";
@@ -321,7 +321,12 @@ describe("LocalMembership", () => {
type: "livekit", type: "livekit",
url: "https://sfu.example.org", url: "https://sfu.example.org",
livekit_service_url: "https://jwt.example.org", livekit_service_url: "https://jwt.example.org",
} as UnstableLivekitTransport; };
const mockTransportLocator: livekitAuth.TransportLocator = {
transport: mockTransport,
serverName: "example.org",
};
const connectionTransportAConnected = { const connectionTransportAConnected = {
livekitRoom: mockLivekitRoom({ livekitRoom: mockLivekitRoom({
@@ -375,7 +380,7 @@ describe("LocalMembership", () => {
), ),
}, },
joinMatrixRTC, joinMatrixRTC,
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
delayId$, delayId$,
}); });
@@ -452,7 +457,7 @@ describe("LocalMembership", () => {
connectionManager: { connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)), connectionManagerData$: constant(new Epoch(connectionManagerData)),
}, },
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
await flushPromises(); await flushPromises();
expect(publisherFactory).toHaveBeenCalledOnce(); expect(publisherFactory).toHaveBeenCalledOnce();
@@ -474,7 +479,7 @@ describe("LocalMembership", () => {
it("tracks livekit state correctly", async () => { it("tracks livekit state correctly", async () => {
const scope = new ObservableScope(); const scope = new ObservableScope();
const connectionManagerData = new ConnectionManagerData(); const connectionManagerData = new ConnectionManagerData();
const localTransport$ = new Subject<UnstableLivekitTransport>(); const localTransport$ = new Subject<livekitAuth.TransportLocator>();
const connectionManagerData$ = new BehaviorSubject( const connectionManagerData$ = new BehaviorSubject(
new Epoch(connectionManagerData), new Epoch(connectionManagerData),
@@ -522,7 +527,7 @@ describe("LocalMembership", () => {
expect(localMembership.localMemberState$.value).toStrictEqual( expect(localMembership.localMemberState$.value).toStrictEqual(
TransportState.Waiting, TransportState.Waiting,
); );
localTransport$.next(mockTransport); localTransport$.next(mockTransportLocator);
await flushPromises(); await flushPromises();
expect(localMembership.localMemberState$.value).toStrictEqual({ expect(localMembership.localMemberState$.value).toStrictEqual({
matrix: RTCMemberStatus.Connected, matrix: RTCMemberStatus.Connected,
@@ -547,7 +552,7 @@ describe("LocalMembership", () => {
}); });
( (
connectionManagerData2.getConnectionForTransport(mockTransport)! connectionManagerData2.getConnectionForTransport(mockTransportLocator)!
.state$ as BehaviorSubject<ConnectionState> .state$ as BehaviorSubject<ConnectionState>
).next(ConnectionState.LivekitConnected); ).next(ConnectionState.LivekitConnected);
expect(localMembership.localMemberState$.value).toStrictEqual({ expect(localMembership.localMemberState$.value).toStrictEqual({
@@ -650,7 +655,7 @@ describe("LocalMembership", () => {
connectionManager: { connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)), connectionManagerData$: constant(new Epoch(connectionManagerData)),
}, },
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
await flushPromises(); await flushPromises();
@@ -687,7 +692,7 @@ describe("LocalMembership", () => {
connectionManager: { connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)), connectionManagerData$: constant(new Epoch(connectionManagerData)),
}, },
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
await flushPromises(); await flushPromises();
@@ -735,7 +740,7 @@ describe("LocalMembership", () => {
connectionManager: { connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)), connectionManagerData$: constant(new Epoch(connectionManagerData)),
}, },
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
await flushPromises(); await flushPromises();
@@ -776,7 +781,7 @@ describe("LocalMembership", () => {
connectionManager: { connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)), connectionManagerData$: constant(new Epoch(connectionManagerData)),
}, },
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
await flushPromises(); await flushPromises();
@@ -842,7 +847,7 @@ describe("LocalMembership", () => {
connectionManager: { connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)), connectionManagerData$: constant(new Epoch(connectionManagerData)),
}, },
localTransport$: constant(mockTransport), localTransport$: constant(mockTransportLocator),
}); });
return { scope, localMembership }; return { scope, localMembership };
}; };
@@ -82,6 +82,7 @@ import {
type ClientDelegationParts, type ClientDelegationParts,
delegateDelayedLeave, delegateDelayedLeave,
type ClientOpenIDParts, type ClientOpenIDParts,
type TransportLocator,
} from "../../../livekit/auth"; } from "../../../livekit/auth";
export enum TransportState { export enum TransportState {
@@ -151,7 +152,7 @@ interface Props {
homeserverConnected: HomeserverConnected; homeserverConnected: HomeserverConnected;
roomId: string; roomId: string;
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
localTransport$: Observable<UnstableLivekitTransport>; localTransport$: Observable<TransportLocator>;
client: ClientDelegationParts & ClientOpenIDParts; client: ClientDelegationParts & ClientOpenIDParts;
matrixRTCSession: Pick< matrixRTCSession: Pick<
MatrixRTCSession, MatrixRTCSession,
@@ -310,7 +311,7 @@ export const createLocalMembership$ = ({
// to whether delayed event delegation is supported // to whether delayed event delegation is supported
const joinParams$ = scope.behavior( const joinParams$ = scope.behavior(
localTransport$.pipe( localTransport$.pipe(
switchMap(async (transport) => { switchMap(async ({ transport }) => {
const transportSupportsDelegation = const transportSupportsDelegation =
"livekit_service_url" in transport && "livekit_service_url" in transport &&
(await checkDelegationSupport( (await checkDelegationSupport(
@@ -5,7 +5,6 @@ SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { type MatrixClient } from "matrix-js-sdk"; import { type MatrixClient } from "matrix-js-sdk";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
@@ -13,6 +12,7 @@ import { Config } from "../../../config/Config.ts";
import { MatrixRTCTransportMissingError } from "../../../utils/errors.ts"; import { MatrixRTCTransportMissingError } from "../../../utils/errors.ts";
import { customTransport as customTransportSetting } from "../../../settings/settings.ts"; import { customTransport as customTransportSetting } from "../../../settings/settings.ts";
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
import { type TransportLocator } from "../../../livekit/auth/types.ts";
/** /**
* Determines the transport to advertise in our MatrixRTC membership and publish * Determines the transport to advertise in our MatrixRTC membership and publish
@@ -20,7 +20,10 @@ import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
*/ */
export async function getLocalTransport( export async function getLocalTransport(
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports">, client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports">,
): Promise<UnstableLivekitTransport> { ): Promise<TransportLocator> {
const serverName = client.getDomain();
if (serverName === null) throw new Error("No server name");
const discovery = new RtcTransportAutoDiscovery({ const discovery = new RtcTransportAutoDiscovery({
client, client,
resolvedConfig: Config.get(), resolvedConfig: Config.get(),
@@ -32,11 +35,10 @@ export async function getLocalTransport(
customTransportSetting.value$.value ?? customTransportSetting.value$.value ??
(await discovery.discoverPreferredTransport()); (await discovery.discoverPreferredTransport());
if (transport === null) if (transport === null) throw new MatrixRTCTransportMissingError(serverName);
throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
// TODO: Since this module no longer maps auth errors to fatal user-facing // TODO: Since this module no longer maps auth errors to fatal user-facing
// errors (as it doesn't even perform auth), that needs to happen somewhere // errors (as it doesn't even perform auth), that needs to happen somewhere
// else. // else.
return transport; return { transport, serverName };
} }
@@ -121,11 +121,12 @@ function setupTest(): void {
function setupRemoteConnection(): Connection { function setupRemoteConnection(): Connection {
const opts: ConnectionOpts = { const opts: ConnectionOpts = {
client: client, client,
roomId: ROOM_ID, roomId: ROOM_ID,
slotId: SLOT_ID, slotId: SLOT_ID,
role: "subscriber", role: "subscriber",
transport: transport, transport,
serverName: "m.localhost",
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
@@ -169,11 +170,12 @@ describe("Start connection states", () => {
setupTest(); setupTest();
const opts: ConnectionOpts = { const opts: ConnectionOpts = {
client: client, client,
roomId: ROOM_ID, roomId: ROOM_ID,
slotId: SLOT_ID, slotId: SLOT_ID,
role: "subscriber", role: "subscriber",
transport: transport, transport,
serverName: "m.localhost",
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
@@ -188,11 +190,12 @@ describe("Start connection states", () => {
vi.useFakeTimers(); vi.useFakeTimers();
const opts: ConnectionOpts = { const opts: ConnectionOpts = {
client: client, client,
roomId: ROOM_ID, roomId: ROOM_ID,
slotId: SLOT_ID, slotId: SLOT_ID,
role: "subscriber", role: "subscriber",
transport: transport, transport,
serverName: "m.localhost",
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
@@ -240,11 +243,12 @@ describe("Start connection states", () => {
vi.useFakeTimers(); vi.useFakeTimers();
const opts: ConnectionOpts = { const opts: ConnectionOpts = {
client: client, client,
roomId: ROOM_ID, roomId: ROOM_ID,
slotId: SLOT_ID, slotId: SLOT_ID,
role: "subscriber", role: "subscriber",
transport: transport, transport,
serverName: "m.localhost",
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
@@ -301,11 +305,12 @@ describe("Start connection states", () => {
vi.useFakeTimers(); vi.useFakeTimers();
const opts: ConnectionOpts = { const opts: ConnectionOpts = {
client: client, client,
roomId: ROOM_ID, roomId: ROOM_ID,
slotId: SLOT_ID, slotId: SLOT_ID,
role: "subscriber", role: "subscriber",
transport: transport, transport,
serverName: "m.localhost",
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
@@ -545,7 +550,8 @@ describe("remote track logging", () => {
roomId: ROOM_ID, roomId: ROOM_ID,
slotId: SLOT_ID, slotId: SLOT_ID,
role: "subscriber", role: "subscriber",
transport: transport, transport,
serverName: "m.localhost",
scope: testScope, scope: testScope,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
livekitRoomFactory: () => fakeLivekitRoom, livekitRoomFactory: () => fakeLivekitRoom,
@@ -51,6 +51,8 @@ export interface ConnectionOpts {
role: "publisher" | "subscriber"; role: "publisher" | "subscriber";
/** The media transport to connect to. */ /** The media transport to connect to. */
transport: UnstableLivekitTransport; transport: UnstableLivekitTransport;
/** The name of the homeserver to which the {@link transport} belongs. */
serverName: string;
/** The Matrix client to use for OpenID and SFU config requests. */ /** The Matrix client to use for OpenID and SFU config requests. */
client: ClientGetTokenParts & ClientOpenIDParts; client: ClientGetTokenParts & ClientOpenIDParts;
/** The ID of the Matrix room in which the session takes place. */ /** The ID of the Matrix room in which the session takes place. */
@@ -114,6 +116,11 @@ export class Connection {
*/ */
public readonly transport: UnstableLivekitTransport; public readonly transport: UnstableLivekitTransport;
/**
* The name of the homeserver to which the {@link transport} belongs.
*/
public readonly serverName: string;
public readonly livekitRoom: LivekitRoom; public readonly livekitRoom: LivekitRoom;
private scope: ObservableScope; private scope: ObservableScope;
@@ -162,12 +169,13 @@ export class Connection {
`[Connection ${JSON.stringify(opts.transport)}]`, `[Connection ${JSON.stringify(opts.transport)}]`,
); );
this.logger.debug(`constructor called`); this.logger.debug(`constructor called`);
const { role, transport, client, scope } = opts; const { role, transport, serverName, client, scope } = opts;
this.scope = scope; this.scope = scope;
this.livekitRoom = opts.livekitRoomFactory(); this.livekitRoom = opts.livekitRoomFactory();
this.role = role; this.role = role;
this.transport = transport; this.transport = transport;
this.serverName = serverName;
this.client = client; this.client = client;
this.remoteParticipants$ = scope.behavior( this.remoteParticipants$ = scope.behavior(
@@ -401,6 +409,7 @@ export class Connection {
membership: this.ownMembershipIdentity, membership: this.ownMembershipIdentity,
role: this.role, role: this.role,
transport: this.transport, transport: this.transport,
serverName: this.serverName,
roomId: this.roomId, roomId: this.roomId,
slotId: this.slotId, slotId: this.slotId,
logger: this.logger, logger: this.logger,
@@ -46,6 +46,7 @@ export interface ConnectionFactory {
scope: ObservableScope, scope: ObservableScope,
role: "publisher" | "subscriber", role: "publisher" | "subscriber",
transport: UnstableLivekitTransport, transport: UnstableLivekitTransport,
serverName: string,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
logger: Logger, logger: Logger,
): Connection; ): Connection;
@@ -98,6 +99,7 @@ export class ECConnectionFactory implements ConnectionFactory {
* *
* @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 transport The transport to use for this connection.
* @param serverName The name of the homeserver to which the {@link transport} belongs.
* @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 sfuConfig optional config in case we already have a token for this connection. * @param sfuConfig optional config in case we already have a token for this connection.
@@ -107,6 +109,7 @@ export class ECConnectionFactory implements ConnectionFactory {
scope: ObservableScope, scope: ObservableScope,
role: "publisher" | "subscriber", role: "publisher" | "subscriber",
transport: UnstableLivekitTransport, transport: UnstableLivekitTransport,
serverName: string,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,
logger: Logger, logger: Logger,
): Connection { ): Connection {
@@ -116,6 +119,7 @@ export class ECConnectionFactory implements ConnectionFactory {
slotId: this.slotId, slotId: this.slotId,
role, role,
transport, transport,
serverName,
client: this.client, client: this.client,
scope: scope, scope: scope,
livekitRoomFactory: this.livekitRoomFactory, livekitRoomFactory: this.livekitRoomFactory,
@@ -15,23 +15,30 @@ import { Epoch, mapEpoch, ObservableScope } from "../../ObservableScope.ts";
import { import {
createConnectionManager$, createConnectionManager$,
ConnectionManagerData, ConnectionManagerData,
areUnstableLivekitTransportsEqual,
} from "./ConnectionManager.ts"; } from "./ConnectionManager.ts";
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts"; import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts";
import { areUnstableLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
import { type Behavior } from "../../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
import { type TransportLocator } from "../../../livekit/auth";
// Some test constants // Some test constants
const TRANSPORT_1: UnstableLivekitTransport = { const TRANSPORT_1: TransportLocator = {
type: "livekit", transport: {
livekit_service_url: "https://lk.example.org", type: "livekit",
livekit_service_url: "https://lk.example.org",
},
serverName: "example.org",
}; };
const TRANSPORT_2: UnstableLivekitTransport = { const TRANSPORT_2: TransportLocator = {
type: "livekit", transport: {
livekit_service_url: "https://lk.sample.com", type: "livekit",
livekit_service_url: "https://lk.sample.com",
},
serverName: "sample.com",
}; };
let fakeConnectionFactory: ConnectionFactory; let fakeConnectionFactory: ConnectionFactory;
@@ -94,13 +101,19 @@ describe("connections$ stream", () => {
).toHaveBeenCalledTimes(2); ).toHaveBeenCalledTimes(2);
const conn1 = connections.find((c) => const conn1 = connections.find((c) =>
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_1), areUnstableLivekitTransportsEqual(
c.transport,
TRANSPORT_1.transport,
),
); );
expect(conn1).toBeDefined(); expect(conn1).toBeDefined();
expect(conn1!.start).toHaveBeenCalled(); expect(conn1!.start).toHaveBeenCalled();
const conn2 = connections.find((c) => const conn2 = connections.find((c) =>
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2), areUnstableLivekitTransportsEqual(
c.transport,
TRANSPORT_2.transport,
),
); );
expect(conn2).toBeDefined(); expect(conn2).toBeDefined();
expect(conn2!.start).toHaveBeenCalled(); expect(conn2!.start).toHaveBeenCalled();
@@ -141,12 +154,18 @@ describe("connections$ stream", () => {
).toHaveBeenCalledTimes(2); ).toHaveBeenCalledTimes(2);
const conn2 = connections.find((c) => const conn2 = connections.find((c) =>
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2), areUnstableLivekitTransportsEqual(
c.transport,
TRANSPORT_2.transport,
),
); );
expect(conn2).toBeDefined(); expect(conn2).toBeDefined();
const conn1 = connections.find((c) => const conn1 = connections.find((c) =>
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_1), areUnstableLivekitTransportsEqual(
c.transport,
TRANSPORT_1.transport,
),
); );
expect(conn1).toBeDefined(); expect(conn1).toBeDefined();
expect(conn1!.start).toHaveBeenCalledOnce(); expect(conn1!.start).toHaveBeenCalledOnce();
@@ -187,7 +206,10 @@ describe("connections$ stream", () => {
expect(connections.length).toBe(1); expect(connections.length).toBe(1);
// The second connection should have been stopped has it is no longer needed. // The second connection should have been stopped has it is no longer needed.
const connection2 = allCreatedConnections.find((c) => const connection2 = allCreatedConnections.find((c) =>
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2), areUnstableLivekitTransportsEqual(
c.transport,
TRANSPORT_2.transport,
),
); );
expect(connection2).toBeDefined(); expect(connection2).toBeDefined();
expect(connection2!.stop).toHaveBeenCalled(); expect(connection2!.stop).toHaveBeenCalled();
@@ -279,7 +301,7 @@ describe("connectionManagerData$ stream", () => {
// Setup the fake participants streams behavior // Setup the fake participants streams behavior
// ============================== // ==============================
fakeRemoteParticipantsStreams.set( fakeRemoteParticipantsStreams.set(
keyForTransport(TRANSPORT_1), keyForTransport(TRANSPORT_1.transport),
behavior("oa-b", { behavior("oa-b", {
o: [], o: [],
a: [{ identity: "user1A" } as RemoteParticipant], a: [{ identity: "user1A" } as RemoteParticipant],
@@ -291,7 +313,7 @@ describe("connectionManagerData$ stream", () => {
); );
fakeRemoteParticipantsStreams.set( fakeRemoteParticipantsStreams.set(
keyForTransport(TRANSPORT_2), keyForTransport(TRANSPORT_2.transport),
behavior("o-a", { behavior("o-a", {
o: [], o: [],
a: [{ identity: "user2A" } as RemoteParticipant], a: [{ identity: "user2A" } as RemoteParticipant],
@@ -20,8 +20,8 @@ import {
type ObservableScope, type ObservableScope,
} from "../../ObservableScope.ts"; } from "../../ObservableScope.ts";
import { generateItemsWithEpoch } from "../../../utils/observable.ts"; import { generateItemsWithEpoch } from "../../../utils/observable.ts";
import { areUnstableLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";
import { type TransportLocator } from "../../../livekit/auth/types.ts";
export class ConnectionManagerData { export class ConnectionManagerData {
private readonly store: Map< private readonly store: Map<
@@ -32,7 +32,7 @@ export class ConnectionManagerData {
public constructor(private readonly logger?: Logger) {} public constructor(private readonly logger?: Logger) {}
public add(connection: Connection, participants: RemoteParticipant[]): void { public add(connection: Connection, participants: RemoteParticipant[]): void {
const key = this.getKey(connection.transport); const key = this.getKey(connection);
const existing = this.store.get(key); const existing = this.store.get(key);
if (!existing) { if (!existing) {
this.store.set(key, { connection, participants }); this.store.set(key, { connection, participants });
@@ -46,7 +46,7 @@ export class ConnectionManagerData {
} }
} }
private getKey(transport: UnstableLivekitTransport): string { private getKey({ transport, serverName }: TransportLocator): string {
// This is enough as a key because the ConnectionManager is already scoped by room. // This is enough as a key because the ConnectionManager is already scoped by room.
// We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`. // We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`.
return JSON.stringify([ return JSON.stringify([
@@ -54,6 +54,7 @@ export class ConnectionManagerData {
"livekit_service_url" in transport "livekit_service_url" in transport
? transport.livekit_service_url ? transport.livekit_service_url
: undefined, : undefined,
serverName,
]); ]);
} }
@@ -62,13 +63,13 @@ export class ConnectionManagerData {
} }
public getConnectionForTransport( public getConnectionForTransport(
transport: UnstableLivekitTransport, transport: TransportLocator,
): Connection | null { ): Connection | null {
return this.store.get(this.getKey(transport))?.connection ?? null; return this.store.get(this.getKey(transport))?.connection ?? null;
} }
public getParticipantsForTransport( public getParticipantsForTransport(
transport: UnstableLivekitTransport, transport: TransportLocator,
): RemoteParticipant[] { ): RemoteParticipant[] {
const key = this.getKey(transport); const key = this.getKey(transport);
const existing = this.store.get(key); const existing = this.store.get(key);
@@ -79,11 +80,44 @@ export class ConnectionManagerData {
} }
} }
/**
* Unpacks an {@link UnstableLivekitTransport} into its constituent `url` and
* `serviceUrl`, discarding the type information that at least one of them must
* be defined.
*/
function unpackTransport(transport: UnstableLivekitTransport): {
url: string | undefined;
serviceUrl: string | undefined;
} {
return {
url: "url" in transport ? transport.url : undefined,
serviceUrl:
"livekit_service_url" in transport
? transport.livekit_service_url
: undefined,
};
}
/**
* Constructs an {@link UnstableLivekitTransport} from its constituent `url` and
* `serviceUrl`. This assumes that at least one of the two are defined.
*/
function repackTransport(
url: string | undefined,
serviceUrl: string | undefined,
): UnstableLivekitTransport {
return {
type: "livekit",
...(url === undefined ? {} : { url }),
...(serviceUrl === undefined ? {} : { livekit_service_url: serviceUrl }),
} as UnstableLivekitTransport;
}
interface Props { interface Props {
scope: ObservableScope; scope: ObservableScope;
connectionFactory: ConnectionFactory; connectionFactory: ConnectionFactory;
localTransport: UnstableLivekitTransport; localTransport: TransportLocator;
remoteTransports$: Behavior<Epoch<UnstableLivekitTransport[]>>; remoteTransports$: Behavior<Epoch<TransportLocator[]>>;
logger: Logger; logger: Logger;
ownMembershipIdentity: CallMembershipIdentityParts; ownMembershipIdentity: CallMembershipIdentityParts;
@@ -129,11 +163,11 @@ export function createConnectionManager$({
* All transports currently managed by the ConnectionManager. This list does * All transports currently managed by the ConnectionManager. This list does
* not include duplicate transports. * not include duplicate transports.
*/ */
const localAndRemoteTransports$: Behavior<Epoch<UnstableLivekitTransport[]>> = const localAndRemoteTransports$: Behavior<Epoch<TransportLocator[]>> =
scope.behavior( scope.behavior(
remoteTransports$.pipe( remoteTransports$.pipe(
mapEpoch((remoteTransports) => mapEpoch((remoteTransports) =>
removeDuplicateTransports([localTransport, ...remoteTransports]), removeDuplicateLocators([localTransport, ...remoteTransports]),
), ),
tap((transports) => tap((transports) =>
logger.debug("localAndRemoteTransports$ = ", transports), logger.debug("localAndRemoteTransports$ = ", transports),
@@ -149,30 +183,32 @@ export function createConnectionManager$({
generateItemsWithEpoch( generateItemsWithEpoch(
"ConnectionManager connections$", "ConnectionManager connections$",
function* (transports) { function* (transports) {
for (const transport of transports) { for (const { transport, serverName } of transports) {
const role = const role =
transport === localTransport serverName === localTransport.serverName &&
areUnstableLivekitTransportsEqual(
transport,
localTransport.transport,
)
? ("publisher" as const) ? ("publisher" as const)
: ("subscriber" as const); : ("subscriber" as const);
const url = "url" in transport ? transport.url : undefined; // In order to maintain a consistent set of map keys, we must
const serviceUrl = // temporarily downgrade `transport` to a weaker type
"livekit_service_url" in transport const { url, serviceUrl } = unpackTransport(transport);
? transport.livekit_service_url yield {
: undefined; keys: [role, url, serviceUrl, serverName],
yield { keys: [role, url, serviceUrl], data: undefined }; data: undefined,
};
} }
}, },
(scope, _data$, role, url, serviceUrl) => { (scope, _data$, role, url, serviceUrl, serverName) => {
const connection = connectionFactory.createConnection( const connection = connectionFactory.createConnection(
scope, scope,
role, role,
{ // Pack the transport keys back into a stronger
type: "livekit", // `UnstableLivekitTransport` type
...(url === undefined ? {} : { url }), repackTransport(url, serviceUrl),
...(serviceUrl === undefined serverName,
? {}
: { livekit_service_url: serviceUrl }),
} as UnstableLivekitTransport,
ownMembershipIdentity, ownMembershipIdentity,
logger, logger,
); );
@@ -230,12 +266,65 @@ export function createConnectionManager$({
return { connectionManagerData$ }; return { connectionManagerData$ };
} }
function removeDuplicateTransports<T extends UnstableLivekitTransport>( // TODO add this to the JS-SDK
transports: T[], export function areUnstableLivekitTransportsEqual<
): T[] { T extends UnstableLivekitTransport,
return transports.reduce((acc, transport) => { >(t1: T | null, t2: T | null): boolean {
if (!acc.some((t) => areUnstableLivekitTransportsEqual(t, transport))) if (t1 && t2) {
acc.push(transport); if ("url" in t1 !== "url" in t2) return false;
return acc; if ("livekit_service_url" in t1 !== "livekit_service_url" in t2)
}, [] as T[]); return false;
if (
"url" in t1 &&
t1.url !== (t2 as UnstableLivekitTransport & { url: string }).url
)
return false;
if (
"livekit_service_url" in t1 &&
t1.livekit_service_url !==
(t2 as UnstableLivekitTransport & { livekit_service_url: string })
.livekit_service_url
)
return false;
return true;
}
return !t1 && !t2;
}
function removeDuplicateLocators(
locators: TransportLocator[],
): TransportLocator[] {
// Set tracking which locators we have added to `deduped` so far
// url -> serviceUrl -> serverNames
const byUrl = new Map<
string | undefined,
Map<string | undefined, Set<string>>
>();
const deduped: TransportLocator[] = [];
for (const locator of locators) {
const { transport, serverName } = locator;
const url = "url" in transport ? transport.url : undefined;
const serviceUrl =
"livekit_service_url" in transport
? transport.livekit_service_url
: undefined;
let byServiceUrl = byUrl.get(url);
if (byServiceUrl === undefined) {
byServiceUrl = new Map();
byUrl.set(url, byServiceUrl);
}
let serverNames = byServiceUrl.get(serviceUrl);
if (serverNames === undefined) {
serverNames = new Set();
byServiceUrl.set(serviceUrl, serverNames);
}
if (!serverNames.has(serverName)) {
serverNames.add(serverName);
deduped.push(locator);
}
}
return deduped;
} }
@@ -97,6 +97,7 @@ describe("ECConnectionFactory - Audio inputs options", () => {
testScope, testScope,
"subscriber", "subscriber",
exampleTransport, exampleTransport,
"example.org",
ownMemberMock, ownMemberMock,
logger, logger,
); );
@@ -147,6 +148,7 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
testScope, testScope,
"subscriber", "subscriber",
exampleTransport, exampleTransport,
"example.org",
ownMemberMock, ownMemberMock,
logger, logger,
); );
@@ -184,6 +186,7 @@ describe("ECConnectionFactory - Camera quality settings", () => {
testScope, testScope,
"subscriber", "subscriber",
exampleTransport, exampleTransport,
"example.org",
ownMemberMock, ownMemberMock,
logger, logger,
); );
@@ -223,6 +226,7 @@ describe("ECConnectionFactory - Camera quality settings", () => {
testScope, testScope,
"subscriber", "subscriber",
exampleTransport, exampleTransport,
"example.org",
ownMemberMock, ownMemberMock,
logger, logger,
); );
@@ -263,6 +267,7 @@ describe("ECConnectionFactory - Camera quality settings", () => {
testScope, testScope,
"subscriber", "subscriber",
exampleTransport, exampleTransport,
"example.org",
ownMemberMock, ownMemberMock,
logger, logger,
); );
@@ -18,12 +18,7 @@ import {
type RemoteMatrixLivekitMember, type RemoteMatrixLivekitMember,
createRemoteMatrixLivekitMembers$, createRemoteMatrixLivekitMembers$,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { import { Epoch, ObservableScope, trackEpoch } from "../../ObservableScope.ts";
Epoch,
mapEpoch,
ObservableScope,
trackEpoch,
} from "../../ObservableScope.ts";
import { ConnectionManagerData } from "./ConnectionManager.ts"; import { ConnectionManagerData } from "./ConnectionManager.ts";
import { import {
flushPromises, flushPromises,
@@ -33,6 +28,8 @@ import {
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
import { constant } from "../../Behavior.ts"; import { constant } from "../../Behavior.ts";
import { localRtcMember } from "../../../utils/test-fixtures.ts"; import { localRtcMember } from "../../../utils/test-fixtures.ts";
import { type TransportLocator } from "../../../livekit/auth";
import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
let testScope: ObservableScope; let testScope: ObservableScope;
@@ -117,21 +114,15 @@ test("should signal participant not yet connected to livekit", async () => {
function createEpochedMemberships$(m$: Observable<CallMembership[]>): { function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
memberships$: Observable<Epoch<CallMembership[]>>; memberships$: Observable<Epoch<CallMembership[]>>;
membershipsWithTransport$: Observable< membershipsWithTransport$: Observable<
Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]> Epoch<
{ membership: CallMembership; transport: TransportLocator | undefined }[]
>
>; >;
} { } {
const memberships$ = m$.pipe(trackEpoch()); const memberships$ = testScope.behavior(m$.pipe(trackEpoch()));
const membershipsWithTransport$ = memberships$.pipe( const { membershipsWithTransport$ } = membershipsAndTransports$(
mapEpoch((members) => { testScope,
return members.map((m) => { memberships$,
const tr = m.getTransport();
return {
membership: m,
transport:
tr?.type === "livekit" ? (tr as UnstableLivekitTransport) : undefined,
};
});
}),
); );
return { return {
memberships$, memberships$,
@@ -6,10 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; import { type LocalParticipant, type RemoteParticipant } from "livekit-client";
import { import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
type CallMembership,
type UnstableLivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc";
import { combineLatest, filter, map } from "rxjs"; import { combineLatest, filter, map } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
@@ -18,6 +15,7 @@ import { type IConnectionManager } from "./ConnectionManager";
import { Epoch, type ObservableScope } from "../../ObservableScope"; import { Epoch, type ObservableScope } from "../../ObservableScope";
import { type Connection } from "./Connection"; import { type Connection } from "./Connection";
import { generateItemsWithEpoch } from "../../../utils/observable"; import { generateItemsWithEpoch } from "../../../utils/observable";
import { type TransportLocator } from "../../../livekit/auth";
interface LocalTaggedParticipant { interface LocalTaggedParticipant {
type: "local"; type: "local";
@@ -61,7 +59,7 @@ interface Props {
scope: ObservableScope; scope: ObservableScope;
membershipsWithTransport$: Behavior< membershipsWithTransport$: Behavior<
Epoch< Epoch<
{ membership: CallMembership; transport?: UnstableLivekitTransport }[] { membership: CallMembership; transport: TransportLocator | undefined }[]
> >
>; >;
connectionManager: IConnectionManager; connectionManager: IConnectionManager;
@@ -165,28 +163,3 @@ export function createRemoteMatrixLivekitMembers$({
} }
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$) // TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
// TODO add this to the JS-SDK
export function areUnstableLivekitTransportsEqual<
T extends UnstableLivekitTransport,
>(t1: T | null, t2: T | null): boolean {
if (t1 && t2) {
if ("url" in t1 !== "url" in t2) return false;
if ("livekit_service_url" in t1 !== "livekit_service_url" in t2)
return false;
if (
"url" in t1 &&
t1.url !== (t2 as UnstableLivekitTransport & { url: string }).url
)
return false;
if (
"livekit_service_url" in t1 &&
t1.livekit_service_url !==
(t2 as UnstableLivekitTransport & { livekit_service_url: string })
.livekit_service_url
)
return false;
return true;
}
return !t1 && !t2;
}
@@ -24,7 +24,7 @@ import {
type ClientOpenIDParts, type ClientOpenIDParts,
} from "../../../livekit/auth"; } from "../../../livekit/auth";
import { import {
exampleTransport, exampleTransportLocator,
mockMediaDevices, mockMediaDevices,
mockRtcMembership, mockRtcMembership,
ownMemberMock, ownMemberMock,
@@ -32,11 +32,13 @@ import {
} from "../../../utils/test.ts"; } from "../../../utils/test.ts";
import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { import {
areUnstableLivekitTransportsEqual,
createRemoteMatrixLivekitMembers$, createRemoteMatrixLivekitMembers$,
type RemoteMatrixLivekitMember, type RemoteMatrixLivekitMember,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { createConnectionManager$ } from "./ConnectionManager.ts"; import {
areUnstableLivekitTransportsEqual,
createConnectionManager$,
} from "./ConnectionManager.ts";
import { membershipsAndTransports$ } from "../../SessionBehaviors.ts"; import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts"; import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts";
@@ -129,7 +131,7 @@ test("bob, carl, then bob joining no tracks yet", () => {
const connectionManager = createConnectionManager$({ const connectionManager = createConnectionManager$({
scope: testScope, scope: testScope,
connectionFactory: ecConnectionFactory, connectionFactory: ecConnectionFactory,
localTransport: exampleTransport, localTransport: exampleTransportLocator,
remoteTransports$: membershipsAndTransports.transports$, remoteTransports$: membershipsAndTransports.transports$,
logger: logger, logger: logger,
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
+13 -9
View File
@@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details.
import { import {
type CallMembership, type CallMembership,
type UnstableLivekitTransport,
type MatrixRTCSession, type MatrixRTCSession,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
isUnstableLivekitTransport, isUnstableLivekitTransport,
@@ -21,6 +20,7 @@ import {
type ObservableScope, type ObservableScope,
} from "./ObservableScope"; } from "./ObservableScope";
import { type Behavior } from "./Behavior"; import { type Behavior } from "./Behavior";
import { type TransportLocator } from "../livekit/auth";
/** /**
* Tracks the transports used by ourselves, plus all other MatrixRTC session * Tracks the transports used by ourselves, plus all other MatrixRTC session
@@ -31,16 +31,15 @@ export const membershipsAndTransports$ = (
memberships$: Behavior<Epoch<CallMembership[]>>, memberships$: Behavior<Epoch<CallMembership[]>>,
): { ): {
membershipsWithTransport$: Behavior< membershipsWithTransport$: Behavior<
Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]> Epoch<
{ membership: CallMembership; transport: TransportLocator | undefined }[]
>
>; >;
transports$: Behavior<Epoch<UnstableLivekitTransport[]>>; transports$: Behavior<Epoch<TransportLocator[]>>;
} => { } => {
const membershipsWithTransport$: Behavior< const membershipsWithTransport$: Behavior<
Epoch< Epoch<
{ { membership: CallMembership; transport: TransportLocator | undefined }[]
membership: CallMembership;
transport: UnstableLivekitTransport | undefined;
}[]
> >
> = scope.behavior( > = scope.behavior(
memberships$.pipe( memberships$.pipe(
@@ -49,14 +48,19 @@ export const membershipsAndTransports$ = (
const transport = membership.getTransport(); const transport = membership.getTransport();
return { return {
membership, membership,
transport: isUnstableLivekitTransport(transport) ? transport : undefined, transport: isUnstableLivekitTransport(transport)
? {
transport,
serverName: membership.userId.replace(/^.*?:/, ""),
}
: undefined,
}; };
}); });
}), }),
), ),
); );
const transports$: Behavior<Epoch<UnstableLivekitTransport[]>> = scope.behavior( const transports$: Behavior<Epoch<TransportLocator[]>> = scope.behavior(
membershipsWithTransport$.pipe( membershipsWithTransport$.pipe(
mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))), mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))),
), ),
+6
View File
@@ -78,6 +78,7 @@ import {
type RemoteScreenShareViewModel, type RemoteScreenShareViewModel,
} from "../state/media/RemoteScreenShareViewModel"; } from "../state/media/RemoteScreenShareViewModel";
import { Connection } from "../state/CallViewModel/remoteMembers/Connection"; import { Connection } from "../state/CallViewModel/remoteMembers/Connection";
import { type TransportLocator } from "../livekit/auth";
export function withFakeTimers(continuation: () => void): void { export function withFakeTimers(continuation: () => void): void {
vi.useFakeTimers(); vi.useFakeTimers();
@@ -213,6 +214,11 @@ export const exampleTransport: UnstableLivekitTransport = {
livekit_service_url: "https://lk.example.org", livekit_service_url: "https://lk.example.org",
}; };
export const exampleTransportLocator: TransportLocator = {
transport: exampleTransport,
serverName: "example.org",
};
export function mockRtcMembership( export function mockRtcMembership(
user: string | RoomMember, user: string | RoomMember,
deviceId: string, deviceId: string,