feat(audio): attach the AI filter per-track after publication (way B, #0054)
Instead of webAudioMix on the room - which would also rewire playback (sink selection through the AudioContext, LiveKit's Chrome echo workaround) - only the local microphone track gets an AudioContext, via setAudioContext() right before setProcessor(). The attach happens in onLocalTrackPublished, so a failing filter can no longer prevent unmuting: the track is already published by then. audioCaptureDefaults now never carry a processor key in any state; the conditional spread only toggles noiseSuppression. A regression test covers the active case too. The gate stays closed. A single test client opts in via two localStorage keys (ai-noise-suppression-dev plus the regular setting); the regular setting alone stays inert. Four unit tests pin the attach order - setAudioContext before setProcessor is exactly what v0.5.0 lacked - and the containment of a failing attach. 77 tests green across the touched suites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
dcc86431dc
commit
df4e5eeb85
+24
-1
@@ -146,11 +146,34 @@ Daraus zwei **stehende Regeln** fuer diesen Fork:
|
|||||||
(`ConnectionFactory.ts`), abgesichert durch Regressionstests in
|
(`ConnectionFactory.ts`), abgesichert durch Regressionstests in
|
||||||
`ECConnectionFactory.test.ts` (die auf dem alten Stand nachweislich rot waren).
|
`ECConnectionFactory.test.ts` (die auf dem alten Stand nachweislich rot waren).
|
||||||
2. **Feature-Tor:** `AI_NOISE_SUPPRESSION_AVAILABLE` in `src/livekit/aiNoiseSuppression.ts`
|
2. **Feature-Tor:** `AI_NOISE_SUPPRESSION_AVAILABLE` in `src/livekit/aiNoiseSuppression.ts`
|
||||||
steht auf `false`, bis `webAudioMix` entschieden und im Call zu zweit getestet ist.
|
steht auf `false`, bis die Track-Anbindung im Call zu zweit abgenommen ist.
|
||||||
Das Tor neutralisiert auch Clients, die die Einstellung bereits aktiviert hatten
|
Das Tor neutralisiert auch Clients, die die Einstellung bereits aktiviert hatten
|
||||||
(localStorage) - deren Zustand ueberlebt jedes Deployment. Die Bedienelemente in
|
(localStorage) - deren Zustand ueberlebt jedes Deployment. Die Bedienelemente in
|
||||||
`SettingsModal.tsx` sind hinter demselben Tor versteckt.
|
`SettingsModal.tsx` sind hinter demselben Tor versteckt.
|
||||||
|
|
||||||
|
### Weg B (Entscheidung sorb 2026-08-16): AudioContext nur auf dem Mikrofon-Track
|
||||||
|
|
||||||
|
Statt `webAudioMix` am Raum (wuerde auch die Wiedergabe umbauen: Ausgabegeraete-Wahl
|
||||||
|
ueber den AudioContext statt `setSinkId`, eigener Chrome-Echo-Workaround in LiveKit)
|
||||||
|
bekommt **nur der lokale Mikrofon-Track** einen AudioContext - via
|
||||||
|
`LocalAudioTrack.setAudioContext()`, unmittelbar vor `setProcessor()`. Umgesetzt in
|
||||||
|
`applyAiNoiseSuppression()` (`aiNoiseSuppression.ts`), aufgerufen aus
|
||||||
|
`Publisher.onLocalTrackPublished` - also erst **nach** der Publikation: ein
|
||||||
|
scheiternder Filter kann das Entmuten dann nicht mehr verhindern, der Fehlerfall ist
|
||||||
|
per Konstruktion auf den Filter selbst begrenzt.
|
||||||
|
|
||||||
|
Die `audioCaptureDefaults` tragen damit in **keinem** Zustand mehr einen
|
||||||
|
`processor`-Schluessel (Regressionstest deckt auch den Aktiv-Fall ab); dort wird nur
|
||||||
|
noch `noiseSuppression` umgeschaltet, damit Browser- und KI-Filter nicht
|
||||||
|
gegeneinander arbeiten.
|
||||||
|
|
||||||
|
**Testausrollung:** Das Tor bleibt zu. Ein einzelner Test-Client aktiviert den Filter
|
||||||
|
ueber ZWEI localStorage-Schluessel: `matrix-setting-ai-noise-suppression-dev = true`
|
||||||
|
(Entwickler-Schalter, bewusst ohne UI) **und** `matrix-setting-ai-noise-suppression =
|
||||||
|
true`. Die normale Einstellung allein bleibt wirkungslos. Erst nach bestandener
|
||||||
|
Abnahme im Call zu zweit wird das Tor geoeffnet und die UI wieder sichtbar.
|
||||||
|
Einstellungs-Aenderungen wirken ab dem naechsten Beitritt, nicht live im Call.
|
||||||
|
|
||||||
## 6. Produktname im Widget (`.env.production`)
|
## 6. Produktname im Widget (`.env.production`)
|
||||||
|
|
||||||
`VITE_PRODUCT_NAME=aXion1337.Chat` in `.env.production` — eine Zeile, keine
|
`VITE_PRODUCT_NAME=aXion1337.Chat` in `.env.production` — eine Zeile, keine
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sorb/threadnet-call-embedded",
|
"name": "@sorb/threadnet-call-embedded",
|
||||||
"version": "0.19.2-threadnet.9",
|
"version": "0.19.2-threadnet.10",
|
||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE-AGPL-3.0",
|
"LICENSE-AGPL-3.0",
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 aXion1337.chat
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
|
||||||
|
ThreadNet-Fork-Anpassung (ADR-0018) - nicht Upstream. Siehe docs/axion1337-fork.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { type LocalAudioTrack } from "livekit-client";
|
||||||
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
|
import { applyAiNoiseSuppression } from "./aiNoiseSuppression";
|
||||||
|
import {
|
||||||
|
aiNoiseSuppressionDevSetting,
|
||||||
|
aiNoiseSuppressionSetting,
|
||||||
|
} from "../settings/settings";
|
||||||
|
|
||||||
|
// jsdom hat keinen AudioContext - wir stellen einen minimalen bereit, der
|
||||||
|
// sich merkt, dass er existiert. Der echte Kontextaufbau ist Browserarbeit
|
||||||
|
// und wird im Call zu zweit abgenommen; hier geht es um die REIHENFOLGE.
|
||||||
|
class FakeAudioContext {
|
||||||
|
public state = "running";
|
||||||
|
public resume = vi.fn().mockResolvedValue(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeTrack = (): {
|
||||||
|
track: LocalAudioTrack;
|
||||||
|
calls: string[];
|
||||||
|
setProcessor: ReturnType<typeof vi.fn>;
|
||||||
|
} => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const setAudioContext = vi.fn(() => {
|
||||||
|
calls.push("setAudioContext");
|
||||||
|
});
|
||||||
|
const setProcessor = vi.fn(async (): Promise<void> => {
|
||||||
|
calls.push("setProcessor");
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
track: { setAudioContext, setProcessor } as unknown as LocalAudioTrack,
|
||||||
|
calls,
|
||||||
|
setProcessor,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal("AudioContext", FakeAudioContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
aiNoiseSuppressionDevSetting.setValue(false);
|
||||||
|
aiNoiseSuppressionSetting.setValue(false);
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyAiNoiseSuppression (Weg B, #0054)", () => {
|
||||||
|
test("does nothing at all when the filter is not enabled", async () => {
|
||||||
|
aiNoiseSuppressionSetting.setValue(false);
|
||||||
|
const { track, calls } = makeTrack();
|
||||||
|
|
||||||
|
await applyAiNoiseSuppression(track, logger);
|
||||||
|
|
||||||
|
// Der Normalfall aller Nutzer: der Track wird nicht einmal beruehrt.
|
||||||
|
expect(calls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a stale enabled setting without the dev override touches nothing", async () => {
|
||||||
|
// sorbs Zustand nach dem Diagnosefenster: Einstellung an, Tor zu.
|
||||||
|
aiNoiseSuppressionSetting.setValue(true);
|
||||||
|
const { track, calls } = makeTrack();
|
||||||
|
|
||||||
|
await applyAiNoiseSuppression(track, logger);
|
||||||
|
|
||||||
|
expect(calls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets the audio context BEFORE attaching the processor", async () => {
|
||||||
|
// Genau die Reihenfolge, an der v0.5.0 gescheitert ist: setProcessor
|
||||||
|
// ohne AudioContext auf dem Track wirft in LiveKit.
|
||||||
|
aiNoiseSuppressionDevSetting.setValue(true);
|
||||||
|
aiNoiseSuppressionSetting.setValue(true);
|
||||||
|
const { track, calls } = makeTrack();
|
||||||
|
|
||||||
|
await applyAiNoiseSuppression(track, logger);
|
||||||
|
|
||||||
|
expect(calls).toEqual(["setAudioContext", "setProcessor"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a failing processor attach is contained and does not throw", async () => {
|
||||||
|
// Der Track ist zu diesem Zeitpunkt publiziert - ein Filterfehler darf
|
||||||
|
// den Call nicht mehr erreichen.
|
||||||
|
aiNoiseSuppressionDevSetting.setValue(true);
|
||||||
|
aiNoiseSuppressionSetting.setValue(true);
|
||||||
|
const { track, setProcessor } = makeTrack();
|
||||||
|
setProcessor.mockRejectedValue(new Error("wasm sagt nein"));
|
||||||
|
|
||||||
|
await expect(applyAiNoiseSuppression(track, logger)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,13 +9,15 @@ ThreadNet-Fork-Anpassung (ADR-0018) - nicht Upstream. Siehe docs/axion1337-fork.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type AudioProcessorOptions,
|
type AudioProcessorOptions,
|
||||||
|
type LocalAudioTrack,
|
||||||
type Track,
|
type Track,
|
||||||
type TrackProcessor,
|
type TrackProcessor,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { DeepFilterNoiseFilterProcessor } from "deepfilternet3-noise-filter";
|
import { DeepFilterNoiseFilterProcessor } from "deepfilternet3-noise-filter";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger, logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
aiNoiseSuppressionDevSetting,
|
||||||
aiNoiseSuppressionLevelSetting,
|
aiNoiseSuppressionLevelSetting,
|
||||||
aiNoiseSuppressionSetting,
|
aiNoiseSuppressionSetting,
|
||||||
} from "../settings/settings";
|
} from "../settings/settings";
|
||||||
@@ -52,12 +54,17 @@ const ASSET_PFAD = "assets/dfn3";
|
|||||||
export const AI_NOISE_SUPPRESSION_AVAILABLE = false;
|
export const AI_NOISE_SUPPRESSION_AVAILABLE = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ist der Filter wirksam? Nur wenn das Tor offen ist UND der Nutzer ihn
|
* Ist der Filter wirksam? Nur wenn das Tor offen ist (oder der
|
||||||
* eingeschaltet hat. Alle Entscheidungen (Optionen-Bau, UI) laufen ueber
|
* Entwickler-Schalter fuer die Testausrollung gesetzt ist) UND der Nutzer ihn
|
||||||
* dieses eine Praedikat, damit es keinen zweiten, abweichenden Pfad gibt.
|
* eingeschaltet hat. Alle Entscheidungen (Optionen-Bau, Track-Anbindung, UI)
|
||||||
|
* laufen ueber dieses eine Praedikat, damit es keinen zweiten, abweichenden
|
||||||
|
* Pfad gibt.
|
||||||
*/
|
*/
|
||||||
export function isAiNoiseSuppressionEnabled(): boolean {
|
export function isAiNoiseSuppressionEnabled(): boolean {
|
||||||
return AI_NOISE_SUPPRESSION_AVAILABLE && aiNoiseSuppressionSetting.getValue();
|
return (
|
||||||
|
(AI_NOISE_SUPPRESSION_AVAILABLE || aiNoiseSuppressionDevSetting.getValue()) &&
|
||||||
|
aiNoiseSuppressionSetting.getValue()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,3 +95,57 @@ export function createAiNoiseSuppressionProcessor():
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein gemeinsamer AudioContext fuer die Prozessor-Anbindung - lazily erzeugt
|
||||||
|
* und wiederverwendet, weil Browser die Anzahl gleichzeitiger AudioContexte
|
||||||
|
* begrenzen und Gerätewechsel denselben Kontext weiterverwenden sollen.
|
||||||
|
*/
|
||||||
|
let sharedAudioContext: AudioContext | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weg B aus #0054: Haengt den Filter NACH der Publikation an den lokalen
|
||||||
|
* Mikrofon-Track - mit einem eigenen AudioContext NUR auf diesem Track.
|
||||||
|
*
|
||||||
|
* Warum nicht `webAudioMix` am Raum: das wuerde auch die Wiedergabe umbauen
|
||||||
|
* (Ausgabegeraete-Wahl ueber den AudioContext statt setSinkId, eigener
|
||||||
|
* Chrome-Echo-Workaround) - maximaler Wirkradius im empfindlichsten Pfad.
|
||||||
|
* `LocalAudioTrack.setAudioContext()` gibt genau dem einen Track, der den
|
||||||
|
* Prozessor braucht, was `setProcessor()` verlangt, und laesst alles andere
|
||||||
|
* unberuehrt.
|
||||||
|
*
|
||||||
|
* Warum nach der Publikation (LocalTrackPublished) und nicht in den
|
||||||
|
* audioCaptureDefaults: erstens war der Streuschluessel in den Defaults die
|
||||||
|
* Ursache des v0.5.0-Vorfalls (die Constraints muessen upstream-identisch
|
||||||
|
* bleiben), zweitens ist der Fehlerfall so beweisbar harmlos - der Track ist
|
||||||
|
* bereits publiziert, ein scheiternder Filter kann das Entmuten nicht mehr
|
||||||
|
* verhindern.
|
||||||
|
*/
|
||||||
|
export async function applyAiNoiseSuppression(
|
||||||
|
track: LocalAudioTrack,
|
||||||
|
parentLogger: Logger,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!isAiNoiseSuppressionEnabled()) return;
|
||||||
|
const processor = createAiNoiseSuppressionProcessor();
|
||||||
|
if (!processor) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
sharedAudioContext ??= new AudioContext({
|
||||||
|
sampleRate: 48000,
|
||||||
|
latencyHint: "interactive",
|
||||||
|
});
|
||||||
|
// Autoplay-Policy: der Kontext kann suspendiert starten; die Anbindung
|
||||||
|
// passiert aber ohnehin im Nachgang einer Nutzeraktion (Beitritt/Entmuten).
|
||||||
|
if (sharedAudioContext.state === "suspended") {
|
||||||
|
await sharedAudioContext.resume();
|
||||||
|
}
|
||||||
|
track.setAudioContext(sharedAudioContext);
|
||||||
|
await track.setProcessor(processor);
|
||||||
|
parentLogger.info(
|
||||||
|
`KI-Geraeuschunterdrueckung aktiv (Staerke ${aiNoiseSuppressionLevelSetting.getValue()} %)`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Track ist publiziert, der Call laeuft - der Filter faellt aus, mehr nicht.
|
||||||
|
parentLogger.error("KI-Geraeuschunterdrueckung konnte nicht angehaengt werden", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -242,6 +242,14 @@ export const aiNoiseSuppressionLevelSetting = new Setting<number>(
|
|||||||
"ai-noise-suppression-level",
|
"ai-noise-suppression-level",
|
||||||
35,
|
35,
|
||||||
);
|
);
|
||||||
|
// Entwickler-Schalter fuer die Testausrollung (Weg B, #0054): erlaubt einem
|
||||||
|
// EINZELNEN Test-Client, den Filter trotz geschlossenem Feature-Tor zu
|
||||||
|
// aktivieren (localStorage: matrix-setting-ai-noise-suppression-dev = true,
|
||||||
|
// zusaetzlich zur normalen Einstellung). Kein UI dafuer - bewusst.
|
||||||
|
export const aiNoiseSuppressionDevSetting = new Setting<boolean>(
|
||||||
|
"ai-noise-suppression-dev",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed setting defaults from config.json's media_quality section.
|
* Seed setting defaults from config.json's media_quality section.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ConnectionState as LivekitConnectionState,
|
ConnectionState as LivekitConnectionState,
|
||||||
|
type LocalAudioTrack,
|
||||||
type LocalTrackPublication,
|
type LocalTrackPublication,
|
||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
ParticipantEvent,
|
ParticipantEvent,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
trackProcessorSync,
|
trackProcessorSync,
|
||||||
} from "../../../livekit/TrackProcessorContext.tsx";
|
} from "../../../livekit/TrackProcessorContext.tsx";
|
||||||
import { getUrlParams } from "../../../UrlParams.ts";
|
import { getUrlParams } from "../../../UrlParams.ts";
|
||||||
|
import { applyAiNoiseSuppression } from "../../../livekit/aiNoiseSuppression.ts";
|
||||||
import { observeTrackReference$ } from "../../observeTrackReference";
|
import { observeTrackReference$ } from "../../observeTrackReference";
|
||||||
import { type Connection } from "../remoteMembers/Connection.ts";
|
import { type Connection } from "../remoteMembers/Connection.ts";
|
||||||
import { ObservableScope } from "../../ObservableScope.ts";
|
import { ObservableScope } from "../../ObservableScope.ts";
|
||||||
@@ -118,6 +120,17 @@ export class Publisher {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (localTrackPublication.source === Track.Source.Microphone) {
|
if (localTrackPublication.source === Track.Source.Microphone) {
|
||||||
|
// ThreadNet-Fork (ADR-0018, Weg B aus #0054): Filter erst NACH der
|
||||||
|
// Publikation anhaengen - der Track ist dann sicher da, und ein
|
||||||
|
// scheiternder Filter kann das Entmuten nicht mehr verhindern. Bei
|
||||||
|
// inaktivem Filter (Normalfall) kehrt der Aufruf sofort zurueck.
|
||||||
|
const publishedTrack = localTrackPublication.track;
|
||||||
|
if (publishedTrack) {
|
||||||
|
void applyAiNoiseSuppression(
|
||||||
|
publishedTrack as LocalAudioTrack,
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
|
}
|
||||||
const muteState = this.muteStates.audio;
|
const muteState = this.muteStates.audio;
|
||||||
// skip this if a sync is in progress: enabled$ still reflects the old
|
// skip this if a sync is in progress: enabled$ still reflects the old
|
||||||
// state while the handler is mid-flight, so the handler itself will apply
|
// state while the handler is mid-flight, so the handler itself will apply
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ import {
|
|||||||
noiseSuppressionSetting,
|
noiseSuppressionSetting,
|
||||||
autoGainControlSetting,
|
autoGainControlSetting,
|
||||||
} from "../../../settings/settings.ts";
|
} from "../../../settings/settings.ts";
|
||||||
import {
|
import { isAiNoiseSuppressionEnabled } from "../../../livekit/aiNoiseSuppression.ts";
|
||||||
createAiNoiseSuppressionProcessor,
|
|
||||||
isAiNoiseSuppressionEnabled,
|
|
||||||
} from "../../../livekit/aiNoiseSuppression.ts";
|
|
||||||
|
|
||||||
// TODO evaluate if this should be done like the Publisher Factory
|
// TODO evaluate if this should be done like the Publisher Factory
|
||||||
export interface ConnectionFactory {
|
export interface ConnectionFactory {
|
||||||
@@ -180,20 +177,19 @@ function generateRoomOption({
|
|||||||
deviceId: devices.audioInput.selected$.value?.id,
|
deviceId: devices.audioInput.selected$.value?.id,
|
||||||
echoCancellation: echoCancellationSetting.getValue(),
|
echoCancellation: echoCancellationSetting.getValue(),
|
||||||
autoGainControl: autoGainControlSetting.getValue(),
|
autoGainControl: autoGainControlSetting.getValue(),
|
||||||
// ThreadNet-Fork (ADR-0018): KI-Geraeuschunterdrueckung als bedingtes
|
// ThreadNet-Fork (ADR-0018): Die KI-Geraeuschunterdrueckung setzt hier
|
||||||
// Spread. Bei AUS entsteht hier BEWUSST kein `processor`-Schluessel -
|
// NIE einen `processor`-Schluessel - LiveKit kopiert jeden Schluessel
|
||||||
// LiveKit kopiert jeden Schluessel dieser Defaults bis in die
|
// dieser Defaults bis in die getUserMedia-Constraints durch
|
||||||
// getUserMedia-Constraints durch (mergeObjectWithoutOverwriting prueft
|
// (mergeObjectWithoutOverwriting prueft nur auf undefined im Ziel, nicht
|
||||||
// nur auf undefined im Ziel, nicht in der Quelle), und ein
|
// in der Quelle), und ein `processor: undefined` hat in v0.5.0 das
|
||||||
// `processor: undefined` hat in v0.5.0 das Entmuten gebrochen. Der
|
// Entmuten gebrochen. Der Filter wird stattdessen NACH der Publikation
|
||||||
// Aus-Pfad muss identisch mit Upstream sein, auch in der Objektform.
|
// an den Track gehaengt (Weg B, siehe aiNoiseSuppression.ts /
|
||||||
// Bei AN: Browser-Rauschunterdrueckung aus, sonst arbeiten zwei Filter
|
// Publisher.onLocalTrackPublished). Hier bleibt nur: bei aktivem Filter
|
||||||
// gegeneinander und der Browser schneidet dem Modell Signalanteile weg.
|
// die Browser-Rauschunterdrueckung ausschalten, sonst arbeiten zwei
|
||||||
|
// Filter gegeneinander und der Browser schneidet dem Modell bereits
|
||||||
|
// Signalanteile weg.
|
||||||
...(isAiNoiseSuppressionEnabled()
|
...(isAiNoiseSuppressionEnabled()
|
||||||
? {
|
? { noiseSuppression: false }
|
||||||
noiseSuppression: false,
|
|
||||||
processor: createAiNoiseSuppressionProcessor(),
|
|
||||||
}
|
|
||||||
: { noiseSuppression: noiseSuppressionSetting.getValue() }),
|
: { noiseSuppression: noiseSuppressionSetting.getValue() }),
|
||||||
},
|
},
|
||||||
audioOutput: {
|
audioOutput: {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
cameraBitrate,
|
cameraBitrate,
|
||||||
cameraCodec,
|
cameraCodec,
|
||||||
aiNoiseSuppressionSetting,
|
aiNoiseSuppressionSetting,
|
||||||
|
aiNoiseSuppressionDevSetting,
|
||||||
} from "../../../settings/settings.ts";
|
} from "../../../settings/settings.ts";
|
||||||
|
|
||||||
// At the top of your test file, after imports
|
// At the top of your test file, after imports
|
||||||
@@ -162,6 +163,25 @@ describe("ECConnectionFactory - AI noise suppression off-path (ADR-0018)", () =>
|
|||||||
|
|
||||||
aiNoiseSuppressionSetting.setValue(false);
|
aiNoiseSuppressionSetting.setValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("even with the filter active (dev override) the defaults carry no processor key", () => {
|
||||||
|
// Weg B aus #0054: der Prozessor wird NACH der Publikation an den Track
|
||||||
|
// gehaengt, nie ueber die Capture-Defaults - die getUserMedia-Constraints
|
||||||
|
// muessen in JEDEM Zustand upstream-identisch bleiben. Hier aendert sich
|
||||||
|
// nur noiseSuppression, damit nicht zwei Filter gegeneinander arbeiten.
|
||||||
|
aiNoiseSuppressionDevSetting.setValue(true);
|
||||||
|
aiNoiseSuppressionSetting.setValue(true);
|
||||||
|
noiseSuppressionSetting.setValue(true);
|
||||||
|
|
||||||
|
const options = buildRoomOptions();
|
||||||
|
expect(options.audioCaptureDefaults).not.toHaveProperty("processor");
|
||||||
|
expect(options.audioCaptureDefaults).toMatchObject({
|
||||||
|
noiseSuppression: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
aiNoiseSuppressionDevSetting.setValue(false);
|
||||||
|
aiNoiseSuppressionSetting.setValue(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user