feat(stream): single-account Go-Live — broadcast on the conversation's session

Proven approach: the conversation (hear+speak) runs on @discordjs/voice; the
Go-Live broadcast is a SEPARATE stream connection created on the SAME selfbot
session (exactly like a real Discord client), so ONE account hears, speaks, AND
broadcasts — no second login, no self_deaf, no voice conflict.

- voice.ts captures its own voice session_id (adapter wrap) and exposes
  getSharedSession() {client, guildId, channelId, sessionId, botId}.
- broadcast.ts threads it into the StreamContext.
- selfbot.ts: when a shared session is present, build the Streamer on the
  conversation client and create the stream on its session_id (no login/joinVoice/
  humanPause); teardown only stops the stream (never leaves voice/destroys the
  shared client). Falls back to the dedicated-account path otherwise.

Verified live: Go-Live connected in ~7s while the conversation voice stayed
ready, and the broadcast was visible in Discord — all on one account.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-13 22:37:26 +09:00
parent e8234b7fb1
commit ddebdd7542
4 changed files with 126 additions and 53 deletions

View File

@@ -8,11 +8,26 @@
*/
import type { AppConfig } from "../config.ts";
/** A live voice session to reuse for single-account Go-Live: the conversation's
* own selfbot client + its negotiated voice session_id. The Go-Live stream is
* created on THIS session (no second login / voice join), so one account both
* hears/speaks (via @discordjs/voice) and broadcasts (via @dank074). */
export interface SharedVoiceSession {
client: unknown;
guildId: string;
channelId: string;
sessionId: string;
botId: string;
}
export interface StreamContext {
guildId: string;
voiceChannelId: string;
/** Post an image to the invoking text channel (used by the screenshot backend). */
postImage?: (png: Buffer, name: string) => Promise<void>;
/** If present and it returns a session, the selfbot backend broadcasts on the
* conversation's existing session instead of a second login (single account). */
getSharedSession?: () => SharedVoiceSession | null;
}
export interface ScreenStreamer {