From 2b6a1eca192fc1f3dc4e1ffb48683258475a14b4 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 31 Aug 2026 19:16:56 +0700 Subject: [PATCH] fix(gateway): re-assert server-undeafen+unmute before every video watch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bot's own VOICE_STATE_UPDATE showed server-level deaf:true — a server-deafened member is NOT sent the streamer's audiovisual RTP by Discord, which is the likely reason no H264 arrives despite the DAVE watch reaching Ready. The previous fire-and-forget forceSelfServerUnmuteUndeafen() ran once after the first Ready join and silently reverted on reconnect/restart. - Export forceSelfServerUnmuteUndeafen from recorder.ts; re-assert it (with read-back verification logging stillDeaf) at the START of every startStreamWatch() before STREAM_WATCH is sent (dynamic import avoids the recorder <-> videoRecorder <-> streamWatchReceiver module cycle). - Re-assert it again after a successful voice reconnect. - startStreamWatch() is now async; callers use void. --- .../src/modules/voice-recording/recorder.ts | 21 +++++++++++++++++-- .../voice-recording/streamWatchReceiver.ts | 13 +++++++++++- .../modules/voice-recording/videoRecorder.ts | 2 +- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/services/discord-gateway/src/modules/voice-recording/recorder.ts b/services/discord-gateway/src/modules/voice-recording/recorder.ts index cd90434c..dad89323 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder.ts @@ -45,8 +45,14 @@ export function setEventBroadcaster(broadcaster: EventBroadcaster | undefined) { * Requires the `MUTE_MEMBERS` + `DEAFEN_MEMBERS` permissions on the bot * (user has granted them). Failures are logged, never fatal — an unmute * failure must not break the voice join/recording. + * + * Exported so the video-receive path (videoRecorder.ts → streamWatch) can + * re-assert it right before requesting STREAM_WATCH — a server-deafened bot + * is NOT sent the streamer's audiovisual RTP, which is why no video arrives + * even when the DAVE watch reaches Ready. Server-deaf can silently revert on + * reconnect/restart, so this is re-invoked on every video watch attempt. */ -async function forceSelfServerUnmuteUndeafen( +export async function forceSelfServerUnmuteUndeafen( client: Client, guild: Guild, ): Promise { @@ -54,7 +60,15 @@ async function forceSelfServerUnmuteUndeafen( const selfId = client.user?.id; if (!selfId) return; await guild.members.edit(selfId, { mute: false, deaf: false }); - logger.info({ guildId: guild.id }, "Forced bot server unmute + undeafen"); + // Verify the PATCH actually landed — read back the live member voice state. + const fresh = guild.members.cache.get(selfId); + const stillDeaf = fresh?.voice?.deaf ?? false; + logger.info( + { guildId: guild.id, stillDeaf }, + stillDeaf + ? "Forced bot server unmute + undeafen (but guild reports still deaf — will retry on next video)" + : "Forced bot server unmute + undeafen", + ); } catch (err) { logger.warn( { @@ -225,6 +239,9 @@ export async function startRecording( ), ]); // Reconnected successfully + // Server-deaf can silently revert across a reconnect — re-assert the bot + // is undeafened + unmuted so video receive keeps working. + void forceSelfServerUnmuteUndeafen(client, channel.guild); } catch { logger.error("Could not reconnect. Destroying connection"); connection.destroy(); diff --git a/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts b/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts index 297a6bd8..0ea1cec1 100644 --- a/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts +++ b/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts @@ -159,13 +159,24 @@ function handleRaw(packet: unknown): void { * waits for STREAM_CREATE / STREAM_SERVER_UPDATE (handled by handleRaw) to * actually open the DAVE connection. Best-effort. */ -export function startStreamWatch(channel: VoiceChannel, userId: string): void { +export async function startStreamWatch( + channel: VoiceChannel, + userId: string, +): Promise { if (!_client) { logger.warn("startStreamWatch: no client set"); return; } const watchKey = `${channel.guild.id}:${userId}`; if (watches.has(watchKey)) return; // already watching + // Re-assert the bot is server-undeafened + unmuted BEFORE requesting video. + // A server-deafened bot is NOT sent the streamer's audiovisual RTP by + // Discord, so no video would arrive even though the DAVE watch reaches + // Ready. Server-deaf can silently revert on reconnect/restart, so this is + // re-invoked on every watch attempt. Dynamic import avoids a static module + // cycle (recorder ⇄ videoRecorder ⇄ streamWatchReceiver). + const { forceSelfServerUnmuteUndeafen } = await import("./recorder.js"); + void forceSelfServerUnmuteUndeafen(_client, channel.guild); const sk = streamKeyFor(channel.id, channel.guild.id, userId); logger.info( { userId, guildId: channel.guild.id, channelId: channel.id, streamKey: sk }, diff --git a/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts b/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts index 3f310d83..131242ed 100644 --- a/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts +++ b/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts @@ -155,7 +155,7 @@ export function startVideoRecording( try { const selfId = _client?.user?.id; if (selfId && userId === selfId) return; - startStreamWatch(channel, userId); + void startStreamWatch(channel, userId); } catch (err) { logger.warn( {