2026-05-17 05:15:38 +07:00
|
|
|
import { EventEmitter } from "node:events";
|
2026-05-17 05:10:46 +07:00
|
|
|
import { PassThrough } from "node:stream";
|
|
|
|
|
import type { Readable } from "node:stream";
|
2026-05-17 17:35:37 +07:00
|
|
|
import type { ChildProcess } from "node:child_process";
|
2026-05-17 05:10:46 +07:00
|
|
|
import type { Client } from "discord.js-selfbot-v13";
|
2026-05-17 19:39:46 +07:00
|
|
|
import {
|
|
|
|
|
Streamer as DankStreamer,
|
|
|
|
|
prepareStream as dankPrepareStream,
|
|
|
|
|
playStream as dankPlayStream,
|
|
|
|
|
Utils,
|
|
|
|
|
Encoders,
|
|
|
|
|
} from "@dank074/discord-video-stream";
|
2026-05-17 05:10:46 +07:00
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
type VoiceConnectionLike = any;
|
|
|
|
|
type StreamConnectionLike = any;
|
2026-05-17 05:15:38 +07:00
|
|
|
|
|
|
|
|
export interface StreamPlayOptions {
|
|
|
|
|
fps?: number;
|
|
|
|
|
bitrate?: number | string;
|
|
|
|
|
includeAudio?: boolean;
|
|
|
|
|
presetH26x?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface StreamSession {
|
|
|
|
|
connection: VoiceConnectionLike;
|
|
|
|
|
stream: StreamConnectionLike;
|
|
|
|
|
play(source: string | Readable, options?: StreamPlayOptions): Promise<void>;
|
|
|
|
|
stop(): void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
export const UtilsAPI = {
|
2026-05-17 05:10:46 +07:00
|
|
|
normalizeVideoCodec: (c: string) => c.toUpperCase?.() ?? c,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class Streamer {
|
|
|
|
|
client: Client;
|
2026-05-17 19:39:46 +07:00
|
|
|
dankStreamer: DankStreamer;
|
|
|
|
|
|
2026-05-17 05:10:46 +07:00
|
|
|
constructor(client: Client) {
|
|
|
|
|
this.client = client;
|
2026-05-17 19:39:46 +07:00
|
|
|
this.dankStreamer = new DankStreamer(client);
|
2026-05-17 05:15:38 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createSession(guildId: string, channelId: string): Promise<StreamSession> {
|
2026-05-17 19:39:46 +07:00
|
|
|
await this.dankStreamer.joinVoice(guildId, channelId);
|
2026-05-17 05:15:38 +07:00
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
let stopped = false;
|
|
|
|
|
let currentCommand: any = null;
|
2026-05-17 05:15:38 +07:00
|
|
|
|
|
|
|
|
const stop = () => {
|
2026-05-17 19:39:46 +07:00
|
|
|
if (stopped) return;
|
|
|
|
|
stopped = true;
|
|
|
|
|
try {
|
|
|
|
|
if (currentCommand?.kill) currentCommand.kill("SIGKILL");
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
this.dankStreamer.stopStream();
|
|
|
|
|
this.dankStreamer.leaveVoice();
|
2026-05-17 05:15:38 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-17 19:39:46 +07:00
|
|
|
connection: {} as any,
|
|
|
|
|
stream: {} as any,
|
|
|
|
|
play: async (source: string | Readable, options: StreamPlayOptions = {}) => {
|
|
|
|
|
if (stopped) return;
|
2026-05-17 17:35:37 +07:00
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
let targetSource: string | Readable = source;
|
2026-05-17 17:35:37 +07:00
|
|
|
if (typeof source === "string" && source.includes("\n")) {
|
|
|
|
|
const urls = source.split("\n").filter((u) => u.trim());
|
2026-05-17 19:39:46 +07:00
|
|
|
targetSource = urls[0] ?? source;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fps = options.fps ?? 30;
|
|
|
|
|
const bitrateStr = String(options.bitrate ?? 2500).replace(/k$/i, "");
|
|
|
|
|
const bitrateVideo = parseInt(bitrateStr, 10) || 2500;
|
|
|
|
|
|
|
|
|
|
const { command, output } = dankPrepareStream(targetSource, {
|
|
|
|
|
encoder: Encoders.software({
|
|
|
|
|
x264: { preset: (options.presetH26x as any) ?? "superfast" },
|
|
|
|
|
x265: { preset: (options.presetH26x as any) ?? "superfast" },
|
|
|
|
|
}),
|
|
|
|
|
videoCodec: Utils.normalizeVideoCodec("H264"),
|
|
|
|
|
width: 1280,
|
|
|
|
|
height: 720,
|
|
|
|
|
bitrateVideo: bitrateVideo,
|
|
|
|
|
frameRate: fps,
|
|
|
|
|
includeAudio: options.includeAudio !== false,
|
|
|
|
|
customHeaders: {
|
|
|
|
|
"User-Agent":
|
|
|
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3",
|
|
|
|
|
Connection: "keep-alive",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
currentCommand = command;
|
|
|
|
|
|
|
|
|
|
const globalAny: any = globalThis;
|
|
|
|
|
const onData = (chunk: Buffer) => {
|
|
|
|
|
try {
|
|
|
|
|
globalAny.broadcastVideoToWeb?.(chunk);
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
2026-05-17 17:35:37 +07:00
|
|
|
}
|
2026-05-17 19:39:46 +07:00
|
|
|
};
|
|
|
|
|
output.on("data", onData);
|
2026-05-17 17:35:37 +07:00
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
command.on("error", (err: Error) => {
|
|
|
|
|
console.error("Transcoder error:", err);
|
|
|
|
|
});
|
2026-05-17 05:15:38 +07:00
|
|
|
|
|
|
|
|
try {
|
2026-05-17 19:39:46 +07:00
|
|
|
await dankPlayStream(output, this.dankStreamer, {
|
|
|
|
|
type: "go-live",
|
|
|
|
|
width: 1280,
|
|
|
|
|
height: 720,
|
|
|
|
|
frameRate: fps,
|
|
|
|
|
});
|
2026-05-17 05:15:38 +07:00
|
|
|
} finally {
|
2026-05-17 19:39:46 +07:00
|
|
|
output.off("data", onData);
|
2026-05-17 05:15:38 +07:00
|
|
|
stop();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
stop,
|
|
|
|
|
};
|
2026-05-17 05:10:46 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
export function prepareStream(source: string, _options: any): any {
|
|
|
|
|
return { command: null, output: new PassThrough() };
|
2026-05-17 05:10:46 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 19:39:46 +07:00
|
|
|
export async function playStream(): Promise<void> {
|
|
|
|
|
return;
|
2026-05-17 05:10:46 +07:00
|
|
|
}
|
2026-05-17 05:15:38 +07:00
|
|
|
|
|
|
|
|
export async function createStreamSession(
|
|
|
|
|
client: Client,
|
|
|
|
|
guildId: string,
|
|
|
|
|
channelId: string,
|
|
|
|
|
): Promise<StreamSession> {
|
|
|
|
|
return new Streamer(client).createSession(guildId, channelId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function playPreparedStream(
|
|
|
|
|
source: string | Readable,
|
|
|
|
|
session: StreamSession,
|
|
|
|
|
options: StreamPlayOptions = {},
|
|
|
|
|
): Promise<void> {
|
2026-05-17 17:35:37 +07:00
|
|
|
await session.play(source, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function playTranscodedPreparedStream(
|
|
|
|
|
source: string | Readable,
|
|
|
|
|
session: StreamSession,
|
|
|
|
|
options: StreamPlayOptions = {},
|
|
|
|
|
): Promise<void> {
|
2026-05-17 05:15:38 +07:00
|
|
|
await session.play(source, options);
|
|
|
|
|
}
|