mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
A transport with only a LiveKit SFU `url` has to be authenticated through our own homeserver, which needs to know the name of the server owning the SFU. Carry that name alongside the transport as a TransportLocator, derived from the member's user ID for remote transports and from the client's domain for our own, and key the connection map on the SFU URL, the service URL and the server name together so that `url`, service URL and combined transport shapes are deduplicated consistently.
199 lines
6.9 KiB
TypeScript
199 lines
6.9 KiB
TypeScript
/*
|
|
Copyright 2025 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
Room as LivekitRoom,
|
|
type RoomOptions,
|
|
type BaseKeyProvider,
|
|
type E2EEManagerOptions,
|
|
type BaseE2EEManager,
|
|
} from "livekit-client";
|
|
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 UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
|
|
|
import { type ObservableScope } from "../../ObservableScope.ts";
|
|
import { Connection } from "./Connection.ts";
|
|
import type { MediaDevices } from "../../MediaDevices.ts";
|
|
import type { Behavior } from "../../Behavior.ts";
|
|
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
|
import { getLiveKitOptions } from "../../../livekit/options.ts";
|
|
import {
|
|
advancedCamera,
|
|
cameraResolution,
|
|
cameraFramerate,
|
|
cameraBitrate,
|
|
cameraCodec,
|
|
parseResolution,
|
|
echoCancellationSetting,
|
|
noiseSuppressionSetting,
|
|
autoGainControlSetting,
|
|
} from "../../../settings/settings.ts";
|
|
import { type ClientOpenIDParts } from "../../../livekit/auth";
|
|
|
|
// TODO evaluate if this should be done like the Publisher Factory
|
|
export interface ConnectionFactory {
|
|
createConnection(
|
|
scope: ObservableScope,
|
|
role: "publisher" | "subscriber",
|
|
transport: UnstableLivekitTransport,
|
|
serverName: string,
|
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
|
logger: Logger,
|
|
): Connection;
|
|
}
|
|
|
|
export class ECConnectionFactory implements ConnectionFactory {
|
|
private readonly livekitRoomFactory: () => LivekitRoom;
|
|
|
|
/**
|
|
* Creates a ConnectionFactory for LiveKit connections.
|
|
*
|
|
* @param client - The OpenID client parts for authentication, needed to get openID and JWT tokens.
|
|
* @param roomId - The ID of the Matrix room in which the session takes place.
|
|
* @param slotId - The ID of the MatrixRTC slot in which the session takes place.
|
|
* @param devices - Used for video/audio out/in capture options.
|
|
* @param processorState$ - Effects like background blur (only for publishing connection?)
|
|
* @param livekitKeyProvider - Optional key provider for end-to-end encryption.
|
|
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
|
|
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
|
*/
|
|
public constructor(
|
|
private client: ClientOpenIDParts,
|
|
private readonly roomId: string,
|
|
private readonly slotId: string,
|
|
private devices: MediaDevices,
|
|
private processorState$: Behavior<ProcessorState>,
|
|
livekitKeyProvider: BaseKeyProvider | undefined,
|
|
private controlledAudioDevices: boolean,
|
|
livekitRoomFactory?: () => LivekitRoom,
|
|
) {
|
|
const defaultFactory = (): LivekitRoom => {
|
|
const roomOptions = generateRoomOption({
|
|
devices: this.devices,
|
|
processorState: this.processorState$.value,
|
|
e2eeLivekitOptions: livekitKeyProvider && {
|
|
keyProvider: livekitKeyProvider,
|
|
// It's important that every room use a separate E2EE worker.
|
|
// They get confused if given streams from multiple rooms.
|
|
worker: new E2EEWorker(),
|
|
},
|
|
controlledAudioDevices: this.controlledAudioDevices,
|
|
});
|
|
logger.info("[ECConnectionFactory] livekit room options: ", roomOptions);
|
|
return new LivekitRoom(roomOptions);
|
|
};
|
|
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param scope The observable scope (used for clean-up)
|
|
* @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 logger The logger instance to use for this connection.
|
|
* @param sfuConfig optional config in case we already have a token for this connection.
|
|
* @returns
|
|
*/
|
|
public createConnection(
|
|
scope: ObservableScope,
|
|
role: "publisher" | "subscriber",
|
|
transport: UnstableLivekitTransport,
|
|
serverName: string,
|
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
|
logger: Logger,
|
|
): Connection {
|
|
return new Connection(
|
|
{
|
|
roomId: this.roomId,
|
|
slotId: this.slotId,
|
|
role,
|
|
transport,
|
|
serverName,
|
|
client: this.client,
|
|
scope: scope,
|
|
livekitRoomFactory: this.livekitRoomFactory,
|
|
ownMembershipIdentity,
|
|
},
|
|
logger,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
|
* Reads audio processing and camera quality settings directly from Settings.
|
|
*/
|
|
function generateRoomOption({
|
|
devices,
|
|
processorState,
|
|
e2eeLivekitOptions,
|
|
controlledAudioDevices,
|
|
}: {
|
|
devices: MediaDevices;
|
|
processorState: ProcessorState;
|
|
e2eeLivekitOptions:
|
|
| E2EEManagerOptions
|
|
| { e2eeManager: BaseE2EEManager }
|
|
| undefined;
|
|
controlledAudioDevices: boolean;
|
|
}): RoomOptions {
|
|
const liveKitOptions = getLiveKitOptions();
|
|
|
|
// Apply advanced camera settings if enabled
|
|
let videoCaptureDefaults = {
|
|
...liveKitOptions.videoCaptureDefaults,
|
|
deviceId: devices.videoInput.selected$.value?.id,
|
|
processor: processorState.processor,
|
|
};
|
|
let publishDefaults = liveKitOptions.publishDefaults;
|
|
|
|
if (advancedCamera.getValue()) {
|
|
const { width, height } = parseResolution(cameraResolution.getValue());
|
|
const fps = cameraFramerate.getValue();
|
|
const bps = cameraBitrate.getValue();
|
|
const codec = cameraCodec.getValue();
|
|
|
|
videoCaptureDefaults = {
|
|
...videoCaptureDefaults,
|
|
resolution: { width, height, frameRate: fps },
|
|
};
|
|
publishDefaults = {
|
|
...publishDefaults,
|
|
videoEncoding: { maxBitrate: bps, maxFramerate: fps },
|
|
videoCodec: codec,
|
|
};
|
|
}
|
|
|
|
return {
|
|
...liveKitOptions,
|
|
videoCaptureDefaults,
|
|
publishDefaults,
|
|
audioCaptureDefaults: {
|
|
...liveKitOptions.audioCaptureDefaults,
|
|
deviceId: devices.audioInput.selected$.value?.id,
|
|
echoCancellation: echoCancellationSetting.getValue(),
|
|
noiseSuppression: noiseSuppressionSetting.getValue(),
|
|
autoGainControl: autoGainControlSetting.getValue(),
|
|
},
|
|
audioOutput: {
|
|
// When using controlled audio devices, we don't want to set the
|
|
// deviceId here, because it will be set by the native app.
|
|
// (also the id does not need to match a browser device id)
|
|
deviceId: controlledAudioDevices
|
|
? undefined
|
|
: devices.audioOutput.selected$.value?.id,
|
|
},
|
|
e2ee: e2eeLivekitOptions,
|
|
// TODO test and consider this:
|
|
// webAudioMix: true,
|
|
};
|
|
}
|