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:
@@ -86,12 +86,32 @@ export class VoiceSession {
|
||||
/** Apply a broadcast directive the brain requested (start/stop the stream). */
|
||||
onBroadcastAction?: (action: "start" | "stop") => void | Promise<void>;
|
||||
|
||||
/** The selfbot client behind this voice connection + the negotiated voice
|
||||
* session_id, so the broadcast (Go-Live) can ride the SAME session — one
|
||||
* account hears/speaks AND broadcasts. */
|
||||
private readonly client: any;
|
||||
private readonly channelId: string;
|
||||
private sessionId?: string;
|
||||
|
||||
constructor(channel: VoiceBasedChannel) {
|
||||
this.guildId = channel.guild.id;
|
||||
this.channelId = channel.id;
|
||||
this.client = (channel as any).client;
|
||||
// Wrap the gateway adapter to capture our own voice session_id (needed to
|
||||
// create the Go-Live stream on this same session).
|
||||
const realCreator = channel.guild.voiceAdapterCreator;
|
||||
const adapterCreator: typeof realCreator = (methods) =>
|
||||
realCreator({
|
||||
...methods,
|
||||
onVoiceStateUpdate: (d: any) => {
|
||||
if (d?.session_id) this.sessionId = d.session_id;
|
||||
return methods.onVoiceStateUpdate(d);
|
||||
},
|
||||
});
|
||||
this.connection = joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guild.id,
|
||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||
adapterCreator,
|
||||
selfDeaf: false, // we need to hear users
|
||||
selfMute: false,
|
||||
});
|
||||
@@ -102,6 +122,20 @@ export class VoiceSession {
|
||||
this.attachReceiver();
|
||||
}
|
||||
|
||||
/** The shared session for single-account Go-Live, or null if session_id isn't
|
||||
* captured yet / the client is unavailable. */
|
||||
getSharedSession(): {
|
||||
client: unknown;
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
sessionId: string;
|
||||
botId: string;
|
||||
} | null {
|
||||
const botId = this.client?.user?.id;
|
||||
if (!this.sessionId || !this.client || !botId) return null;
|
||||
return { client: this.client, guildId: this.guildId, channelId: this.channelId, sessionId: this.sessionId, botId };
|
||||
}
|
||||
|
||||
async ready(): Promise<void> {
|
||||
await entersState(this.connection, VoiceConnectionStatus.Ready, 20_000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user