UNPKG

@bililive-tools/manager

Version:
205 lines (204 loc) 6.48 kB
import path from "node:path"; import EventEmitter from "node:events"; import { spawn } from "node:child_process"; import { DEFAULT_USER_AGENT } from "./index.js"; import { StreamManager, getMesioPath } from "../index.js"; class MesioCommand extends EventEmitter { _input = ""; _output = ""; _inputOptions = []; process = null; constructor() { super(); } input(source) { this._input = source; return this; } output(target) { this._output = target; return this; } inputOptions(...options) { const opts = Array.isArray(options[0]) ? options[0] : options; this._inputOptions.push(...opts); return this; } _getArguments() { const args = []; // Add input options first args.push(...this._inputOptions); // Add output target if (this._output) { const { dir, name } = path.parse(this._output); args.push("-o", dir); args.push("-n", name); } // args.push("-v"); // Add input source if (this._input) { args.push(this._input); } return args; } run() { const args = this._getArguments(); const mesioExecutable = getMesioPath(); this.process = spawn(mesioExecutable, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, }); if (this.process.stdout) { this.process.stdout.on("data", (data) => { const output = data.toString(); // console.log(output); this.emit("stderr", output); }); } if (this.process.stderr) { this.process.stderr.on("data", (data) => { const output = data.toString(); // console.error(output); this.emit("stderr", output); }); } this.process.on("error", (error) => { this.emit("error", error); }); []; this.process.on("close", (code) => { if (code === 0) { this.emit("end"); } else { this.emit("error", new Error(`mesio process exited with code ${code}`)); } }); } kill() { if (this.process) { this.process.stdin?.write("q"); this.process.stdin?.end(); } } } // Factory function similar to createFFMPEGBuilder export const createMesioBuilder = () => { return new MesioCommand(); }; export class mesioDownloader extends EventEmitter { onEnd; onUpdateLiveInfo; type = "mesio"; command; streamManager; hasSegment; getSavePath; segment; inputOptions = []; disableDanma = false; url; debugLevel = "none"; headers; constructor(opts, onEnd, onUpdateLiveInfo) { super(); this.onEnd = onEnd; this.onUpdateLiveInfo = onUpdateLiveInfo; // 存在自动分段,永远为true const hasSegment = true; this.hasSegment = hasSegment; this.disableDanma = opts.disableDanma ?? false; this.debugLevel = opts.debugLevel ?? "none"; let videoFormat = "flv"; if (opts.url.includes(".m3u8")) { videoFormat = "ts"; } if (opts.formatName === "fmp4") { videoFormat = "m4s"; } else if (opts.formatName === "ts") { videoFormat = "ts"; } else if (opts.formatName === "flv") { videoFormat = "flv"; } this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "mesio", videoFormat, { onUpdateLiveInfo: this.onUpdateLiveInfo, }); this.getSavePath = opts.getSavePath; this.inputOptions = []; this.url = opts.url; this.segment = opts.segment; this.headers = { "User-Agent": DEFAULT_USER_AGENT, ...(opts.headers || {}), }; this.command = this.createCommand(); this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => { this.emit("videoFileCreated", { filename, cover, rawFilename, title }); }); this.streamManager.on("videoFileCompleted", (data) => { this.emit("videoFileCompleted", data); }); this.streamManager.on("DebugLog", (data) => { this.emit("DebugLog", data); }); } createCommand() { const inputOptions = [...this.inputOptions, "--fix", "--no-proxy", "--disable-log-file"]; if (this.debugLevel === "verbose") { inputOptions.push("-v"); } if (this.headers) { Object.entries(this.headers).forEach(([key, value]) => { if (!value) return; inputOptions.push("-H", `${key}: ${value}`); }); } if (this.segment) { if (typeof this.segment === "number") { inputOptions.push("-d", `${this.segment * 60}s`); } else if (typeof this.segment === "string") { inputOptions.push("-m", this.segment); } } const command = createMesioBuilder() .input(this.url) .inputOptions(inputOptions) .output(this.streamManager.videoFilePath) .on("error", this.onEnd) .on("end", () => this.onEnd("finished")) .on("stderr", async (stderrLine) => { this.emit("DebugLog", { type: "ffmpeg", text: stderrLine }); await this.streamManager.handleVideoStarted(stderrLine); }); return command; } run() { this.command.run(); } getArguments() { return this.command._getArguments(); } async stop() { try { this.command.kill(); await new Promise((resolve) => setTimeout(resolve, 2000)); await this.streamManager.handleVideoCompleted(); } catch (err) { this.emit("DebugLog", { type: "error", text: String(err) }); } } getExtraDataController() { return this.streamManager?.getExtraDataController(); } get videoFilePath() { return this.streamManager.videoFilePath; } cut() { throw new Error("Mesio downloader does not support cut operation."); } }