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>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
/**
|
|
* Pluggable VNC screen-broadcast backends.
|
|
*
|
|
* Per the chosen design (option 1): the streaming method is swappable via
|
|
* STREAM_BACKEND in .env. The default is the real live "Go Live" stream via a
|
|
* selfbot account (only way to get a native Discord video broadcast), with safe
|
|
* fallbacks (noVNC link / periodic screenshots) available without code changes.
|
|
*/
|
|
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 {
|
|
readonly kind: AppConfig["streamBackend"];
|
|
/** Start broadcasting. Returns a short user-facing status/link message. */
|
|
start(ctx: StreamContext): Promise<string>;
|
|
stop(): Promise<void>;
|
|
isActive(): boolean;
|
|
}
|
|
|
|
export async function createStreamer(config: AppConfig): Promise<ScreenStreamer> {
|
|
switch (config.streamBackend) {
|
|
case "selfbot": {
|
|
const { SelfbotStreamer } = await import("./selfbot.ts");
|
|
return new SelfbotStreamer(config);
|
|
}
|
|
case "novnc": {
|
|
const { NoVncStreamer } = await import("./novnc.ts");
|
|
return new NoVncStreamer(config);
|
|
}
|
|
case "screenshot": {
|
|
const { ScreenshotStreamer } = await import("./screenshot.ts");
|
|
return new ScreenshotStreamer(config);
|
|
}
|
|
case "none":
|
|
default:
|
|
return {
|
|
kind: "none",
|
|
async start() {
|
|
return "화면 송출이 비활성화되어 있습니다 (STREAM_BACKEND=none).";
|
|
},
|
|
async stop() {},
|
|
isActive: () => false,
|
|
};
|
|
}
|
|
}
|