Merge pull request #3590 from element-hq/valere/noise_cancellation
Config: UrlParams to control noiseSuppression and echoCancellation
This commit is contained in:
@@ -332,6 +332,42 @@ describe("UrlParams", () => {
|
|||||||
expect(computeUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
expect(computeUrlParams("?intent=join_existing").skipLobby).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("noiseSuppression", () => {
|
||||||
|
it("defaults to true", () => {
|
||||||
|
expect(computeUrlParams().noiseSuppression).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is parsed", () => {
|
||||||
|
expect(
|
||||||
|
computeUrlParams("?intent=start_call&noiseSuppression=true")
|
||||||
|
.noiseSuppression,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
computeUrlParams("?intent=start_call&noiseSuppression&bar=foo")
|
||||||
|
.noiseSuppression,
|
||||||
|
).toBe(true);
|
||||||
|
expect(computeUrlParams("?noiseSuppression=false").noiseSuppression).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("echoCancellation", () => {
|
||||||
|
it("defaults to true", () => {
|
||||||
|
expect(computeUrlParams().echoCancellation).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is parsed", () => {
|
||||||
|
expect(computeUrlParams("?echoCancellation=true").echoCancellation).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(computeUrlParams("?echoCancellation=false").echoCancellation).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("header", () => {
|
describe("header", () => {
|
||||||
it("uses header if provided", () => {
|
it("uses header if provided", () => {
|
||||||
expect(computeUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
expect(computeUrlParams("?header=app_bar&hideHeader=true").header).toBe(
|
||||||
|
|||||||
@@ -233,6 +233,17 @@ export interface UrlConfiguration {
|
|||||||
*/
|
*/
|
||||||
waitForCallPickup: boolean;
|
waitForCallPickup: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable echo cancellation for audio capture.
|
||||||
|
* Defaults to true.
|
||||||
|
*/
|
||||||
|
echoCancellation?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to enable noise suppression for audio capture.
|
||||||
|
* Defaults to true.
|
||||||
|
*/
|
||||||
|
noiseSuppression?: boolean;
|
||||||
|
|
||||||
callIntent?: RTCCallIntent;
|
callIntent?: RTCCallIntent;
|
||||||
}
|
}
|
||||||
interface IntentAndPlatformDerivedConfiguration {
|
interface IntentAndPlatformDerivedConfiguration {
|
||||||
@@ -525,6 +536,8 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
|||||||
]),
|
]),
|
||||||
waitForCallPickup: parser.getFlag("waitForCallPickup"),
|
waitForCallPickup: parser.getFlag("waitForCallPickup"),
|
||||||
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
||||||
|
noiseSuppression: parser.getFlagParam("noiseSuppression", true),
|
||||||
|
echoCancellation: parser.getFlagParam("echoCancellation", true),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the final configuration for debugging purposes.
|
// Log the final configuration for debugging purposes.
|
||||||
|
|||||||
@@ -413,6 +413,8 @@ export function createCallViewModel$(
|
|||||||
livekitKeyProvider,
|
livekitKeyProvider,
|
||||||
getUrlParams().controlledAudioDevices,
|
getUrlParams().controlledAudioDevices,
|
||||||
options.livekitRoomFactory,
|
options.livekitRoomFactory,
|
||||||
|
getUrlParams().echoCancellation,
|
||||||
|
getUrlParams().noiseSuppression,
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectionManager = createConnectionManager$({
|
const connectionManager = createConnectionManager$({
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import {
|
import {
|
||||||
type E2EEOptions,
|
|
||||||
Room as LivekitRoom,
|
Room as LivekitRoom,
|
||||||
type RoomOptions,
|
type RoomOptions,
|
||||||
type BaseKeyProvider,
|
type BaseKeyProvider,
|
||||||
|
type E2EEManagerOptions,
|
||||||
|
type BaseE2EEManager,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
||||||
@@ -41,8 +42,10 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
* @param client - The OpenID client parts for authentication, needed to get openID and JWT tokens.
|
* @param client - The OpenID client parts for authentication, needed to get openID and JWT tokens.
|
||||||
* @param devices - Used for video/audio out/in capture options.
|
* @param devices - Used for video/audio out/in capture options.
|
||||||
* @param processorState$ - Effects like background blur (only for publishing connection?)
|
* @param processorState$ - Effects like background blur (only for publishing connection?)
|
||||||
* @param e2eeLivekitOptions - The E2EE options to use for the LiveKit Room.
|
* @param livekitKeyProvider
|
||||||
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
|
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
|
||||||
|
* @param echoCancellation - Whether to enable echo cancellation for audio capture.
|
||||||
|
* @param noiseSuppression - Whether to enable noise suppression for audio capture.
|
||||||
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
@@ -52,20 +55,24 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
livekitKeyProvider: BaseKeyProvider | undefined,
|
livekitKeyProvider: BaseKeyProvider | undefined,
|
||||||
private controlledAudioDevices: boolean,
|
private controlledAudioDevices: boolean,
|
||||||
livekitRoomFactory?: () => LivekitRoom,
|
livekitRoomFactory?: () => LivekitRoom,
|
||||||
|
echoCancellation: boolean = true,
|
||||||
|
noiseSuppression: boolean = true,
|
||||||
) {
|
) {
|
||||||
const defaultFactory = (): LivekitRoom =>
|
const defaultFactory = (): LivekitRoom =>
|
||||||
new LivekitRoom(
|
new LivekitRoom(
|
||||||
generateRoomOption(
|
generateRoomOption({
|
||||||
this.devices,
|
devices: this.devices,
|
||||||
this.processorState$.value,
|
processorState: this.processorState$.value,
|
||||||
livekitKeyProvider && {
|
e2eeLivekitOptions: livekitKeyProvider && {
|
||||||
keyProvider: livekitKeyProvider,
|
keyProvider: livekitKeyProvider,
|
||||||
// It's important that every room use a separate E2EE worker.
|
// It's important that every room use a separate E2EE worker.
|
||||||
// They get confused if given streams from multiple rooms.
|
// They get confused if given streams from multiple rooms.
|
||||||
worker: new E2EEWorker(),
|
worker: new E2EEWorker(),
|
||||||
},
|
},
|
||||||
this.controlledAudioDevices,
|
controlledAudioDevices: this.controlledAudioDevices,
|
||||||
),
|
echoCancellation,
|
||||||
|
noiseSuppression,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
|
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
|
||||||
}
|
}
|
||||||
@@ -90,12 +97,24 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
/**
|
/**
|
||||||
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
||||||
*/
|
*/
|
||||||
function generateRoomOption(
|
function generateRoomOption({
|
||||||
devices: MediaDevices,
|
devices,
|
||||||
processorState: ProcessorState,
|
processorState,
|
||||||
e2eeLivekitOptions: E2EEOptions | undefined,
|
e2eeLivekitOptions,
|
||||||
controlledAudioDevices: boolean,
|
controlledAudioDevices,
|
||||||
): RoomOptions {
|
echoCancellation,
|
||||||
|
noiseSuppression,
|
||||||
|
}: {
|
||||||
|
devices: MediaDevices;
|
||||||
|
processorState: ProcessorState;
|
||||||
|
e2eeLivekitOptions:
|
||||||
|
| E2EEManagerOptions
|
||||||
|
| { e2eeManager: BaseE2EEManager }
|
||||||
|
| undefined;
|
||||||
|
controlledAudioDevices: boolean;
|
||||||
|
echoCancellation: boolean;
|
||||||
|
noiseSuppression: boolean;
|
||||||
|
}): RoomOptions {
|
||||||
return {
|
return {
|
||||||
...defaultLiveKitOptions,
|
...defaultLiveKitOptions,
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
@@ -106,6 +125,8 @@ function generateRoomOption(
|
|||||||
audioCaptureDefaults: {
|
audioCaptureDefaults: {
|
||||||
...defaultLiveKitOptions.audioCaptureDefaults,
|
...defaultLiveKitOptions.audioCaptureDefaults,
|
||||||
deviceId: devices.audioInput.selected$.value?.id,
|
deviceId: devices.audioInput.selected$.value?.id,
|
||||||
|
echoCancellation,
|
||||||
|
noiseSuppression,
|
||||||
},
|
},
|
||||||
audioOutput: {
|
audioOutput: {
|
||||||
// When using controlled audio devices, we don't want to set the
|
// When using controlled audio devices, we don't want to set the
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
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 { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { Room as LivekitRoom } from "livekit-client";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
import { ObservableScope } from "../../ObservableScope.ts";
|
||||||
|
import { ECConnectionFactory } from "./ConnectionFactory.ts";
|
||||||
|
import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
||||||
|
import { exampleTransport, mockMediaDevices } from "../../../utils/test.ts";
|
||||||
|
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||||
|
import { constant } from "../../Behavior";
|
||||||
|
|
||||||
|
// At the top of your test file, after imports
|
||||||
|
vi.mock("livekit-client", async (importOriginal) => {
|
||||||
|
return {
|
||||||
|
...(await importOriginal()),
|
||||||
|
Room: vi.fn().mockImplementation(function (this: LivekitRoom, options) {
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
return {
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
off: emitter.off.bind(emitter),
|
||||||
|
emit: emitter.emit.bind(emitter),
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
remoteParticipants: new Map(),
|
||||||
|
} as unknown as LivekitRoom;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let testScope: ObservableScope;
|
||||||
|
let mockClient: OpenIDClientParts;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testScope = new ObservableScope();
|
||||||
|
mockClient = {
|
||||||
|
getOpenIdToken: vi.fn().mockReturnValue(""),
|
||||||
|
getDeviceId: vi.fn().mockReturnValue("DEV000"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ECConnectionFactory - Audio inputs options", () => {
|
||||||
|
test.each([
|
||||||
|
{ echo: true, noise: true },
|
||||||
|
{ echo: true, noise: false },
|
||||||
|
{ echo: false, noise: true },
|
||||||
|
{ echo: false, noise: false },
|
||||||
|
])(
|
||||||
|
"it sets echoCancellation=$echo and noiseSuppression=$noise based on constructor parameters",
|
||||||
|
({ echo, noise }) => {
|
||||||
|
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
|
||||||
|
const RoomConstructor = vi.mocked(LivekitRoom);
|
||||||
|
|
||||||
|
const ecConnectionFactory = new ECConnectionFactory(
|
||||||
|
mockClient,
|
||||||
|
mockMediaDevices({}),
|
||||||
|
new BehaviorSubject<ProcessorState>({
|
||||||
|
supported: true,
|
||||||
|
processor: undefined,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
echo,
|
||||||
|
noise,
|
||||||
|
);
|
||||||
|
ecConnectionFactory.createConnection(exampleTransport, testScope, logger);
|
||||||
|
|
||||||
|
// Check if Room was constructed with expected options
|
||||||
|
expect(RoomConstructor).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
audioCaptureDefaults: expect.objectContaining({
|
||||||
|
echoCancellation: echo,
|
||||||
|
noiseSuppression: noise,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
||||||
|
test.each([{ controlled: true }, { controlled: false }])(
|
||||||
|
"it sets controlledAudioDevice=$controlled then uses deviceId accordingly",
|
||||||
|
({ controlled }) => {
|
||||||
|
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
|
||||||
|
const RoomConstructor = vi.mocked(LivekitRoom);
|
||||||
|
|
||||||
|
const ecConnectionFactory = new ECConnectionFactory(
|
||||||
|
mockClient,
|
||||||
|
mockMediaDevices({
|
||||||
|
audioOutput: {
|
||||||
|
available$: constant(new Map<never, never>()),
|
||||||
|
selected$: constant({ id: "DEV00", virtualEarpiece: false }),
|
||||||
|
select: () => {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new BehaviorSubject<ProcessorState>({
|
||||||
|
supported: true,
|
||||||
|
processor: undefined,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
controlled,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
ecConnectionFactory.createConnection(exampleTransport, testScope, logger);
|
||||||
|
|
||||||
|
// Check if Room was constructed with expected options
|
||||||
|
expect(RoomConstructor).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
audioOutput: expect.objectContaining({
|
||||||
|
deviceId: controlled ? undefined : "DEV00",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testScope.end();
|
||||||
|
fetchMock.reset();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user