2026-07-31 12:00:00 +00:00
# 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`)
2026-08-09 12:00:00 +00:00
⚠️ 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
2026-07-31 12:00:00 +00:00
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).
2026-08-09 12:00:00 +00:00
**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.
2026-07-31 12:00:00 +00:00
## 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).
2026-08-15 12:00:00 +00:00
## 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.
2026-08-16 12:00:00 +00:00
⚠️ ** `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).
2026-08-15 12:00:00 +00:00
**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.
2026-08-17 12:00:00 +00:00
**Mobil ungeprueft — Telefontest gestrichen (Entscheidung sorb 2026-08-18).** Weil der
Filter opt-in und hinter dem Feature-Tor ist, traefe ein mobiler Fehlschlag nur den Nutzer,
der ihn einschaltet; sein Ausweg ist die Checkbox. Messung und Herleitung: management #0054 .
2026-08-15 12:00:00 +00:00
2026-08-16 12:00:00 +00:00
### 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).
2026-08-17 12:00:00 +00:00
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.
2026-08-16 12:00:00 +00:00
2026-08-16 12:00:00 +00:00
### 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.
2026-08-17 12:00:00 +00:00
**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.
2026-08-16 12:00:00 +00:00
Einstellungs-Aenderungen wirken ab dem naechsten Beitritt, nicht live im Call.
2026-08-06 12:00:00 +00:00
## 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):
2026-08-15 12:00:00 +00:00
| Stelle | sichtbar wo |
| ------------------------------------ | ------------------------------------------------------- |
2026-08-06 12:00:00 +00:00
| `error.matrix_rtc_transport_missing` | Fehlermeldung „Der Server ist nicht für … konfiguriert" |
2026-08-15 12:00:00 +00:00
| `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 |
2026-08-06 12:00:00 +00:00
2026-08-15 12:00:00 +00:00
**Was im Embedded-Build ohnehin nie erscheint** — und deshalb _nicht_ angefasst
2026-08-06 12:00:00 +00:00
wurde:
2026-08-06 12:00:00 +00:00
- **Die Logo-SVGs.** Alle vier sind importiert und liegen im Bundle — gerendert wird
im Widget aber keins, und zwar jeweils durch dieselbe Bedingung gesperrt:
2026-08-15 12:00:00 +00:00
| 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 |
2026-08-06 12:00:00 +00:00
`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.
2026-08-15 12:00:00 +00:00
2026-08-06 12:00:00 +00:00
- **`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
2026-08-15 12:00:00 +00:00
_Gegenstellen_ („alle beteiligten Element Call Clients v0.17.0 oder neuer") — das
2026-08-06 12:00:00 +00:00
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.
2026-08-09 12:00:00 +00:00
**Verifiziert** am CI-Artefakt von Pipeline 189 (`ac688d3` ), nicht am Quelltext: das
2026-08-06 12:00:00 +00:00
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.
2026-07-31 12:00:00 +00:00
## 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.