@bililive-tools/manager
Version:
Batch scheduling recorders
109 lines (108 loc) • 3.86 kB
JavaScript
import EventEmitter from "node:events";
import { createFFMPEGBuilder, StreamManager, utils } from "./index.js";
import { createInvalidStreamChecker, assert } from "./utils.js";
export class FFMPEGRecorder extends EventEmitter {
onEnd;
command;
streamManager;
timeoutChecker;
hasSegment;
getSavePath;
segment;
ffmpegOutputOptions = [];
inputOptions = [];
isHls = false;
disableDanma = false;
url;
constructor(opts, onEnd) {
super();
this.onEnd = onEnd;
const hasSegment = !!opts.segment;
this.disableDanma = opts.disableDanma ?? false;
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat);
this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3);
this.hasSegment = hasSegment;
this.getSavePath = opts.getSavePath;
this.ffmpegOutputOptions = opts.outputOptions;
this.inputOptions = opts.inputOptions ?? [];
this.url = opts.url;
this.segment = opts.segment;
this.isHls = opts.isHls ?? false;
this.command = this.createCommand();
this.streamManager.on("videoFileCreated", ({ filename }) => {
this.emit("videoFileCreated", { filename });
});
this.streamManager.on("videoFileCompleted", ({ filename }) => {
this.emit("videoFileCompleted", { filename });
});
this.streamManager.on("DebugLog", (data) => {
this.emit("DebugLog", data);
});
}
createCommand() {
const invalidCount = this.isHls ? 35 : 15;
const isInvalidStream = createInvalidStreamChecker(invalidCount);
const command = createFFMPEGBuilder()
.input(this.url)
.inputOptions([
...this.inputOptions,
"-user_agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
])
.outputOptions(this.ffmpegOutputOptions)
.output(this.streamManager.videoFilePath)
.on("error", this.onEnd)
.on("end", () => this.onEnd("finished"))
.on("stderr", async (stderrLine) => {
assert(typeof stderrLine === "string");
await this.streamManager.handleVideoStarted(stderrLine);
// TODO:解析时间
this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
const info = this.formatLine(stderrLine);
if (info) {
this.emit("progress", info);
}
if (isInvalidStream(stderrLine)) {
this.onEnd("invalid stream");
}
})
.on("stderr", this.timeoutChecker.update);
if (this.hasSegment) {
command.outputOptions("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
}
return command;
}
formatLine(line) {
if (!line.includes("time=")) {
return null;
}
let time = null;
const timeMatch = line.match(/time=([0-9:.]+)/);
if (timeMatch) {
time = timeMatch[1];
}
return {
time,
};
}
run() {
this.command.run();
}
getArguments() {
return this.command._getArguments();
}
async stop() {
this.timeoutChecker.stop();
try {
// @ts-ignore
this.command.ffmpegProc?.stdin?.write("q");
await this.streamManager.handleVideoCompleted();
}
catch (err) {
this.emit("DebugLog", { type: "common", text: String(err) });
}
}
getExtraDataController() {
return this.streamManager?.getExtraDataController();
}
}