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; }, };