UNPKG

@bililive-tools/manager

Version:
109 lines (108 loc) 3.86 kB
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(); } }