fix(voice): screen share crashed gateway — Streamer never joined voice + unhandledRejection
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.
This commit is contained in:
@@ -178,12 +178,19 @@ export class MediaHandler {
|
|||||||
addedAt: Date.now(),
|
addedAt: Date.now(),
|
||||||
status: "playing",
|
status: "playing",
|
||||||
};
|
};
|
||||||
playback.done.finally(() => {
|
playback.done
|
||||||
this.screenPlayback = null;
|
.catch((err) => {
|
||||||
if (currentTrackItem?.mode === "screen") {
|
this.logger.error(
|
||||||
currentTrackItem = null;
|
{ 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");
|
this.logger.info({ url }, "Screen share started");
|
||||||
return { id: cmd.id, success: true, data: buildStatusPayload() };
|
return { id: cmd.id, success: true, data: buildStatusPayload() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ export interface ScreenSharePlayback {
|
|||||||
stop(): void;
|
stop(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScreenShareVoiceStatus {
|
||||||
|
connected: boolean;
|
||||||
|
activeGuildId: string | null;
|
||||||
|
activeChannelId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ScreenShareController {
|
export interface ScreenShareController {
|
||||||
isActive(): boolean;
|
isActive(): boolean;
|
||||||
start(source: string): Promise<ScreenSharePlayback>;
|
start(source: string): Promise<ScreenSharePlayback>;
|
||||||
|
|||||||
@@ -60,6 +60,22 @@ export class ScreenShareController {
|
|||||||
this.streamer = new Streamer(this.client);
|
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, {
|
const { command, output } = prepareStream(directUrl, {
|
||||||
encoder: Encoders.software({ x264: { preset: "superfast" } }),
|
encoder: Encoders.software({ x264: { preset: "superfast" } }),
|
||||||
width: 1280,
|
width: 1280,
|
||||||
@@ -74,15 +90,43 @@ export class ScreenShareController {
|
|||||||
let stopped = false;
|
let stopped = false;
|
||||||
const done = playStream(output, this.streamer, {
|
const done = playStream(output, this.streamer, {
|
||||||
type: "go-live",
|
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 = {
|
this.active = {
|
||||||
done,
|
done,
|
||||||
stop: () => {
|
stop: () => {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
stopped = true;
|
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;
|
this.active = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user