16 KiB
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-calls
livekit-Branch, weil dieser bereits den noch nicht upstream gemergten PR
element-hq/element-call#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:
- 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.cdnUrlzeigt deshalb aufpublic/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). - Browser-
noiseSuppressionbleibt bei aktivem KI-Filter AUS. Sonst arbeiten zwei Filter gegeneinander und der Browser schneidet dem Modell bereits Signalanteile weg. - 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 — 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.
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 mitwebAudioMixgebaut wird - und das steht inConnectionFactory.tsals 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: undefinedund schriebnoiseSuppressionum. LiveKitsmergeObjectWithoutOverwritingkopiert jeden Schluessel deraudioCaptureDefaultsbis in diegetUserMedia-Constraints durch - auch einen mit Wertundefined. In Safari brach damit das Entmuten, obwohl der Track publiziert wurde.
Daraus zwei stehende Regeln fuer diesen Fork:
- 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 ingenerateRoomOption(ConnectionFactory.ts), abgesichert durch Regressionstests inECConnectionFactory.test.ts(die auf dem alten Stand nachweislich rot waren). - Feature-Tor:
AI_NOISE_SUPPRESSION_AVAILABLEinsrc/livekit/aiNoiseSuppression.ts. Seit der bestandenen Abnahme (2026-08-17, Call zu zweit: Filter wirksam, Tastatur weg, Entmuten intakt) steht es auftrue. 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 LogoHeader.tsx(HeaderLogo)FullScreenViewzeigt den Header nur beiheader === "standard"LogoMark,LogoTypeCallFooter.tsxshowLogo = headerStyle === HeaderStyle.StandardLogoLargeLoginPage,RegisterPagegibt es nur standalone HeaderStyle.Standardist der Default für Nicht-Widget-Nutzung; als Widget ist der HeaderNone(Desktop) oderAppBar(Web/Mobile) — sieheUrlParams.ts. Das gilt auch für Fehlerseiten:FullScreenViewprü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>ausLogo.svgdrin, obwohl es nie gezeichnet wird. Maßgeblich ist die Render-Bedingung, nicht der Treffer. -
header_label(„Element Call-Startseite") undlogin_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.