The WebRTC noise suppression estimates a running noise profile, so it removes stationary noise but not keystrokes, which are transient and never look like noise to it. Chiclet keyboards are as affected as mechanical ones. DeepFilterNet3 runs client-side as a LiveKit track processor and removes them. Off by default and the model is only fetched once a user switches it on, so nobody pays the download who does not want the filter. Default strength is 35 percent rather than full: measured, that already gives keystrokes gone with the voice still natural, and more attenuation only adds artefact risk. Assets ship with us instead of the package's default CDN, which would report every participant's IP to a third party at call start and tie call setup to foreign infrastructure. The Dockerfile gzips the model wasm, which the existing top-level glob missed — 4.1 MB instead of 15.7 MB per client. Browser noise suppression is switched off while the filter runs so the two do not work against each other. Regulation goes through the model's own attenuation limit, so there is no dry/wet mixer and no delay compensation to get wrong. Decision and measurements: management ADR-0018 and issue #0054. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
174 lines
11 KiB
Markdown
174 lines
11 KiB
Markdown
# threadnet-call: Fork-Anpassungen für axion1337.chat
|
|
|
|
Dieses Dokument beschreibt alles, was dieser Fork gegenüber Upstream Element Call ändert. Die
|
|
übrigen Dateien in `docs/` sind unverändertes Upstream-Material.
|
|
|
|
## 1. Warum Fork von `emmick4/element-call` (Branch `livekit`)?
|
|
|
|
Nicht direkt von `element-hq/element-call` geforkt, sondern von `emmick4/element-call`s
|
|
`livekit`-Branch, weil dieser bereits den noch nicht upstream gemergten PR
|
|
[element-hq/element-call#3736](https://github.com/element-hq/element-call/pull/3736)
|
|
enthält - config-driven `media_quality`. Ohne diesen PR wäre eigener Custom-Code nötig gewesen,
|
|
um Video-/Audio-Qualitätslimits pro Deployment konfigurierbar zu machen.
|
|
|
|
## 2. media_quality-Defaults
|
|
|
|
Konfiguriert in `vite-embedded.config.ts` (im `generateFile`-Plugin-Aufruf, der
|
|
`media_quality` in die zur Build-Zeit gebackene `config.json` des Embedded-Packages schreibt):
|
|
|
|
- Kamera: **1440p / 60fps / ~8 Mbps** (`video.max_resolution/max_framerate/max_bitrate`)
|
|
- Screen-Share: **1440p / 30fps / ~6 Mbps**
|
|
- 720p-Zwischen-Simulcast-Layer (`video.simulcast_layers`) - ohne diesen fiel die Übertragung
|
|
bei kleinsten Netzwerkschwankungen direkt von 1440p auf blockiges 360p
|
|
- `video_codec: "h264"` (siehe Incident unten für die Begründung)
|
|
|
|
Das sind Startwerte, keine harten Limits - Nutzer können in den Call-Settings weiter hochdrehen.
|
|
|
|
## 3. VP9-Incident (2026-07-28) — ⚠️ nicht ohne Browser-Repro erneut versuchen
|
|
|
|
Erster Versuch setzte `video_codec: "vp9"` (Commit `83db5224`). Das hat Calls **live komplett
|
|
kaputt gemacht** (kein Bild/Ton), obwohl die LiveKit-SFU-Server-Logs den
|
|
Codec-Regression-Fallback auf VP8 als scheinbar erfolgreich zeigten. Sofort zurückgerollt
|
|
(Commit `f845d81e`). Vermutete Ursache: LiveKit nutzt für VP9/AV1 SVC statt klassischem
|
|
Simulcast, aber `buildPublishOptions()` in diesem Fork setzt immer Simulcast-Layer - ein
|
|
echter Code-Fix wäre nötig, um das aufzulösen. **Root Cause nie abschließend isoliert** (hätte
|
|
einen Browser-Konsolen-/WebRTC-Internals-Repro gebraucht). Danach auf 720p-Zwischen-Layer +
|
|
`h264` (klassisches Simulcast, kein SVC-Risiko, oft hardwarebeschleunigt v.a. auf iOS)
|
|
umgestellt - live verifiziert (7/8 Tracks nativ H.264, 1 sauberer VP8-Fallback).
|
|
|
|
**Nicht erneut versuchen, ohne vorher einen echten Browser-Repro zu haben.**
|
|
|
|
## 4. npm-Publish zu Gitea (`@sorb/threadnet-call-embedded`)
|
|
|
|
⚠️ Dieser Abschnitt beschrieb bis 2026-08-09 einen manuellen/lokalen Vorgang, der
|
|
seit dem 2026-08-06 nicht mehr zutrifft — korrigiert bei einer Doku-Durchsicht.
|
|
|
|
Das Embedded-Package (`embedded/web/package.json`, aktuell `0.19.2-threadnet.7`) wird zu
|
|
Gitea's npm-Registry veröffentlicht (`https://rohana.axion1337.de/api/packages/sorb/npm/`,
|
|
Scope `@sorb`). Der obere Versionierungs-Track hier (`0.19.2-threadnet.N`) ist unabhängig von
|
|
den Docker-Image-Tags, unter denen das fertig gebaute Widget im gitops-Repo deployt wird (z.B.
|
|
`v0.2.3-elementcall-h264` als `threadnet-web`-Image-Tag) - zwei getrennte Versionsschemata für
|
|
zwei verschiedene Artefakte (npm-Package vs. Docker-Image).
|
|
|
|
**Der Publish läuft über den `publish_npm`-Job in `.gitlab-ci.yml`**, nicht mehr manuell:
|
|
`needs: build_embedded`, ausgelöst per Tag oder auf `main` (`when: manual`, bewusst kein
|
|
Automatismus - Veröffentlichen bleibt ein Akt). Bricht ab, wenn `embedded/web/dist` zu wenige
|
|
Dateien enthält (Schutz gegen die leeren Pakete aus threadnet-call#4), und veröffentlicht mit
|
|
`--tag threadnet` statt `latest`, weil unsere Versionen als npm-Prereleases gelten. Auth über
|
|
die CI-Variable `GITEA_NPM_TOKEN` (Gitea-Token, Scope `write:package`) - die frühere lokale,
|
|
untrackte `.npmrc` mit Klartext-Token ist damit hinfällig.
|
|
|
|
Das committete `.github/workflows/publish-embedded-packages.yaml` ist unbenutztes
|
|
Upstream-Material (zielt weiterhin auf `registry.npmjs.org`/`@element-hq`) - Gitea Actions
|
|
läuft hier nicht, siehe Repo-Topologie unten.
|
|
|
|
## 5. Bewusst keine Server-seitige ML-Rauschunterdrückung
|
|
|
|
Nur der client-seitige WebRTC-Standardtoggle (Echo/Noise/Gain-Suppression, aus derselben
|
|
Upstream-PR-Linie) wurde übernommen. Bewusst **kein** LiveKit-Agents-basiertes Server-seitiges
|
|
ML-Noise-Cancellation (z.B. selbst gehostetes DTLN/RNNoise) - LiveKits eigene Dokumentation
|
|
beschreibt diesen Baustein als für AI-Voice-Agents gedacht, nicht für Mensch-zu-Mensch-Calls
|
|
(kein unterstützter Weg, bereinigtes Audio an andere Teilnehmer weiterzuleiten).
|
|
|
|
## 5b. KI-Geraeuschunterdrueckung client-seitig (ADR-0018)
|
|
|
|
Ergaenzt §5: dort ging es um **server-seitige** ML-Filterung, die verworfen bleibt. Diese
|
|
Anpassung ist **client-seitig** und damit von jenem Einwand nicht betroffen.
|
|
|
|
**Warum:** Der WebRTC-Standardfilter schaetzt ein laufendes Rauschprofil und filtert damit
|
|
STATIONAERE Stoerungen. Tastaturanschlaege sind TRANSIENT und werden nicht als Stoerung
|
|
erkannt - sie gehen ungefiltert raus. Betroffen sind auch leise Chiclet-Tastaturen.
|
|
|
|
**Was geaendert wurde:**
|
|
|
|
| Datei | Aenderung |
|
|
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
| `src/livekit/aiNoiseSuppression.ts` | **neu** - baut den DeepFilterNet3-TrackProcessor |
|
|
| `src/settings/settings.ts` | zwei Settings: `ai-noise-suppression` (bool, Default **false**), `ai-noise-suppression-level` (number, Default **35**) |
|
|
| `src/state/CallViewModel/remoteMembers/ConnectionFactory.ts` | `processor:` in `audioCaptureDefaults`; `noiseSuppression` wird bei aktivem KI-Filter **abgeschaltet** |
|
|
| `src/settings/SettingsModal.tsx` | `AiNoiseSuppressionSettings` im Audio-Tab (Checkbox + Regler) |
|
|
| `public/assets/dfn3/**` | Modell + wasm, **23 MB**, selbst ausgeliefert |
|
|
| `package.json` | `deepfilternet3-noise-filter` |
|
|
|
|
**Drei Dinge, die beim Portieren nicht verlorengehen duerfen:**
|
|
|
|
1. **Assets selbst ausliefern.** Das Paket laedt Modell und wasm sonst von `cdn.mezon.ai` -
|
|
das meldet bei jedem Call-Start die IP jedes Teilnehmers an einen Dritten und haengt die
|
|
Verfuegbarkeit an fremde Infrastruktur. `assetConfig.cdnUrl` zeigt deshalb auf
|
|
`public/assets/dfn3/`. **Beim Aktualisieren des Pakets pruefen, ob sich die Asset-Pfade
|
|
geaendert haben** (`v3/pkg/df_bg.wasm`, `v3/models/DeepFilterNet3_onnx.tar.gz`).
|
|
2. **Browser-`noiseSuppression` bleibt bei aktivem KI-Filter AUS.** Sonst arbeiten zwei
|
|
Filter gegeneinander und der Browser schneidet dem Modell bereits Signalanteile weg.
|
|
3. **Default 35 %, nicht 100 %.** Gemessen reicht das fuer "Tastatur weg und Stimme
|
|
natuerlich"; mehr Daempfung erhoeht nur das Artefaktrisiko.
|
|
|
|
**Kein** Rust/wasm-Build noetig (das fertige Paket genuegt), **kein** Dry/Wet-Mixer und
|
|
**kein** Delay-Node - geregelt wird ueber den Modellparameter (`noiseReductionLevel` /
|
|
`atten_lim`), nicht ueber Zurueckmischen des Originalsignals.
|
|
|
|
⚠️ **Mobil ungeprueft.** Der Telefontest wurde bewusst ausgesetzt; weil der Filter opt-in
|
|
ist, bleibt er auf schwachen Geraeten schlicht aus. Messung und Herleitung: management #0054.
|
|
|
|
## 6. Produktname im Widget (`.env.production`)
|
|
|
|
`VITE_PRODUCT_NAME=aXion1337.Chat` in `.env.production` — eine Zeile, keine
|
|
Quelldatei angefasst. Upstream liest den Namen an jeder Stelle als
|
|
`import.meta.env.VITE_PRODUCT_NAME || "Element Call"`; die Variable zu setzen ist
|
|
deshalb die vollständige Umbenennung **ohne Merge-Reibung**.
|
|
|
|
Der Name folgt der Regel aus `management/shared/branding.md`: „in der Anwendung"
|
|
heißt es aXion1337.Chat — und das Call-Widget läuft in der Anwendung.
|
|
|
|
**Was dadurch tatsächlich umbenannt wird** (Inventur 2026-08-06, embedded-Modus):
|
|
|
|
| Stelle | sichtbar wo |
|
|
| ------------------------------------ | ------------------------------------------------------- |
|
|
| `error.matrix_rtc_transport_missing` | Fehlermeldung „Der Server ist nicht für … konfiguriert" |
|
|
| `error.open_elsewhere_description` | „… wurde in einem anderen Tab geöffnet" |
|
|
| `DeveloperSettingsTab` Versionszeile | Einstellungen → Entwicklermodus |
|
|
| `usePageTitle` → `document.title` | im iframe unsichtbar, der Vollständigkeit halber |
|
|
|
|
**Was im Embedded-Build ohnehin nie erscheint** — und deshalb _nicht_ angefasst
|
|
wurde:
|
|
|
|
- **Die Logo-SVGs.** Alle vier sind importiert und liegen im Bundle — gerendert wird
|
|
im Widget aber keins, und zwar jeweils durch dieselbe Bedingung gesperrt:
|
|
|
|
| SVG | gerendert von | Sperre |
|
|
| ---------------------- | --------------------------- | ----------------------------------------------------------------- |
|
|
| `Logo` | `Header.tsx` (`HeaderLogo`) | `FullScreenView` zeigt den Header nur bei `header === "standard"` |
|
|
| `LogoMark`, `LogoType` | `CallFooter.tsx` | `showLogo = headerStyle === HeaderStyle.Standard` |
|
|
| `LogoLarge` | `LoginPage`, `RegisterPage` | gibt es nur standalone |
|
|
|
|
`HeaderStyle.Standard` ist der Default für **Nicht**-Widget-Nutzung; als Widget ist
|
|
der Header `None` (Desktop) oder `AppBar` (Web/Mobile) — siehe `UrlParams.ts`. Das
|
|
gilt auch für Fehlerseiten: `FullScreenView` prüft dieselbe Bedingung, ein Absturz
|
|
im Widget zeigt also kein Logo.
|
|
|
|
⚠️ Nicht danach greppen, ob ein Logo „irgendwo vorkommt" — im gebauten Bundle steht
|
|
`<desc>Element Call (Beta)</desc>` aus `Logo.svg` drin, obwohl es nie gezeichnet
|
|
wird. Maßgeblich ist die Render-Bedingung, nicht der Treffer.
|
|
|
|
- **`header_label` („Element Call-Startseite") und `login_subheading`.** Startseite
|
|
und Login gibt es nur im Standalone-Modus.
|
|
- **Die drei deutschen `developer_mode.matrixRTCMode.*.description`.** Die reden über
|
|
_Gegenstellen_ („alle beteiligten Element Call Clients v0.17.0 oder neuer") — das
|
|
ist eine Kompatibilitätsaussage über fremde Clients, keine Selbstbezeichnung. Sie
|
|
umzubenennen wäre inhaltlich falsch.
|
|
|
|
⚠️ `build:embedded:development` läuft mit `--mode development` und lädt
|
|
`.env.production` nicht — ein Dev-Build zeigt weiter „Element Call". Beabsichtigt;
|
|
ausgeliefert wird der Produktions-Build.
|
|
|
|
**Verifiziert** am CI-Artefakt von Pipeline 189 (`ac688d3`), nicht am Quelltext: das
|
|
gebaute Bundle enthält „aXion1337.Chat"; die verbliebenen „Element Call"-Treffer sind
|
|
Source-Maps, die 22 Sprachdateien (`header_label`, standalone) und die
|
|
`<desc>`-Zeile aus `Logo.svg` — keine davon im Widget sichtbar.
|
|
|
|
## Repo-Topologie (seit 2026-07-31)
|
|
|
|
**Kanonisch ist `git.lab/axion1337.chat/threadnet-call`** (Homelab-GitLab) — die Kopie auf
|
|
`rohana.axion1337.de/sorb/threadnet-call` ist ein automatischer **Push-Mirror** (Lesekopie,
|
|
Issues, npm-Registry). **Niemals direkt nach rohana pushen** — der Mirror überschreibt
|
|
divergente Stände.
|