Commit Graph
8 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5 1154e7a6a5 Radio: Lautstaerke im Panel -- und leise reinkommen statt erschrecken
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Anlass ist eine Beobachtung aus dem Betrieb: der Bot kam mit voller
Aussteuerung in den Kanal, und wer schon drin sass, ist erschrocken. Dass
sich jeder den Bot fuer sich leiser drehen kann, hilft erst *nachdem* es zu
laut war. Also liegt die Vorgabe jetzt bei 50 %, und lauter machen kostet
zwei Knopfdruecke.

Dafuer musste die Tonkette umgebaut werden. Bisher lief Opus unveraendert
durch (ffmpeg -> Ogg/Opus -> Discord). Das war eine Stufe sparsamer, aber an
fertigen Opus-Paketen laesst sich die Lautstaerke nicht drehen -- dafuer muss
man an die Abtastwerte. Jetzt: ffmpeg gibt PCM aus, die Sprachbibliothek
regelt und kodiert.

Der alte Einwand im Dateikopf ("sonst muesste @discordjs/opus mit ins Image,
und das will gebaut werden") stimmt weiterhin -- fuer @discordjs/opus.
Nachgemessen am 14.08.2026 in node:22-slim:

  @discordjs/opus  npm i bricht ab, kein Prebuild fuer node-v127/glibc-2.36,
                   node-gyp will bauen, im Image fehlen die Werkzeuge
  opusscript       reines JavaScript, installiert in 740 ms, kein Compiler
                   0,3 % eines Kerns fuers Kodieren eines Dauerstroms

Der Umweg ueber Opus-Dekodieren entfaellt dabei ganz, ffmpeg liefert ja schon
Abtastwerte. Deshalb 0,3 % und nicht 2,9 %, was Dekodieren und Kodieren
zusammen gekostet haetten.

Die Kette ist mit echtem ffmpeg gegen einen Sinuston geprueft: 200
Opus-Pakete fuer 4,0 s Ton, kein Versatz, und die Lautstaerke laesst sich
mitten im Strom aendern. Genau darum geht der Weg ueber PCM und nicht ueber
ffmpegs volume-Filter -- der haette bei jedem Klick einen Neustart des
Prozesses gebraucht, also eine hoerbare Luecke.

Am Panel zwei Knoepfe in der bestehenden Reihe, Schritte von 10, Bereich 10
bis 200. Am Anschlag graut der Knopf aus, statt sich druecken zu lassen und
nichts zu tun. Die Rueckmeldung ist die neu gezeichnete Tafel mit dem neuen
Prozentwert.

Der Wert steht in den Einstellungen, nicht in radio_state: der Merker dort
wird beim Stoppen geloescht, die Lautstaerke soll das ueberleben.

Dazu der Kanal-Status: was laeuft, steht jetzt auch in der Statuszeile des
Sprachkanals und wird mit dem Titel nachgefuehrt.

  ACHTUNG, das ist ein UNDOKUMENTIERTER Endpunkt.
  PUT /channels/{id}/voice-status -- Discord hat ihn nie in die API-Doku
  aufgenommen, die Pull Requests dazu liegen seit 2023 offen, und discord.js
  hat die Umsetzung als "not planned" geschlossen. Roher Aufruf ueber
  client.rest also, und er kann ohne Ankuendigung verschwinden.

Deshalb ist er als Beiwerk gebaut: faellt er aus, sagt er einmal warum und
schweigt dann -- ein Fehler alle 30 Sekunden waere kein Hinweis mehr,
sondern Rauschen. Das Radio laeuft in jedem Fall weiter. Der naechste
Sender-Start versucht es erneut, falls das Recht inzwischen erteilt wurde.

Das Recht heisst "Kanalstatus festlegen" und sitzt auf Bit 48. discord.js
kennt es nicht einmal als Konstante, deshalb steht die Zahl im Quelltext.
Ungeprueft, weil dafuer ein echter Sprachkanal noetig ist: ob das Recht dem
Bot hier tatsaechlich erteilt ist. Faellt es aus, steht der Grund im Log.

Beim Stoppen wird die Zeile geleert -- sonst behauptet sie stundenlang, es
liefe ein Lied, das laengst vorbei ist.

Nebenbei: prism-media wurde direkt importiert, stand aber nie in der
package.json. Der Import ist mit dem Ogg-Auspacker weggefallen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:53:16 +02:00
D4rkst3randClaude Opus 5 f08a0e8b35 Radio: es war nie UDP -- Discord verlangt DAVE
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Mitschnitt von 12:33 hat es benannt:

  Netzteil: Websocket auf -> Anmeldung -> zu -> Websocket zu (4017) -> zu

4017 heisst DAVE. Discord erzwingt seit dem 01.03.2026 Ende-zu-Ende-
Verschluesselung in allen Nicht-Stage-Sprachkanaelen und weist Gegenstellen,
die das nicht koennen, sofort nach der Anmeldung ab -- lange bevor irgendein
UDP-Paket fliegt. Nachgeschlagen bei Discord, nicht gedeutet.

Gemessen, warum es uns traf: @discordjs/voice 0.18.0 kennt DAVE ueberhaupt
nicht (null Fundstellen im Bundle) und spricht Sprach-Gateway v4. Es meldet
deshalb max_dave_protocol_version: 0. 0.19.2 spricht v8, bringt
@snazzah/davey als feste Abhaengigkeit mit und hat 48 Fundstellen.

Also hochgezogen. Im gebauten Abbild gegengeprueft, weil das native Modul
plattformabhaengig ist und der Lock auf Windows entstanden ist:

  DAVE Libraries
  - @snazzah/davey: 0.1.12

@snazzah/davey-linux-x64-gnu steht im Lock, node:22-slim ist Debian/glibc,
das Modul laedt im Abbild. engines steht jetzt auf >=22.12.0 -- das verlangt
0.19.2, und ">=20" waere ab jetzt gelogen.

Und ein Fehler in der frischen Diagnose selbst, den erst der Ernstfall zeigte:
sie hat den Schritt nicht benannt, obwohl der Mitschnitt ihn enthielt. Das
Netzteil schliesst sich erst und meldet dann den Code, also endet jeder
Mitschnitt auf "zu" -- und genau den nahm die Auswertung als letzten Schritt.
Damit lief alles in "Ursache offen". Jetzt wird "zu" uebersprungen; die
Gegenprobe mit 4009 landet wieder beim UDP-Handschlag statt im Nichts.

Nebenbei stand "zu" zweimal im Mitschnitt, einmal vor und einmal nach dem
Schliesscode: die Entdopplung verglich mit dem letzten Eintrag statt mit dem
letzten Schritt.

Der Weg hierher gehoert zur Sache: erst hiess es "ausgehendes UDP dicht" --
geraten. Dann wurde gemessen, dass UDP rauskommt, und die Meldung sagte
ehrlich "Ursache offen". Diese eine ehrliche Meldung hat den Schliesscode
sichtbar gemacht, und der war die Antwort. Eine Pruefung, die nichts sehen
kann, meldet nicht "sauber" und auch nicht "UDP".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:39:52 +02:00
D4rkst3randClaude Opus 5 484f2edc62 Radio: den Schliesscode festhalten, statt UDP zu behaupten
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Die Meldung "ausgehendes UDP dicht" war geraten. Am 14.08.2026 im laufenden
Container gegengemessen, und sie stimmt nicht:

  UDP raus (DNS)              1.1.1.1:53                 4 ms
  UDP auf hohem Port (STUN)   stun.l.google.com:19302    9 ms
  TCP zum Sprachserver        c-fra13-...discord.media:2096  offen
  Verschluesselung            libsodium-wrappers 0.8.4 geladen

UDP kommt also raus, auch auf hohen Ports und mit Rueckweg durchs NAT. Der
Satz stand trotzdem da, weil "Weg enthaelt connecting" als Beweis genommen
wurde. Ist es nicht: in @discordjs/voice 0.18 faellt die Verbindung bei
*jedem* Websocket-Schluss ausser 4014 stumm nach "signalling" zurueck
(onNetworkingClose) und wirft den Schliesscode dabei weg. Ob der UDP-Teil je
begonnen hat, steht nirgends -- die Pruefung konnte den Unterschied nicht
sehen und hat trotzdem geurteilt.

Jetzt wird das Netzteil selbst mitgeschrieben: seine Schrittnummer
(NetworkingStatusCode, nachgeschlagen -- Websocket auf / Anmeldung /
UDP-Handschlag / Protokollwahl) und der Schliesscode. Nicht ueber `debug`,
denn das druckt Token und Sitzungsschluessel mit. Daraus folgt die Diagnose:

  stehen bei UDP-Handschlag  -> jetzt darf sie UDP sagen
  zu vor dem UDP-Teil        -> Sitzung (4006/4009), kein Netzproblem
  stehen bei Protokollwahl   -> Verschluesselung
  kein Mitschnitt            -> "Ursache offen". Nicht mehr "UDP".

Warum der UDP-Fall so aussieht, als haenge er: performIPDiscovery hat in
0.18 keine Zeitschranke, und die UDP-Keepalives werden nicht mehr
ausgewertet. Bleibt die Antwort aus, steht der Schritt, bis Discord den
Websocket zumacht.

Drei Sachen noch, die beim Nachsehen auffielen:

- Ein Fehler aus dem Netzteil hat den ganzen Prozess beendet. VoiceConnection
  reicht ihn als 'error' weiter, und ein EventEmitter ohne 'error'-Hoerer
  wirft. Genau dieser Weg ist im Fehlerfall offen -- die UDP-Erkennung meldet
  sich darueber. Jetzt gibt es einen Hoerer.
- schritte.endpunkt wurde bei jedem VOICE_SERVER_UPDATE ueberschrieben. Bei
  einem Rueckfall nennt Discord den Server erneut, also verdeckte die zweite
  Meldung den Endpunkt, an dem es scheiterte. Jetzt gesammelt, und fuer die
  Diagnose zaehlt der erste echte.
- tools/ landete nie im Abbild (COPY src ./src). Der Aufruf im Kopf von
  udp-pruefen.mjs -- "docker exec ... node tools/udp-pruefen.mjs" -- lief ins
  Leere. Jetzt kopiert das Dockerfile es mit.

Die neun Diagnose-Zweige sind gegen die exportierten Funktionen im Container
durchgespielt, nicht gegen eine Kopie.

Was weiter offen bleibt: eine Sperre speziell auf Zielports 50000-65535 ist
nicht widerlegt -- dafuer fehlt eine Gegenstelle, die dort antwortet. Der
naechste Fehlversuch sagt es von selbst.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:31:27 +02:00
D4rkst3randClaude Opus 5 228853febd Radio: die zweite Sackgasse von der ersten trennen — und UDP messbar machen
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Aus dem Betrieb: alle drei Beitrittsschritte durch, trotzdem "signalling".
Das ist kein Widerspruch. In @discordjs/voice gibt es zwei Wege dorthin:

  - `configureNetworking` steigt aus, wenn der Endpunkt leer ist. Discord
    schickt das bei einem Regionswechsel; kommt keine zweite Meldung, bleibt
    die Verbindung fuer immer stehen.
  - Scheitert der Austausch mit dem Sprachserver, wirft die Bibliothek die
    Verbindung nach "signalling" zurueck und faengt von vorn an. Von aussen
    sieht das genauso aus.

Deshalb schreibt der Adapter jetzt den Endpunkt mit, und die Verbindung
protokolliert ihren Weg. War sie einmal bei "connecting" und ist
zurueckgefallen, ist es der UDP-Austausch — dann steht der Endpunkt in der
Meldung, damit man ihn von Hand pruefen kann.

Dazu tools/udp-pruefen.mjs: im Container aufrufen und es sagt, ob UDP
ueberhaupt rauskommt und ob auch auf hohen Ports. Genau das unterscheidet
"Firewall zu" von "Firewall laesst nur die ueblichen Ports durch" — und
Discord-Sprache braucht die hohen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:15:14 +02:00
D4rkst3randClaude Opus 5 bbb56e1276 Radio: den Beitritt in drei Schritte zerlegen statt "signalling" zu melden
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
"signalling" sagt nur, dass etwas fehlt, nicht was. Ein Beitritt zum
Sprachkanal ist ein Dreischritt:

  1. Der Bot bittet Discord ueber die bestehende Verbindung um den Beitritt.
  2. Discord antwortet mit dem eigenen Sprachstatus (darin die Sitzungs-ID).
  3. Discord nennt den Sprachserver.

Der Adapter zwischen Verbindung und Sprachbibliothek haelt jetzt fest,
welcher davon stattgefunden hat. Aus den drei Haekchen wird ein Satz, der
sagt, was zu tun ist — fehlende Berechtigung, fehlender Intent, oder ein
Netz, das UDP nicht rauslaesst. Fuenf Faelle, fuenf verschiedene Antworten;
der Test besteht darauf, dass keine zwei gleich lauten.

Dazu eine Pruefung vorweg, die genau dasselbe Bild erzeugt haette: ein
voller Sprachkanal. Discord nimmt die Anfrage entgegen und antwortet dann
einfach nicht mehr — das sieht aus wie ein Rechteproblem. Wer Mitglieder
verschieben darf, kommt am Limit vorbei, sonst nicht.

Nachgesehen und ausgeschlossen: discord.js 14.27 verdrahtet beide
Sprachereignisse (VOICE_STATE_UPDATE ueber die Action, VOICE_SERVER_UPDATE
ueber den Handler), und GuildVoiceStates steht in den Intents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:09:59 +02:00
D4rkst3randClaude Opus 5 e246dadbec Zwei Befunde aus dem Betriebslog
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
1. /api/auditlog antwortete mit 500. `displayAvatarURL({ size: 48 })` — 48 ist
   keine Zweierpotenz, Discord nimmt nur 16 bis 4096. Ueberall sonst im Code
   stehen 64 oder 128, nur hier nicht. Der Wurf lief durch das map, also riss
   ein einziges Avatar die ganze Liste mit. Jetzt 64, und die Bildsuche faengt
   ihre Fehler selbst ab. Dazu eine Pruefung, die verbotene Groessen im
   Quelltext findet, bevor sie jemand im Log findet.

2. "Sprachverbindung kam nicht zustande" sagt jetzt, WO sie stehen blieb.
   Das entscheidet naemlich die Ursache: bleibt sie bei `signalling`, hat
   Discord nie einen Sprachserver genannt (Rechte, Gateway). Bleibt sie bei
   `connecting`, ist der Sprachserver bekannt und der UDP-Austausch mit ihm
   scheitert — praktisch immer gesperrtes ausgehendes UDP. Beides steht jetzt
   im Log und in der Antwort auf /radio.

   Dazu der dokumentierte Umgang mit Close-Code 4014: rausgeworfen oder
   verschoben heisst nicht wiederverbinden, sondern aufhoeren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:00:14 +02:00
D4rkst3randClaude Opus 5 4d71122820 Radio: Schalter dorthin, wo man ihn sucht — und Stille sagt jetzt warum
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Drei Sachen, alle vom selben Bericht ausgeloest.

1. Der Schalter war nur im Modul-Reiter zu finden, die Meldung schickte aber
   nach "Community". Beim Nachsehen kam heraus: Modul-Schalter haben in
   /api/settings gar keinen Weg — jeder stand von Hand in api.js, und fuenf
   fehlten dort (radio, automod, anti_raid, linked_roles, ls_farm). Die
   liessen sich nur ueber den Modulreiter umlegen. Jetzt kommen sie wie
   Kanaele, Rollen und Texte aus dem Register; jeder neue ist damit von
   allein speicherbar. Dazu steht der Haken jetzt oben im Radio-Abschnitt,
   wo auch die Sender stehen.

2. Der Bot sass im Sprachkanal und schwieg. Ursache im Code: nach
   joinVoiceChannel wurde sofort losgespielt. Der Aufruf kommt zurueck,
   sobald der Bot sichtbar im Kanal steht — der UDP-Weg wird danach erst
   ausgehandelt. Wer vorher sendet, sendet ins Leere. Jetzt wird auf
   VoiceConnectionStatus.Ready gewartet, und schlaegt das fehl, sagt der
   Befehl es statt still dazustehen.

3. Stille erklaert sich. Fehlt ffmpeg im Container, setzte sich der Bot
   bisher hin und schwieg — nichts stuerzte ab, nichts fehlte sichtbar.
   ffmpeg wird jetzt beim Start geprueft, /radio verweigert mit klarer
   Ansage, und kommt binnen acht Sekunden kein Ton, steht der Grund in der
   Tafel: fehlendes ffmpeg oder ein Sender, der nichts liefert.

Nebenbei: -analyzeduration stand hinter -i und wurde damit als
Ausgabe-Option gelesen, also ignoriert.

Geprueft: die Tonkette laeuft lokal gegen einen echten Sender durch bis zum
Abspieler (4,1 s Ton in 6 s Laufzeit, Status bleibt playing). Alle 16
Modul-Schalter gehen angemeldet durch /api/settings hin und zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:54:49 +02:00
D4rkst3randClaude Opus 5 89f6dfcd29 Radio: Webradio im Sprachkanal, 44 Sender zum Durchschalten
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
/radio holt den Bot in den Sprachkanal und postet eine Tafel: Auswahlmenue
mit allen Sendern, Stopp, und ein Embed mit dem laufenden Titel, das sich
alle 30 Sekunden selbst nachfuehrt.

Der Titel kommt aus dem Icecast-Strom selbst (Icy-MetaData). Bewusst so und
nicht ueber eine Sender-API: das funktioniert bei jedem Strom, den jemand
eintraegt, und nicht nur bei den drei, die ich kenne. Leere Titel-Bloecke
heissen "unveraendert", deshalb werden bis zu drei gelesen.

Die Tonkette braucht keine Opus-Bibliothek: ffmpeg holt das MP3 und gibt
direkt Ogg/Opus aus, prism-media packt nur noch aus. Gemessen: 600 Pakete in
fuenf Sekunden. Sonst muesste @discordjs/opus mit ins Image, und das will
gebaut werden. Neu im Image ist nur ffmpeg.

44 RauteMusik-Sender sind ab Werk eingetragen — dieselben, die im LS25 und
ETS als Bordradio laufen. Die Liste ist nicht abgeschrieben, sondern
gemessen: jede Adresse einzeln angefragt, der Name kommt aus icy-name.
Doppelgaenger sind raus (deutschrap-charts und wackenradio liefern denselben
Strom wie deutschrap und metal). Eingetragen wird einmal, solange die Liste
leer ist — wer loescht, behaelt es geloescht.

Ein Auswahlmenue fasst nur 25 Eintraege. Bei 44 Sendern haette ein Deckel
die letzten 19 stillschweigend verschluckt; stattdessen mehrere Menues,
benannt nach ihrem ersten und letzten Sender.

Der Bot geht raus, wenn der Kanal leer ist, und kommt nach einem Neustart in
den Kanal zurueck — sonst beendet jeder Deploy die Musik endgueltig.

Kein YouTube. Genau daran sind Groovy und Rythm gestorben, und was davon
uebrig ist, ist ein Dauerlauf gegen kaputte Extraktoren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:40:15 +02:00