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>
This commit is contained in:
2026-08-14 12:15:14 +02:00
co-authored by Claude Opus 5
parent bbb56e1276
commit 228853febd
2 changed files with 131 additions and 3 deletions
+35 -3
View File
@@ -165,6 +165,11 @@ function beobachteterAdapter(guild, schritte) {
},
onVoiceServerUpdate: (daten) => {
schritte.sprachserver = true;
// Ein leerer Endpunkt ist kein Versehen: Discord schickt das,
// wenn es den Sprachserver gerade neu zuteilt. Die Bibliothek
// wartet dann auf die zweite Meldung — kommt die nie, bleibt
// sie fuer immer bei "signalling" stehen, ohne Fehler.
schritte.endpunkt = daten?.endpoint ?? null;
return methods.onVoiceServerUpdate(daten);
},
};
@@ -197,7 +202,23 @@ export function beitrittsDiagnose(schritte) {
return 'Der Sprachserver steht, aber der eigene Sprachstatus kam nie an — '
+ 'dem Bot fehlt der Intent **GuildVoiceStates**.';
}
return 'Der Austausch mit dem Sprachserver kam nicht zu Ende — ausgehendes UDP dicht?';
// Ab hier ist alles angekommen. Jetzt entscheidet der Weg, den die
// Verbindung genommen hat.
if (schritte.endpunkt === null) {
return 'Discord hat den Beitritt bestätigt, aber keinen Sprachserver zugeteilt. '
+ 'Das passiert bei einem Regionswechsel — im Kanal unter *Region überschreiben* '
+ 'einmal eine feste Region wählen.';
}
const kamAnsNetz = (schritte.weg ?? []).includes('connecting');
if (!kamAnsNetz) {
return `Der Sprachserver (${schritte.endpunkt ?? '?'}) wurde genannt, aber die Verbindung `
+ 'kam nie bis zum Netzwerk-Schritt.';
}
// Sie war beim Netzwerk-Schritt und ist zurückgefallen: der Austausch mit
// dem Sprachserver selbst scheitert. Das ist UDP, und zwar ausgehend.
return `Der Austausch mit dem Sprachserver **${schritte.endpunkt}** scheitert. `
+ 'Discord-Sprache braucht **ausgehendes UDP** (Zielports 5000065535) — '
+ 'wenn nur TCP rausdarf, sieht es genau so aus.';
}
/** Läuft gerade etwas auf diesem Server? */
@@ -226,7 +247,10 @@ export async function spielen(kanal, station) {
const vorher = laufend.get(guildId);
// Wird nur gefüllt, wenn wirklich neu verbunden wird
const schritte = { angefragt: null, eigenerStatus: false, sprachserver: false };
const schritte = {
angefragt: null, eigenerStatus: false, sprachserver: false,
endpunkt: undefined, weg: [],
};
let verbindung = getVoiceConnection(guildId);
if (!verbindung || verbindung.joinConfig.channelId !== kanal.id) {
@@ -237,6 +261,13 @@ export async function spielen(kanal, station) {
adapterCreator: beobachteterAdapter(kanal.guild, schritte),
selfDeaf: true, // der Bot muss nicht hören, nur senden
});
// Der Weg, den die Verbindung nimmt, unterscheidet zwei Sackgassen,
// die von aussen gleich aussehen: nie über „signalling" hinaus (dann
// fehlt der Sprachserver) oder immer wieder dorthin zurück (dann
// scheitert der UDP-Austausch und die Bibliothek fängt von vorn an).
verbindung.on('stateChange', (_, neuerStand) => {
if (schritte.weg.at(-1) !== neuerStand.status) schritte.weg.push(neuerStand.status);
});
// Discord verschiebt Sprachserver im Betrieb; ohne dieses Zurückholen
// bleibt die Verbindung dann still stehen, statt sich zu erholen.
//
@@ -283,7 +314,8 @@ export async function spielen(kanal, station) {
const grund = beitrittsDiagnose(schritte);
console.error(`[radio] Beitritt steckt bei "${steckt}" — `
+ `angefragt: ${schritte.angefragt}, eigener Status: ${schritte.eigenerStatus}, `
+ `Sprachserver: ${schritte.sprachserver}`);
+ `Sprachserver: ${schritte.sprachserver}, Endpunkt: ${schritte.endpunkt ?? '—'}, `
+ `Weg: ${(schritte.weg ?? []).join(' → ') || '(keiner)'}`);
throw new Error(`Sprachverbindung kam nicht zustande. ${grund}`);
}