Files
dc-recorder/src/streaming/index.ts
T
2026-05-17 19:39:46 +07:00

160 lines
4.3 KiB
TypeScript

import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import type { Readable } from "node:stream";
import type { ChildProcess } from "node:child_process";
import type { Client } from "discord.js-selfbot-v13";
import {
Streamer as DankStreamer,
prepareStream as dankPrepareStream,
playStream as dankPlayStream,
Utils,
Encoders,
} from "@dank074/discord-video-stream";
type VoiceConnectionLike = any;
type StreamConnectionLike = any;
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;
}
export const UtilsAPI = {
normalizeVideoCodec: (c: string) => c.toUpperCase?.() ?? c,
};
export class Streamer {
client: Client;
dankStreamer: DankStreamer;
constructor(client: Client) {
this.client = client;
this.dankStreamer = new DankStreamer(client);
}
async createSession(guildId: string, channelId: string): Promise<StreamSession> {
await this.dankStreamer.joinVoice(guildId, channelId);
let stopped = false;
let currentCommand: any = null;
const stop = () => {
if (stopped) return;
stopped = true;
try {
if (currentCommand?.kill) currentCommand.kill("SIGKILL");
} catch (e) {
// ignore
}
this.dankStreamer.stopStream();
this.dankStreamer.leaveVoice();
};
return {
connection: {} as any,
stream: {} as any,
play: async (source: string | Readable, options: StreamPlayOptions = {}) => {
if (stopped) return;
let targetSource: string | Readable = source;
if (typeof source === "string" && source.includes("\n")) {
const urls = source.split("\n").filter((u) => u.trim());
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
}
};
output.on("data", onData);
command.on("error", (err: Error) => {
console.error("Transcoder error:", err);
});
try {
await dankPlayStream(output, this.dankStreamer, {
type: "go-live",
width: 1280,
height: 720,
frameRate: fps,
});
} finally {
output.off("data", onData);
stop();
}
},
stop,
};
}
}
export function prepareStream(source: string, _options: any): any {
return { command: null, output: new PassThrough() };
}
export async function playStream(): Promise<void> {
return;
}
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> {
await session.play(source, options);
}
export async function playTranscodedPreparedStream(
source: string | Readable,
session: StreamSession,
options: StreamPlayOptions = {},
): Promise<void> {
await session.play(source, options);
}