@bililive-tools/manager
Version:
Batch scheduling recorders
133 lines (132 loc) • 4.67 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;
onUpdateLiveInfo;
command;
streamManager;
timeoutChecker;
hasSegment;
getSavePath;
segment;
ffmpegOutputOptions = [];
inputOptions = [];
isHls;
disableDanma = false;
url;
headers;
constructor(opts, onEnd, onUpdateLiveInfo) {
super();
this.onEnd = onEnd;
this.onUpdateLiveInfo = onUpdateLiveInfo;
const hasSegment = !!opts.segment;
this.disableDanma = opts.disableDanma ?? false;
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat, {
onUpdateLiveInfo: this.onUpdateLiveInfo,
});
this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
this.hasSegment = hasSegment;
this.getSavePath = opts.getSavePath;
this.ffmpegOutputOptions = opts.outputOptions;
this.inputOptions = opts.inputOptions ?? [];
this.url = opts.url;
this.segment = opts.segment;
this.headers = opts.headers;
if (opts.isHls === undefined) {
this.isHls = this.url.includes("m3u8");
}
else {
this.isHls = opts.isHls;
}
this.command = this.createCommand();
this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
this.emit("videoFileCreated", { filename, cover });
});
this.streamManager.on("videoFileCompleted", ({ filename }) => {
this.emit("videoFileCompleted", { filename });
});
this.streamManager.on("DebugLog", (data) => {
this.emit("DebugLog", data);
});
}
createCommand() {
this.timeoutChecker?.start();
const invalidCount = this.isHls ? 35 : 15;
const isInvalidStream = createInvalidStreamChecker(invalidCount);
const 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",
];
if (this.headers) {
const headers = [];
Object.entries(this.headers).forEach(([key, value]) => {
if (!value)
return;
headers.push(`${key}:${value}`);
});
if (headers.length) {
inputOptions.push("-headers", headers.join("\\r\\n"));
}
}
const command = createFFMPEGBuilder()
.input(this.url)
.inputOptions(inputOptions)
.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);
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 {
this.command.kill("SIGINT");
// @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();
}
}