@bililive-tools/manager 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { FFMPEGRecorder } from "./FFMPEGRecorder.js";
2
+ import { mesioRecorder } from "./mesioRecorder.js";
3
+ export { FFMPEGRecorder } from "./FFMPEGRecorder.js";
4
+ export { mesioRecorder } from "./mesioRecorder.js";
5
+ import type { IRecorder, FFMPEGRecorderOptions, MesioRecorderOptions } from "./IRecorder.js";
6
+ /**
7
+ * 录制器类型
8
+ */
9
+ export type RecorderType = "ffmpeg" | "mesio";
10
+ /**
11
+ * 根据录制器类型获取对应的配置选项类型
12
+ */
13
+ export type RecorderOptions<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorderOptions : MesioRecorderOptions;
14
+ /**
15
+ * 根据录制器类型获取对应的录制器实例类型
16
+ */
17
+ export type RecorderInstance<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorder : mesioRecorder;
18
+ /**
19
+ * 创建录制器的工厂函数
20
+ */
21
+ export declare function createBaseRecorder<T extends RecorderType>(type: T, opts: RecorderOptions<T> & {
22
+ mesioOptions?: string[];
23
+ }, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
24
+ title?: string;
25
+ cover?: string;
26
+ }>): IRecorder;
@@ -0,0 +1,18 @@
1
+ import { FFMPEGRecorder } from "./FFMPEGRecorder.js";
2
+ import { mesioRecorder } from "./mesioRecorder.js";
3
+ export { FFMPEGRecorder } from "./FFMPEGRecorder.js";
4
+ export { mesioRecorder } from "./mesioRecorder.js";
5
+ /**
6
+ * 创建录制器的工厂函数
7
+ */
8
+ export function createBaseRecorder(type, opts, onEnd, onUpdateLiveInfo) {
9
+ if (type === "ffmpeg") {
10
+ return new FFMPEGRecorder(opts, onEnd, onUpdateLiveInfo);
11
+ }
12
+ else if (type === "mesio") {
13
+ return new mesioRecorder({ ...opts, inputOptions: opts.mesioOptions ?? [] }, onEnd, onUpdateLiveInfo);
14
+ }
15
+ else {
16
+ throw new Error(`Unsupported recorder type: ${type}`);
17
+ }
18
+ }
@@ -0,0 +1,46 @@
1
+ import EventEmitter from "node:events";
2
+ import { IRecorder, MesioRecorderOptions } from "./IRecorder.js";
3
+ declare class MesioCommand extends EventEmitter {
4
+ private _input;
5
+ private _output;
6
+ private _inputOptions;
7
+ private process;
8
+ constructor();
9
+ input(source: string): MesioCommand;
10
+ output(target: string): MesioCommand;
11
+ inputOptions(options: string[]): MesioCommand;
12
+ inputOptions(...options: string[]): MesioCommand;
13
+ _getArguments(): string[];
14
+ run(): void;
15
+ kill(signal?: NodeJS.Signals): void;
16
+ }
17
+ export declare const createMesioBuilder: () => MesioCommand;
18
+ export declare class mesioRecorder extends EventEmitter implements IRecorder {
19
+ private onEnd;
20
+ private onUpdateLiveInfo;
21
+ private command;
22
+ private streamManager;
23
+ readonly hasSegment: boolean;
24
+ readonly getSavePath: (data: {
25
+ startTime: number;
26
+ title?: string;
27
+ }) => string;
28
+ readonly segment: number;
29
+ readonly inputOptions: string[];
30
+ readonly isHls: boolean;
31
+ readonly disableDanma: boolean;
32
+ readonly url: string;
33
+ readonly headers: {
34
+ [key: string]: string | undefined;
35
+ } | undefined;
36
+ constructor(opts: MesioRecorderOptions, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
37
+ title?: string;
38
+ cover?: string;
39
+ }>);
40
+ createCommand(): MesioCommand;
41
+ run(): void;
42
+ getArguments(): string[];
43
+ stop(): Promise<void>;
44
+ getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
45
+ }
46
+ export {};
@@ -0,0 +1,195 @@
1
+ import path from "node:path";
2
+ import EventEmitter from "node:events";
3
+ import { spawn } from "node:child_process";
4
+ import { StreamManager, getMesioPath } from "../index.js";
5
+ // Mesio command builder class similar to ffmpeg
6
+ class MesioCommand extends EventEmitter {
7
+ _input = "";
8
+ _output = "";
9
+ _inputOptions = [];
10
+ process = null;
11
+ constructor() {
12
+ super();
13
+ }
14
+ input(source) {
15
+ this._input = source;
16
+ return this;
17
+ }
18
+ output(target) {
19
+ this._output = target;
20
+ return this;
21
+ }
22
+ inputOptions(...options) {
23
+ const opts = Array.isArray(options[0]) ? options[0] : options;
24
+ this._inputOptions.push(...opts);
25
+ return this;
26
+ }
27
+ _getArguments() {
28
+ const args = [];
29
+ // Add input options first
30
+ args.push(...this._inputOptions);
31
+ // Add output target
32
+ if (this._output) {
33
+ const { dir, name } = path.parse(this._output);
34
+ args.push("-o", dir);
35
+ args.push("-n", name);
36
+ }
37
+ // args.push("-v");
38
+ // Add input source
39
+ if (this._input) {
40
+ args.push(this._input);
41
+ }
42
+ return args;
43
+ }
44
+ run() {
45
+ const args = this._getArguments();
46
+ const mesioExecutable = getMesioPath();
47
+ this.process = spawn(mesioExecutable, args, {
48
+ stdio: ["pipe", "pipe", "pipe"],
49
+ });
50
+ if (this.process.stdout) {
51
+ this.process.stdout.on("data", (data) => {
52
+ const output = data.toString();
53
+ // console.log(output);
54
+ this.emit("stderr", output);
55
+ });
56
+ }
57
+ if (this.process.stderr) {
58
+ this.process.stderr.on("data", (data) => {
59
+ const output = data.toString();
60
+ // console.error(output);
61
+ this.emit("stderr", output);
62
+ });
63
+ }
64
+ this.process.on("error", (error) => {
65
+ this.emit("error", error);
66
+ });
67
+ [];
68
+ this.process.on("close", (code) => {
69
+ if (code === 0) {
70
+ this.emit("end");
71
+ }
72
+ else {
73
+ this.emit("error", new Error(`mesio process exited with code ${code}`));
74
+ }
75
+ });
76
+ }
77
+ kill(signal = "SIGTERM") {
78
+ if (this.process) {
79
+ this.process.kill(signal);
80
+ }
81
+ }
82
+ }
83
+ // Factory function similar to createFFMPEGBuilder
84
+ export const createMesioBuilder = () => {
85
+ return new MesioCommand();
86
+ };
87
+ export class mesioRecorder extends EventEmitter {
88
+ onEnd;
89
+ onUpdateLiveInfo;
90
+ command;
91
+ streamManager;
92
+ hasSegment;
93
+ getSavePath;
94
+ segment;
95
+ inputOptions = [];
96
+ isHls;
97
+ disableDanma = false;
98
+ url;
99
+ headers;
100
+ constructor(opts, onEnd, onUpdateLiveInfo) {
101
+ super();
102
+ this.onEnd = onEnd;
103
+ this.onUpdateLiveInfo = onUpdateLiveInfo;
104
+ const hasSegment = true;
105
+ this.disableDanma = opts.disableDanma ?? false;
106
+ let videoFormat = "flv";
107
+ if (opts.url.includes(".m3u8")) {
108
+ videoFormat = "ts";
109
+ }
110
+ if (opts.formatName) {
111
+ if (opts.formatName === "fmp4") {
112
+ videoFormat = "m4s";
113
+ }
114
+ else if (opts.formatName === "ts") {
115
+ videoFormat = "ts";
116
+ }
117
+ else if (opts.formatName === "flv") {
118
+ videoFormat = "flv";
119
+ }
120
+ }
121
+ this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "mesio", videoFormat, {
122
+ onUpdateLiveInfo: this.onUpdateLiveInfo,
123
+ });
124
+ this.hasSegment = hasSegment;
125
+ this.getSavePath = opts.getSavePath;
126
+ this.inputOptions = opts.inputOptions ?? [];
127
+ this.url = opts.url;
128
+ this.segment = opts.segment;
129
+ this.headers = opts.headers;
130
+ if (opts.isHls === undefined) {
131
+ this.isHls = this.url.includes("m3u8");
132
+ }
133
+ else {
134
+ this.isHls = opts.isHls;
135
+ }
136
+ this.command = this.createCommand();
137
+ this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
138
+ this.emit("videoFileCreated", { filename, cover });
139
+ });
140
+ this.streamManager.on("videoFileCompleted", ({ filename }) => {
141
+ this.emit("videoFileCompleted", { filename });
142
+ });
143
+ this.streamManager.on("DebugLog", (data) => {
144
+ this.emit("DebugLog", data);
145
+ });
146
+ }
147
+ createCommand() {
148
+ const inputOptions = [
149
+ ...this.inputOptions,
150
+ "--fix",
151
+ "-H",
152
+ "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",
153
+ ];
154
+ if (this.headers) {
155
+ Object.entries(this.headers).forEach(([key, value]) => {
156
+ if (!value)
157
+ return;
158
+ inputOptions.push("-H", `${key}: ${value}`);
159
+ });
160
+ }
161
+ if (this.hasSegment) {
162
+ inputOptions.push("-d", `${this.segment * 60}s`);
163
+ }
164
+ const command = createMesioBuilder()
165
+ .input(this.url)
166
+ .inputOptions(inputOptions)
167
+ .output(this.streamManager.videoFilePath)
168
+ .on("error", this.onEnd)
169
+ .on("end", () => this.onEnd("finished"))
170
+ .on("stderr", async (stderrLine) => {
171
+ await this.streamManager.handleVideoStarted(stderrLine);
172
+ this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
173
+ });
174
+ return command;
175
+ }
176
+ run() {
177
+ this.command.run();
178
+ }
179
+ getArguments() {
180
+ return this.command._getArguments();
181
+ }
182
+ async stop() {
183
+ try {
184
+ // 直接发送SIGINT信号,会导致数据丢失
185
+ this.command.kill("SIGINT");
186
+ await this.streamManager.handleVideoCompleted();
187
+ }
188
+ catch (err) {
189
+ this.emit("DebugLog", { type: "error", text: String(err) });
190
+ }
191
+ }
192
+ getExtraDataController() {
193
+ return this.streamManager?.getExtraDataController();
194
+ }
195
+ }
@@ -1,20 +1,23 @@
1
1
  import EventEmitter from "node:events";
2
- import { createRecordExtraDataController } from "./record_extra_data_controller.js";
2
+ import { createRecordExtraDataController } from "../xml_stream_controller.js";
3
+ import type { RecorderCreateOpts } from "../recorder.js";
3
4
  export type GetSavePath = (data: {
4
5
  startTime: number;
5
6
  title?: string;
6
7
  }) => string;
8
+ type RecorderType = Exclude<RecorderCreateOpts["recorderType"], undefined | "auto">;
9
+ type VideoFormat = "auto" | "ts" | "mkv" | "flv" | "mp4" | "m4s";
7
10
  export declare class Segment extends EventEmitter {
8
11
  extraDataController: ReturnType<typeof createRecordExtraDataController> | null;
9
12
  init: boolean;
10
13
  getSavePath: GetSavePath;
11
- /** 原始的ffmpeg文件名,用于重命名 */
14
+ /** 原始的文件名,用于重命名 */
12
15
  rawRecordingVideoPath: string;
13
16
  /** 输出文件名名,不包含拓展名 */
14
17
  outputVideoFilePath: string;
15
18
  disableDanma: boolean;
16
- videoExt: "ts" | "mkv" | "mp4";
17
- constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: "ts" | "mkv" | "mp4");
19
+ videoExt: VideoFormat;
20
+ constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: VideoFormat);
18
21
  handleSegmentEnd(): Promise<void>;
19
22
  onSegmentStart(stderrLine: string, callBack?: {
20
23
  onUpdateLiveInfo: () => Promise<{
@@ -30,9 +33,10 @@ export declare class StreamManager extends EventEmitter {
30
33
  recordSavePath: string;
31
34
  recordStartTime?: number;
32
35
  hasSegment: boolean;
33
- private videoFormat?;
36
+ recorderType: RecorderType;
37
+ private videoFormat;
34
38
  private callBack?;
35
- constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, videoFormat?: "auto" | "ts" | "mkv", callBack?: {
39
+ constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, recorderType: RecorderType, videoFormat: VideoFormat, callBack?: {
36
40
  onUpdateLiveInfo: () => Promise<{
37
41
  title?: string;
38
42
  cover?: string;
@@ -40,7 +44,8 @@ export declare class StreamManager extends EventEmitter {
40
44
  });
41
45
  handleVideoStarted(stderrLine: string): Promise<void>;
42
46
  handleVideoCompleted(): Promise<void>;
43
- getExtraDataController(): import("./record_extra_data_controller.js").RecordExtraDataController | null;
44
- get videoExt(): "ts" | "mkv" | "mp4";
47
+ getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
48
+ get videoExt(): VideoFormat;
45
49
  get videoFilePath(): string;
46
50
  }
51
+ export {};
@@ -1,12 +1,12 @@
1
1
  import EventEmitter from "node:events";
2
2
  import fs from "fs/promises";
3
- import { createRecordExtraDataController } from "./record_extra_data_controller.js";
4
- import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isFfmpegStart, retry, } from "./utils.js";
3
+ import { createRecordExtraDataController } from "../xml_stream_controller.js";
4
+ import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isMesioStartSegment, isFfmpegStart, retry, cleanTerminalText, } from "../utils.js";
5
5
  export class Segment extends EventEmitter {
6
6
  extraDataController = null;
7
7
  init = true;
8
8
  getSavePath;
9
- /** 原始的ffmpeg文件名,用于重命名 */
9
+ /** 原始的文件名,用于重命名 */
10
10
  rawRecordingVideoPath;
11
11
  /** 输出文件名名,不包含拓展名 */
12
12
  outputVideoFilePath;
@@ -64,10 +64,17 @@ export class Segment extends EventEmitter {
64
64
  });
65
65
  ensureFolderExist(this.outputVideoFilePath);
66
66
  if (!this.disableDanma) {
67
- this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.json`);
67
+ this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.xml`);
68
+ }
69
+ // 支持两种格式的正则表达式
70
+ // 1. FFmpeg格式: Opening 'filename' for writing
71
+ // 2. Mesio格式: Opening FLV segment path=filename Processing
72
+ const ffmpegRegex = /'([^']+)'/;
73
+ const mesioRegex = /segment path=(.+?\.(?:flv|ts|m4s))/is;
74
+ let match = stderrLine.match(ffmpegRegex);
75
+ if (!match) {
76
+ match = cleanTerminalText(stderrLine).match(mesioRegex);
68
77
  }
69
- const regex = /'([^']+)'/;
70
- const match = stderrLine.match(regex);
71
78
  if (match) {
72
79
  const filename = match[1];
73
80
  this.rawRecordingVideoPath = filename;
@@ -91,13 +98,15 @@ export class StreamManager extends EventEmitter {
91
98
  recordSavePath;
92
99
  recordStartTime;
93
100
  hasSegment;
101
+ recorderType = "ffmpeg";
94
102
  videoFormat;
95
103
  callBack;
96
- constructor(getSavePath, hasSegment, disableDanma, videoFormat, callBack) {
104
+ constructor(getSavePath, hasSegment, disableDanma, recorderType, videoFormat, callBack) {
97
105
  super();
98
106
  const recordSavePath = getSavePath({ startTime: Date.now() });
99
107
  this.recordSavePath = recordSavePath;
100
- this.videoFormat = videoFormat;
108
+ this.videoFormat = videoFormat ?? "auto";
109
+ this.recorderType = recorderType;
101
110
  this.hasSegment = hasSegment;
102
111
  this.callBack = callBack;
103
112
  if (hasSegment) {
@@ -113,36 +122,50 @@ export class StreamManager extends EventEmitter {
113
122
  });
114
123
  }
115
124
  else {
116
- const extraDataSavePath = replaceExtName(recordSavePath, ".json");
125
+ const extraDataSavePath = replaceExtName(recordSavePath, ".xml");
117
126
  if (!disableDanma) {
118
127
  this.extraDataController = createRecordExtraDataController(extraDataSavePath);
119
128
  }
120
129
  }
121
130
  }
122
131
  async handleVideoStarted(stderrLine) {
123
- if (this.segment) {
124
- if (isFfmpegStartSegment(stderrLine)) {
125
- await this.segment.onSegmentStart(stderrLine, this.callBack);
132
+ if (this.recorderType === "ffmpeg") {
133
+ if (this.segment) {
134
+ if (isFfmpegStartSegment(stderrLine)) {
135
+ await this.segment.onSegmentStart(stderrLine, this.callBack);
136
+ }
137
+ }
138
+ else {
139
+ // 不能直接在onStart回调进行判断,在某些情况下会链接无法录制的情况
140
+ if (isFfmpegStart(stderrLine)) {
141
+ if (this.recordStartTime)
142
+ return;
143
+ this.recordStartTime = Date.now();
144
+ this.emit("videoFileCreated", { filename: this.videoFilePath });
145
+ }
126
146
  }
127
147
  }
128
- else {
129
- // 不能直接在onStart回调进行判断,在某些情况下会链接无法录制的情况
130
- if (isFfmpegStart(stderrLine)) {
131
- if (this.recordStartTime)
132
- return;
133
- this.recordStartTime = Date.now();
134
- this.emit("videoFileCreated", { filename: this.videoFilePath });
148
+ else if (this.recorderType === "mesio") {
149
+ if (this.segment && isMesioStartSegment(stderrLine)) {
150
+ await this.segment.onSegmentStart(stderrLine, this.callBack);
135
151
  }
136
152
  }
137
153
  }
138
154
  async handleVideoCompleted() {
139
- if (this.segment) {
140
- await this.segment.handleSegmentEnd();
155
+ if (this.recorderType === "ffmpeg") {
156
+ if (this.segment) {
157
+ await this.segment.handleSegmentEnd();
158
+ }
159
+ else {
160
+ if (this.recordStartTime) {
161
+ await this.getExtraDataController()?.flush();
162
+ this.emit("videoFileCompleted", { filename: this.videoFilePath });
163
+ }
164
+ }
141
165
  }
142
- else {
143
- if (this.recordStartTime) {
144
- await this.getExtraDataController()?.flush();
145
- this.emit("videoFileCompleted", { filename: this.videoFilePath });
166
+ else if (this.recorderType === "mesio") {
167
+ if (this.segment) {
168
+ await this.segment.handleSegmentEnd();
146
169
  }
147
170
  }
148
171
  }
@@ -150,19 +173,25 @@ export class StreamManager extends EventEmitter {
150
173
  return this.segment?.extraDataController || this.extraDataController;
151
174
  }
152
175
  get videoExt() {
153
- if (this.videoFormat === "mkv") {
154
- return "mkv";
176
+ if (this.recorderType === "ffmpeg") {
177
+ return this.videoFormat;
155
178
  }
156
- else if (this.videoFormat === "auto") {
157
- if (!this.hasSegment) {
158
- return "mp4";
159
- }
179
+ else if (this.recorderType === "mesio") {
180
+ return this.videoFormat;
181
+ }
182
+ else {
183
+ throw new Error("Unknown recorderType");
160
184
  }
161
- return "ts";
162
185
  }
163
186
  get videoFilePath() {
164
- return this.segment
165
- ? `${this.recordSavePath}-PART%03d.${this.videoExt}`
166
- : `${this.recordSavePath}.${this.videoExt}`;
187
+ if (this.recorderType === "ffmpeg") {
188
+ return this.segment
189
+ ? `${this.recordSavePath}-PART%03d.${this.videoExt}`
190
+ : `${this.recordSavePath}.${this.videoExt}`;
191
+ }
192
+ else if (this.recorderType === "mesio") {
193
+ return `${this.recordSavePath}-PART%i.${this.videoExt}`;
194
+ }
195
+ return `${this.recordSavePath}.${this.videoExt}`;
167
196
  }
168
197
  }
package/lib/recorder.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Emitter } from "mitt";
2
2
  import { ChannelId, Message, Quality } from "./common.js";
3
3
  import { RecorderProvider } from "./manager.js";
4
4
  import { AnyObject, PickRequired, UnknownObject } from "./utils.js";
5
+ import { Cache } from "./cache.js";
5
6
  type FormatName = "auto" | "flv" | "hls" | "fmp4" | "flv_only" | "hls_only" | "fmp4_only";
6
7
  type CodecName = "auto" | "avc" | "hevc" | "avc_only" | "hevc_only";
7
8
  export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
@@ -9,6 +10,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
9
10
  channelId: ChannelId;
10
11
  id?: string;
11
12
  remarks?: string;
13
+ weight?: number;
12
14
  disableAutoCheck?: boolean;
13
15
  disableProvideCommentsWhenRecording?: boolean;
14
16
  quality: Quality;
@@ -24,7 +26,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
24
26
  /** 身份验证 */
25
27
  auth?: string;
26
28
  /** cookie所有者uid,B站弹幕录制 */
27
- uid?: number;
29
+ uid?: number | string;
28
30
  /** 画质匹配重试次数 */
29
31
  qualityRetry?: number;
30
32
  /** 抖音是否使用双屏直播流,开启后如果是双屏直播,那么就使用拼接的流,默认为true */
@@ -37,12 +39,14 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
37
39
  formatName?: FormatName;
38
40
  /** 流编码 */
39
41
  codecName?: CodecName;
40
- /** 选择使用的api,虎牙支持 */
41
- api?: "auto" | "web" | "mp";
42
+ /** 选择使用的api,虎牙支持: auto,web,mp,抖音支持:web,webHTML,mobile,userHTML */
43
+ api?: "auto" | "web" | "mp" | "webHTML" | "mobile" | "userHTML";
42
44
  /** 标题关键词,如果直播间标题包含这些关键词,则不会自动录制(仅对斗鱼有效),多个关键词用英文逗号分隔 */
43
45
  titleKeywords?: string;
44
46
  /** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
45
47
  videoFormat?: "auto" | "ts" | "mkv";
48
+ /** 录制类型 */
49
+ recorderType?: "auto" | "ffmpeg" | "mesio";
46
50
  /** 流格式优先级 */
47
51
  formatriorities?: Array<"flv" | "hls">;
48
52
  /** 只录制音频 */
@@ -52,9 +56,10 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
52
56
  /** 控制弹幕是否使用服务端时间戳 */
53
57
  useServerTimestamp?: boolean;
54
58
  extra?: Partial<E>;
59
+ cache: Cache;
55
60
  }
56
- export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id">;
57
- export type RecorderState = "idle" | "recording" | "stopping-record";
61
+ export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id"> & Pick<Recorder<E>, "id" | "channelId" | "remarks" | "disableAutoCheck" | "quality" | "streamPriorities" | "sourcePriorities" | "extra" | "segment" | "saveSCDanma" | "saveCover" | "saveGiftDanma" | "disableProvideCommentsWhenRecording" | "liveInfo" | "uid" | "titleKeywords">;
62
+ export type RecorderState = "idle" | "recording" | "stopping-record" | "check-error";
58
63
  export type Progress = {
59
64
  time: string | null;
60
65
  };
@@ -106,7 +111,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
106
111
  state: RecorderState;
107
112
  qualityMaxRetry: number;
108
113
  qualityRetry: number;
109
- uid?: number;
114
+ uid?: number | string;
110
115
  liveInfo?: {
111
116
  living: boolean;
112
117
  owner: string;
@@ -117,6 +122,8 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
117
122
  liveId?: string;
118
123
  };
119
124
  tempStopIntervalCheck?: boolean;
125
+ /** 缓存实例引用,由 manager 设置 */
126
+ cache: Cache;
120
127
  getChannelURL: (this: Recorder<E>) => string;
121
128
  checkLiveStatusAndRecord: (this: Recorder<E>, opts: {
122
129
  getSavePath: GetSavePath;
package/lib/utils.d.ts CHANGED
@@ -36,9 +36,19 @@ export declare function assertObjectType(data: unknown, msg?: string): asserts d
36
36
  export declare function formatDate(date: Date, format: string): string;
37
37
  export declare function removeSystemReservedChars(filename: string): string;
38
38
  export declare function isFfmpegStartSegment(line: string): boolean;
39
+ export declare function isMesioStartSegment(line: string): boolean;
39
40
  export declare function isFfmpegStart(line: string): boolean;
41
+ export declare function cleanTerminalText(text: string): string;
40
42
  export declare const formatTemplate: (string: string, ...args: any[]) => string;
41
- export declare function createInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => boolean;
43
+ /**
44
+ * 检查ffmpeg无效流
45
+ * @param count 连续多少次帧数不变就判定为无效流
46
+ * @returns
47
+ * "receive repart stream": b站最后的无限流
48
+ * "receive invalid aac stream": ADTS无法被解析的flv流
49
+ * "invalid stream": 一段时间内帧数不变
50
+ */
51
+ export declare function createInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => [boolean, string];
42
52
  export declare function createTimeoutChecker(onTimeout: () => void, time: number, autoStart?: boolean): {
43
53
  update: () => void;
44
54
  stop: () => void;
package/lib/utils.js CHANGED
@@ -126,10 +126,16 @@ export function removeSystemReservedChars(filename) {
126
126
  export function isFfmpegStartSegment(line) {
127
127
  return line.includes("Opening ") && line.includes("for writing");
128
128
  }
129
+ export function isMesioStartSegment(line) {
130
+ return line.includes("Opening ") && line.includes("Opening segment");
131
+ }
129
132
  export function isFfmpegStart(line) {
130
133
  return ((line.includes("frame=") && line.includes("fps=")) ||
131
134
  (line.includes("speed=") && line.includes("time=")));
132
135
  }
136
+ export function cleanTerminalText(text) {
137
+ return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
138
+ }
133
139
  export const formatTemplate = function template(string, ...args) {
134
140
  const nargs = /\{([0-9a-zA-Z_]+)\}/g;
135
141
  let params;
@@ -156,13 +162,25 @@ export const formatTemplate = function template(string, ...args) {
156
162
  }
157
163
  });
158
164
  };
165
+ /**
166
+ * 检查ffmpeg无效流
167
+ * @param count 连续多少次帧数不变就判定为无效流
168
+ * @returns
169
+ * "receive repart stream": b站最后的无限流
170
+ * "receive invalid aac stream": ADTS无法被解析的flv流
171
+ * "invalid stream": 一段时间内帧数不变
172
+ */
159
173
  export function createInvalidStreamChecker(count = 15) {
160
174
  let prevFrame = 0;
161
175
  let frameUnchangedCount = 0;
162
176
  return (ffmpegLogLine) => {
163
177
  // B站某些cdn在直播结束后仍会返回一些数据 https://github.com/renmu123/biliLive-tools/issues/123
164
178
  if (ffmpegLogLine.includes("New subtitle stream with index")) {
165
- return true;
179
+ return [true, "receive repart stream"];
180
+ }
181
+ // 虎牙某些cdn会返回无法解析ADTS的flv流 https://github.com/renmu123/biliLive-tools/issues/150
182
+ if (ffmpegLogLine.includes("AAC bitstream not in ADTS format and extradata missing")) {
183
+ return [true, "receive invalid aac stream"];
166
184
  }
167
185
  const streamInfo = ffmpegLogLine.match(/frame=\s*(\d+) fps=.*? q=.*? size=.*? time=.*? bitrate=.*? speed=.*?/);
168
186
  if (streamInfo != null) {
@@ -170,16 +188,16 @@ export function createInvalidStreamChecker(count = 15) {
170
188
  const frame = Number(frameText);
171
189
  if (frame === prevFrame) {
172
190
  if (++frameUnchangedCount >= count) {
173
- return true;
191
+ return [true, "invalid stream"];
174
192
  }
175
193
  }
176
194
  else {
177
195
  prevFrame = frame;
178
196
  frameUnchangedCount = 0;
179
197
  }
180
- return false;
198
+ return [false, ""];
181
199
  }
182
- return false;
200
+ return [false, ""];
183
201
  };
184
202
  }
185
203
  export function createTimeoutChecker(onTimeout, time, autoStart = true) {