1154e7a6a575a5b7808bdadd9bf65530c33ce54f
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1154e7a6a5 |
Radio: Lautstaerke im Panel -- und leise reinkommen statt erschrecken
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>
|
||
|
|
f08a0e8b35 |
Radio: es war nie UDP -- Discord verlangt DAVE
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> |
||
|
|
484f2edc62 |
Radio: den Schliesscode festhalten, statt UDP zu behaupten
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> |
||
|
|
228853febd |
Radio: die zweite Sackgasse von der ersten trennen — und UDP messbar machen
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>
|
||
|
|
bbb56e1276 |
Radio: den Beitritt in drei Schritte zerlegen statt "signalling" zu melden
"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> |
||
|
|
e246dadbec |
Zwei Befunde aus dem Betriebslog
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>
|
||
|
|
4d71122820 |
Radio: Schalter dorthin, wo man ihn sucht — und Stille sagt jetzt warum
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> |
||
|
|
89f6dfcd29 |
Radio: Webradio im Sprachkanal, 44 Sender zum Durchschalten
/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> |