@bililive-tools/manager
Version:
Batch scheduling recorders
205 lines (204 loc) • 6.48 kB
JavaScript
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.");
}
}