fix(voice): screen share releases audio connection so Streamer owns voice session

The dank074 Streamer creates its own WebRTC voice connection, but Discord
allows only ONE voice session per user. When VoiceController (audio) was
already connected, the Streamer join hung forever (never got
VOICE_SERVER_UPDATE). Now:

1. ScreenShareController takes releaseVoice/restoreVoice callbacks.
2. Before joining, it disconnects the @discordjs audio connection via
   VoiceController.disconnectGuild.
3. Streamer joins + streams GoLive.
4. After the stream ends, restoreVoice reconnects the audio connection so
   mic/listen keep working.
5. media.handler wires these via a new setVoiceController accessor from
   commandHandler; VoiceController is the single source of truth.

Also adds caller-bound timeouts & safe .catch() everywhere so a stream
failure can never become an unhandledRejection again.
This commit is contained in:
asepharyana
2026-08-03 08:41:00 +07:00
parent 8528f2c73d
commit 02e2243a98
3 changed files with 74 additions and 13 deletions
@@ -86,6 +86,9 @@ export class CommandHandler {
this.mediaHandler = new MediaHandler(client, () =>
voiceController.getStatus(),
);
// Give media handler access to disconnect/reconnect voice around screen
// share (GoLive needs its own WebRTC connection).
this.mediaHandler.setVoiceController(() => voiceController);
this.guildHandler = new GuildHandler(client);
this.moderationHandler = new ModerationHandler(client);
@@ -102,6 +102,21 @@ export class MediaHandler {
});
}
/**
* Give MediaHandler access to the VoiceController so screen-share can
* disconnect/reconnect the @discordjs audio connection around a GoLive
* stream (Discord allows only one voice session per user).
*/
private voiceControllerAccessor: (() => {
disconnectGuild(guildId: string): Promise<void>;
connect(guildId: string, channelId: string): Promise<unknown>;
getStatus(): { activeGuildId: string | null; activeChannelId: string | null };
}) | null = null;
setVoiceController(accessor: typeof this.voiceControllerAccessor): void {
this.voiceControllerAccessor = accessor;
}
/**
* Persist the latest media state to Redis so the backend/frontend see queue
* advances that happen outside a command (natural track end, screen-share
@@ -164,6 +179,24 @@ export class MediaHandler {
this.screenController = new ScreenShareController(
this.client,
this.getVoiceStatus,
// releaseVoice — disconnect the @discordjs/voice connection so the
// dank074 Streamer can take over (Discord: one voice session/user).
async () => {
const vc = this.voiceControllerAccessor?.();
const guildId = vc?.getStatus().activeGuildId ?? null;
if (vc && guildId) {
await vc.disconnectGuild(guildId);
}
},
// restoreVoice — reconnect the @discordjs audio connection after
// the stream ends so mic/listen keep working.
async () => {
const vc = this.voiceControllerAccessor?.();
const status = vc?.getStatus();
if (vc && status?.activeGuildId && status.activeChannelId) {
await vc.connect(status.activeGuildId, status.activeChannelId);
}
},
);
}
const playback = await this.screenController.start(url);