From 53f26185bcfe0b863953dae8f632ecce1bc7f8e2 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 3 Aug 2026 08:05:02 +0700 Subject: [PATCH] =?UTF-8?q?fix(voice):=20screen=20share=20crashed=20gatewa?= =?UTF-8?q?y=20=E2=80=94=20Streamer=20never=20joined=20voice=20+=20unhandl?= =?UTF-8?q?edRejection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: ScreenShareController created @dank074 Streamer but never called streamer.joinVoiceChannel() — playStream threw 'Bot is not connected to a voice channel', and since the code only used .finally() (no .catch), the rejection became an unhandledRejection that took down the whole gateway (graceful shutdown triggered, systemd restarted). Fixes: 1. Resolve active channel + streamer.joinVoiceChannel(channel) before prepareStream/playStream (dank074 needs its OWN WebRTC voice connection). 2. .catch() on the playStream done promise — log + kill ffmpeg instead of crashing the process. 3. .catch() on playback.done in media.handler too. 4. stop() now kills ffmpeg AND stops the streamer's voice connection. --- .../modules/command-handler/media.handler.ts | 19 ++++--- .../src/modules/voice-recording/mediaTypes.ts | 6 +++ .../voice-recording/screenShareController.ts | 52 +++++++++++++++++-- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/services/discord-gateway/src/modules/command-handler/media.handler.ts b/services/discord-gateway/src/modules/command-handler/media.handler.ts index 5428592..ce8e686 100644 --- a/services/discord-gateway/src/modules/command-handler/media.handler.ts +++ b/services/discord-gateway/src/modules/command-handler/media.handler.ts @@ -178,12 +178,19 @@ export class MediaHandler { addedAt: Date.now(), status: "playing", }; - playback.done.finally(() => { - this.screenPlayback = null; - if (currentTrackItem?.mode === "screen") { - currentTrackItem = null; - } - }); + playback.done + .catch((err) => { + this.logger.error( + { error: err instanceof Error ? err.message : String(err) }, + "Screen playback promise rejected", + ); + }) + .finally(() => { + this.screenPlayback = null; + if (currentTrackItem?.mode === "screen") { + currentTrackItem = null; + } + }); this.logger.info({ url }, "Screen share started"); return { id: cmd.id, success: true, data: buildStatusPayload() }; } catch (err) { diff --git a/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts b/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts index bc68fbd..c87a3f5 100644 --- a/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts +++ b/services/discord-gateway/src/modules/voice-recording/mediaTypes.ts @@ -55,6 +55,12 @@ export interface ScreenSharePlayback { stop(): void; } +export interface ScreenShareVoiceStatus { + connected: boolean; + activeGuildId: string | null; + activeChannelId: string | null; +} + export interface ScreenShareController { isActive(): boolean; start(source: string): Promise; diff --git a/services/discord-gateway/src/modules/voice-recording/screenShareController.ts b/services/discord-gateway/src/modules/voice-recording/screenShareController.ts index e97d20d..54fadb5 100644 --- a/services/discord-gateway/src/modules/voice-recording/screenShareController.ts +++ b/services/discord-gateway/src/modules/voice-recording/screenShareController.ts @@ -60,6 +60,22 @@ export class ScreenShareController { this.streamer = new Streamer(this.client); } + // The dank074 Streamer needs its OWN voice connection — it cannot reuse + // the @discordjs/voice connection owned by VoiceController/recorder. + // Resolve the channel object and join via the Streamer's raw gateway + // path (joinVoiceChannel → joinVoice → WebRTC). + const guild = this.client.guilds.cache.get(status.activeGuildId); + const channel = guild?.channels.cache.get(status.activeChannelId); + if ( + !channel || + (channel.type !== "GUILD_VOICE" && channel.type !== "GUILD_STAGE_VOICE") + ) { + throw new Error( + `Voice channel ${status.activeChannelId} not found for screen share`, + ); + } + await this.streamer.joinVoiceChannel(channel); + const { command, output } = prepareStream(directUrl, { encoder: Encoders.software({ x264: { preset: "superfast" } }), width: 1280, @@ -74,15 +90,43 @@ export class ScreenShareController { let stopped = false; const done = playStream(output, this.streamer, { type: "go-live", - }).finally(() => { - this.active = null; - }); + }) + .catch((err) => { + // Never let a stream failure become an unhandledRejection — that + // crashed the whole gateway. Log + surface via the done promise. + const message = err instanceof Error ? err.message : String(err); + this.logger.error( + { error: message, source }, + "Screen stream failed during playback", + ); + if (!stopped) { + stopped = true; + try { + command.kill("SIGTERM"); + } catch { + /* already dead */ + } + } + }) + .finally(() => { + this.active = null; + }); this.active = { done, stop: () => { if (stopped) return; stopped = true; - command.kill("SIGTERM"); + try { + command.kill("SIGTERM"); + } catch { + /* already dead */ + } + // Leave the voice channel the Streamer joined (its own connection). + try { + this.streamer?.voiceConnection?.stop(); + } catch { + /* already gone */ + } this.active = null; }, };