# 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. ⚠️ **`vite-embedded.config.ts`: `publicDir` MUSS aktiv bleiben.** Upstream steht dort `publicDir: false` mit der Begruendung, `public/` enthalte nur das Favicon - bei uns liegen dort die Modell-Assets. Mit dem Upstream-Wert baut alles fehlerfrei, der Standalone-Build funktioniert, und **nur im Widget ist der Filter tot** (404 auf das Modell). Genau diese Zeile ist beim Rebase der wahrscheinlichste stille Rueckfall. **Auslieferungsweg (nicht offensichtlich):** Element Call laeuft hier **nicht** als eigenes Image. Der Weg ist `threadnet-call` -> npm-Paket `@sorb/threadnet-call-embedded` -> Gitea -> `ThreadNet-Web` (webpack kopiert das Paket-`dist` nach `webapp/widgets/element-call/`) -> `threadnet-web`-Image. Deshalb muessen die Assets **ins Paket**; das Widget loest sie relativ zu seiner eigenen URL auf (`/widgets/element-call/assets/dfn3/...`). Das Paket waechst dadurch von ~41 auf ~66 MB (gemessen; der Rest sind Source-Maps und die Crypto-/Vision-wasm). **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. ### Vorfall v0.5.0 (2026-08-16): Filter stillgelegt, Tor in `aiNoiseSuppression.ts` Die erste Fassung (`0.19.2-threadnet.8`) hat in Produktion das **Entmuten gebrochen** - in beide Richtungen: - **Filter an:** `LocalAudioTrack.setProcessor()` verlangt einen AudioContext auf dem Track. Den gibt es nur, wenn der Raum mit `webAudioMix` gebaut wird - und das steht in `ConnectionFactory.ts` als unerprobtes Upstream-TODO auskommentiert. Folge: *"Audio context needs to be set on LocalAudioTrack in order to enable processors"*, es wird nie ein Audio-Track publiziert. - **Filter aus:** Der Optionen-Bau setzte `processor: undefined` und schrieb `noiseSuppression` um. LiveKits `mergeObjectWithoutOverwriting` kopiert **jeden** Schluessel der `audioCaptureDefaults` bis in die `getUserMedia`-Constraints durch - auch einen mit Wert `undefined`. In Safari brach damit das Entmuten, obwohl der Track publiziert wurde. Daraus zwei **stehende Regeln** fuer diesen Fork: 1. **Der Aus-Pfad eines Opt-in-Features muss identisch mit Upstream sein** - auch in der Objektform. Kein zusaetzlicher Schluessel, auch nicht mit Wert `undefined`, kein umgeschriebenes Feld. Umgesetzt als bedingtes Spread in `generateRoomOption` (`ConnectionFactory.ts`), abgesichert durch Regressionstests in `ECConnectionFactory.test.ts` (die auf dem alten Stand nachweislich rot waren). 2. **Feature-Tor:** `AI_NOISE_SUPPRESSION_AVAILABLE` in `src/livekit/aiNoiseSuppression.ts`. Seit der bestandenen Abnahme (2026-08-17, Call zu zweit: Filter wirksam, Tastatur weg, Entmuten intakt) steht es auf `true`. **Bei einer Regression zuerst das Tor schliessen** (`false`) - das legt Filter UND Bedienelemente still, neutralisiert auch Clients mit aktivierter Einstellung im localStorage, und braucht kein Zurueckrollen der Auslieferung. ### 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 (durchlaufen, 2026-08-17 bestanden):** Waehrend das Tor zu war, aktivierte ein einzelner Test-Client 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`. Der Entwickler-Schalter bleibt im Code (harmlos: ohne die eigentliche Einstellung wirkungslos, per Test abgesichert) - er ist das Werkzeug fuer die naechste Testphase dieser Art. Die Bedienelemente sitzen in den Einstellungen **im laufenden Call-Widget** (Audio-Reiter), nicht in Element Web. Einstellungs-Aenderungen wirken ab dem naechsten Beitritt, nicht live im Call. ## 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 `Element Call (Beta)` 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 ``-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.