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( {