@bililive-tools/manager 1.6.0 → 1.8.0

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.
@@ -1,80 +1,48 @@
1
1
  import { FFMPEGRecorder } from "./FFMPEGRecorder.js";
2
- import { mesioRecorder } from "./mesioRecorder.js";
2
+ import { MesioRecorder } from "./mesioRecorder.js";
3
+ import { BililiveRecorder } from "./BililiveRecorder.js";
3
4
  export { FFMPEGRecorder } from "./FFMPEGRecorder.js";
4
- export { mesioRecorder } from "./mesioRecorder.js";
5
+ export { MesioRecorder } from "./mesioRecorder.js";
6
+ export { BililiveRecorder } from "./BililiveRecorder.js";
7
+ import type { IRecorder, FFMPEGRecorderOptions, MesioRecorderOptions, BililiveRecorderOptions } from "./IRecorder.js";
5
8
  /**
6
9
  * 录制器类型
7
10
  */
8
- export type RecorderType = "ffmpeg" | "mesio";
11
+ export type RecorderType = "ffmpeg" | "mesio" | "bililive";
12
+ export type FormatName = "flv" | "ts" | "fmp4";
9
13
  /**
10
- * 录制器基础配置选项
14
+ * 根据录制器类型获取对应的配置选项类型
11
15
  */
12
- export interface BaseRecorderOptions {
13
- url: string;
14
- getSavePath: (data: {
15
- startTime: number;
16
- title?: string;
17
- }) => string;
18
- segment: number;
19
- inputOptions?: string[];
20
- disableDanma?: boolean;
21
- videoFormat?: "auto" | "ts" | "mkv";
22
- formatName?: "flv" | "ts" | "fmp4";
23
- headers?: {
24
- [key: string]: string | undefined;
25
- };
26
- }
16
+ export type RecorderOptions<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorderOptions : T extends "mesio" ? MesioRecorderOptions : BililiveRecorderOptions;
27
17
  /**
28
- * FFMPEG录制器配置选项
18
+ * 根据录制器类型获取对应的录制器实例类型
29
19
  */
30
- export interface FFMPEGRecorderOptions extends BaseRecorderOptions {
31
- outputOptions: string[];
32
- }
20
+ export type RecorderInstance<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorder : T extends "mesio" ? MesioRecorder : BililiveRecorder;
21
+ type RecorderOpts = FFMPEGRecorderOptions | MesioRecorderOptions | BililiveRecorderOptions;
33
22
  /**
34
- * Mesio录制器配置选项
23
+ * 创建录制器的工厂函数
35
24
  */
36
- export interface MesioRecorderOptions extends BaseRecorderOptions {
37
- }
25
+ export declare function createRecorder<T extends RecorderType>(type: T, opts: RecorderOptions<T> & {
26
+ onlyAudio?: boolean;
27
+ }, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
28
+ title?: string;
29
+ cover?: string;
30
+ }>): IRecorder;
38
31
  /**
39
- * 根据录制器类型获取对应的配置选项类型
32
+ * 选择录制器
40
33
  */
41
- export type RecorderOptions<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorderOptions : MesioRecorderOptions;
34
+ export declare function selectRecorder(preferredRecorder: "auto" | RecorderType | undefined): RecorderType;
42
35
  /**
43
- * 根据录制器类型获取对应的录制器实例类型
36
+ * 判断原始录制流格式,flv, ts, m4s
44
37
  */
45
- export type RecorderInstance<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorder : mesioRecorder;
38
+ export declare function getSourceFormatName(streamUrl: string, formatName: FormatName | undefined): FormatName;
39
+ type PickPartial<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>;
46
40
  /**
47
41
  * 创建录制器的工厂函数
48
- *
49
- * @example
50
- * ```typescript
51
- * // 创建 FFMPEG 录制器
52
- * const ffmpegRecorder = createRecorder("ffmpeg", {
53
- * url: "https://example.com/stream.m3u8",
54
- * getSavePath: ({ startTime, title }) => `/recordings/${title}_${startTime}.ts`,
55
- * segment: 30,
56
- * outputOptions: ["-c", "copy"],
57
- * inputOptions: ["-user_agent", "Custom-Agent"]
58
- * }, onEnd, onUpdateLiveInfo);
59
- *
60
- * // 创建 Mesio 录制器
61
- * const mesioRecorder = createRecorder("mesio", {
62
- * url: "https://example.com/stream.m3u8",
63
- * getSavePath: ({ startTime, title }) => `/recordings/${title}_${startTime}.ts`,
64
- * segment: 30,
65
- * inputOptions: ["--fix"]
66
- * }, onEnd, onUpdateLiveInfo);
67
- * ```
68
- *
69
- * @param type 录制器类型
70
- * @param opts 录制器配置选项
71
- * @param onEnd 录制结束回调
72
- * @param onUpdateLiveInfo 更新直播信息回调
73
- * @returns 对应类型的录制器实例
74
42
  */
75
- export declare function createBaseRecorder<T extends RecorderType>(type: T, opts: RecorderOptions<T> & {
76
- mesioOptions?: string[];
43
+ export declare function createBaseRecorder(type: "auto" | RecorderType | undefined, opts: PickPartial<RecorderOpts, "formatName"> & {
44
+ onlyAudio?: boolean;
77
45
  }, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
78
46
  title?: string;
79
47
  cover?: string;
80
- }>): RecorderInstance<T>;
48
+ }>): IRecorder;
@@ -1,44 +1,78 @@
1
1
  import { FFMPEGRecorder } from "./FFMPEGRecorder.js";
2
- import { mesioRecorder } from "./mesioRecorder.js";
2
+ import { MesioRecorder } from "./mesioRecorder.js";
3
+ import { BililiveRecorder } from "./BililiveRecorder.js";
3
4
  export { FFMPEGRecorder } from "./FFMPEGRecorder.js";
4
- export { mesioRecorder } from "./mesioRecorder.js";
5
+ export { MesioRecorder } from "./mesioRecorder.js";
6
+ export { BililiveRecorder } from "./BililiveRecorder.js";
5
7
  /**
6
8
  * 创建录制器的工厂函数
7
- *
8
- * @example
9
- * ```typescript
10
- * // 创建 FFMPEG 录制器
11
- * const ffmpegRecorder = createRecorder("ffmpeg", {
12
- * url: "https://example.com/stream.m3u8",
13
- * getSavePath: ({ startTime, title }) => `/recordings/${title}_${startTime}.ts`,
14
- * segment: 30,
15
- * outputOptions: ["-c", "copy"],
16
- * inputOptions: ["-user_agent", "Custom-Agent"]
17
- * }, onEnd, onUpdateLiveInfo);
18
- *
19
- * // 创建 Mesio 录制器
20
- * const mesioRecorder = createRecorder("mesio", {
21
- * url: "https://example.com/stream.m3u8",
22
- * getSavePath: ({ startTime, title }) => `/recordings/${title}_${startTime}.ts`,
23
- * segment: 30,
24
- * inputOptions: ["--fix"]
25
- * }, onEnd, onUpdateLiveInfo);
26
- * ```
27
- *
28
- * @param type 录制器类型
29
- * @param opts 录制器配置选项
30
- * @param onEnd 录制结束回调
31
- * @param onUpdateLiveInfo 更新直播信息回调
32
- * @returns 对应类型的录制器实例
33
9
  */
34
- export function createBaseRecorder(type, opts, onEnd, onUpdateLiveInfo) {
10
+ export function createRecorder(type, opts, onEnd, onUpdateLiveInfo) {
35
11
  if (type === "ffmpeg") {
36
12
  return new FFMPEGRecorder(opts, onEnd, onUpdateLiveInfo);
37
13
  }
38
14
  else if (type === "mesio") {
39
- return new mesioRecorder({ ...opts, inputOptions: opts.mesioOptions ?? [] }, onEnd, onUpdateLiveInfo);
15
+ return new MesioRecorder(opts, onEnd, onUpdateLiveInfo);
16
+ }
17
+ else if (type === "bililive") {
18
+ if (opts.formatName === "flv") {
19
+ // 录播姬引擎不支持只录音频
20
+ if (!opts.onlyAudio) {
21
+ return new BililiveRecorder(opts, onEnd, onUpdateLiveInfo);
22
+ }
23
+ }
24
+ return new FFMPEGRecorder(opts, onEnd, onUpdateLiveInfo);
40
25
  }
41
26
  else {
42
27
  throw new Error(`Unsupported recorder type: ${type}`);
43
28
  }
44
29
  }
30
+ /**
31
+ * 选择录制器
32
+ */
33
+ export function selectRecorder(preferredRecorder) {
34
+ let recorderType;
35
+ if (preferredRecorder === "auto") {
36
+ // 默认优先使用ffmpeg录制器
37
+ recorderType = "ffmpeg";
38
+ }
39
+ else if (preferredRecorder === "ffmpeg") {
40
+ recorderType = "ffmpeg";
41
+ }
42
+ else if (preferredRecorder === "mesio") {
43
+ recorderType = "mesio";
44
+ }
45
+ else if (preferredRecorder === "bililive") {
46
+ recorderType = "bililive";
47
+ }
48
+ else {
49
+ recorderType = "ffmpeg";
50
+ }
51
+ return recorderType;
52
+ }
53
+ /**
54
+ * 判断原始录制流格式,flv, ts, m4s
55
+ */
56
+ export function getSourceFormatName(streamUrl, formatName) {
57
+ if (formatName) {
58
+ return formatName;
59
+ }
60
+ if (streamUrl.includes(".m3u8")) {
61
+ return "ts";
62
+ }
63
+ else if (streamUrl.includes(".m4s")) {
64
+ // TODO: 使用b站的流进行测试
65
+ return "fmp4";
66
+ }
67
+ else {
68
+ return "flv";
69
+ }
70
+ }
71
+ /**
72
+ * 创建录制器的工厂函数
73
+ */
74
+ export function createBaseRecorder(type, opts, onEnd, onUpdateLiveInfo) {
75
+ const recorderType = selectRecorder(type);
76
+ const sourceFormatName = getSourceFormatName(opts.url, opts.formatName);
77
+ return createRecorder(recorderType, { ...opts, formatName: sourceFormatName }, onEnd, onUpdateLiveInfo);
78
+ }
@@ -1,4 +1,5 @@
1
1
  import EventEmitter from "node:events";
2
+ import { IRecorder, MesioRecorderOptions } from "./IRecorder.js";
2
3
  declare class MesioCommand extends EventEmitter {
3
4
  private _input;
4
5
  private _output;
@@ -14,40 +15,26 @@ declare class MesioCommand extends EventEmitter {
14
15
  kill(signal?: NodeJS.Signals): void;
15
16
  }
16
17
  export declare const createMesioBuilder: () => MesioCommand;
17
- export declare class mesioRecorder extends EventEmitter {
18
+ export declare class MesioRecorder extends EventEmitter implements IRecorder {
18
19
  private onEnd;
19
20
  private onUpdateLiveInfo;
21
+ type: "mesio";
20
22
  private command;
21
23
  private streamManager;
22
- hasSegment: boolean;
23
- getSavePath: (data: {
24
+ readonly hasSegment: boolean;
25
+ readonly getSavePath: (data: {
24
26
  startTime: number;
25
27
  title?: string;
26
28
  }) => string;
27
- segment: number;
28
- inputOptions: string[];
29
- isHls: boolean;
30
- disableDanma: boolean;
31
- url: string;
32
- headers: {
29
+ readonly segment: number;
30
+ readonly inputOptions: string[];
31
+ readonly disableDanma: boolean;
32
+ readonly url: string;
33
+ readonly debugLevel: "none" | "basic" | "verbose";
34
+ readonly headers: {
33
35
  [key: string]: string | undefined;
34
36
  } | undefined;
35
- constructor(opts: {
36
- url: string;
37
- getSavePath: (data: {
38
- startTime: number;
39
- title?: string;
40
- }) => string;
41
- segment: number;
42
- outputOptions?: string[];
43
- inputOptions?: string[];
44
- isHls?: boolean;
45
- disableDanma?: boolean;
46
- formatName?: "flv" | "ts" | "fmp4";
47
- headers?: {
48
- [key: string]: string | undefined;
49
- };
50
- }, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
37
+ constructor(opts: MesioRecorderOptions, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
51
38
  title?: string;
52
39
  cover?: string;
53
40
  }>);
@@ -55,6 +42,6 @@ export declare class mesioRecorder extends EventEmitter {
55
42
  run(): void;
56
43
  getArguments(): string[];
57
44
  stop(): Promise<void>;
58
- getExtraDataController(): import("../record_extra_data_controller.js").RecordExtraDataController | null;
45
+ getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
59
46
  }
60
47
  export {};
@@ -64,6 +64,7 @@ class MesioCommand extends EventEmitter {
64
64
  this.process.on("error", (error) => {
65
65
  this.emit("error", error);
66
66
  });
67
+ [];
67
68
  this.process.on("close", (code) => {
68
69
  if (code === 0) {
69
70
  this.emit("end");
@@ -83,18 +84,19 @@ class MesioCommand extends EventEmitter {
83
84
  export const createMesioBuilder = () => {
84
85
  return new MesioCommand();
85
86
  };
86
- export class mesioRecorder extends EventEmitter {
87
+ export class MesioRecorder extends EventEmitter {
87
88
  onEnd;
88
89
  onUpdateLiveInfo;
90
+ type = "mesio";
89
91
  command;
90
92
  streamManager;
91
93
  hasSegment;
92
94
  getSavePath;
93
95
  segment;
94
96
  inputOptions = [];
95
- isHls;
96
97
  disableDanma = false;
97
98
  url;
99
+ debugLevel = "none";
98
100
  headers;
99
101
  constructor(opts, onEnd, onUpdateLiveInfo) {
100
102
  super();
@@ -102,39 +104,32 @@ export class mesioRecorder extends EventEmitter {
102
104
  this.onUpdateLiveInfo = onUpdateLiveInfo;
103
105
  const hasSegment = true;
104
106
  this.disableDanma = opts.disableDanma ?? false;
107
+ this.debugLevel = opts.debugLevel ?? "none";
105
108
  let videoFormat = "flv";
106
109
  if (opts.url.includes(".m3u8")) {
107
110
  videoFormat = "ts";
108
111
  }
109
- if (opts.formatName) {
110
- if (opts.formatName === "fmp4") {
111
- videoFormat = "m4s";
112
- }
113
- else if (opts.formatName === "ts") {
114
- videoFormat = "ts";
115
- }
116
- else if (opts.formatName === "flv") {
117
- videoFormat = "flv";
118
- }
112
+ if (opts.formatName === "fmp4") {
113
+ videoFormat = "m4s";
114
+ }
115
+ else if (opts.formatName === "ts") {
116
+ videoFormat = "ts";
117
+ }
118
+ else if (opts.formatName === "flv") {
119
+ videoFormat = "flv";
119
120
  }
120
121
  this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "mesio", videoFormat, {
121
122
  onUpdateLiveInfo: this.onUpdateLiveInfo,
122
123
  });
123
124
  this.hasSegment = hasSegment;
124
125
  this.getSavePath = opts.getSavePath;
125
- this.inputOptions = opts.inputOptions ?? [];
126
+ this.inputOptions = [];
126
127
  this.url = opts.url;
127
128
  this.segment = opts.segment;
128
129
  this.headers = opts.headers;
129
- if (opts.isHls === undefined) {
130
- this.isHls = this.url.includes("m3u8");
131
- }
132
- else {
133
- this.isHls = opts.isHls;
134
- }
135
130
  this.command = this.createCommand();
136
- this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
137
- this.emit("videoFileCreated", { filename, cover });
131
+ this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
132
+ this.emit("videoFileCreated", { filename, cover, rawFilename });
138
133
  });
139
134
  this.streamManager.on("videoFileCompleted", ({ filename }) => {
140
135
  this.emit("videoFileCompleted", { filename });
@@ -150,6 +145,9 @@ export class mesioRecorder extends EventEmitter {
150
145
  "-H",
151
146
  "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",
152
147
  ];
148
+ if (this.debugLevel === "verbose") {
149
+ inputOptions.push("-v");
150
+ }
153
151
  if (this.headers) {
154
152
  Object.entries(this.headers).forEach(([key, value]) => {
155
153
  if (!value)
@@ -167,8 +165,8 @@ export class mesioRecorder extends EventEmitter {
167
165
  .on("error", this.onEnd)
168
166
  .on("end", () => this.onEnd("finished"))
169
167
  .on("stderr", async (stderrLine) => {
170
- await this.streamManager.handleVideoStarted(stderrLine);
171
168
  this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
169
+ await this.streamManager.handleVideoStarted(stderrLine);
172
170
  });
173
171
  return command;
174
172
  }
@@ -1,5 +1,5 @@
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
3
  import type { RecorderCreateOpts } from "../recorder.js";
4
4
  export type GetSavePath = (data: {
5
5
  startTime: number;
@@ -44,7 +44,7 @@ export declare class StreamManager extends EventEmitter {
44
44
  });
45
45
  handleVideoStarted(stderrLine: string): Promise<void>;
46
46
  handleVideoCompleted(): Promise<void>;
47
- getExtraDataController(): import("../record_extra_data_controller.js").RecordExtraDataController | null;
47
+ getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
48
48
  get videoExt(): VideoFormat;
49
49
  get videoFilePath(): string;
50
50
  }
@@ -1,7 +1,7 @@
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, isMesioStartSegment, isFfmpegStart, retry, cleanTerminalText, } from "../utils.js";
3
+ import { createRecordExtraDataController } from "../xml_stream_controller.js";
4
+ import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isMesioStartSegment, isBililiveStartSegment, isFfmpegStart, retry, cleanTerminalText, } from "../utils.js";
5
5
  export class Segment extends EventEmitter {
6
6
  extraDataController = null;
7
7
  init = true;
@@ -27,8 +27,12 @@ export class Segment extends EventEmitter {
27
27
  return;
28
28
  }
29
29
  try {
30
+ this.emit("DebugLog", {
31
+ type: "info",
32
+ text: `Renaming segment file: ${this.rawRecordingVideoPath} -> ${this.outputFilePath}`,
33
+ });
30
34
  await Promise.all([
31
- retry(() => fs.rename(this.rawRecordingVideoPath, this.outputFilePath), 10, 2000),
35
+ retry(() => fs.rename(this.rawRecordingVideoPath, this.outputFilePath), 20, 1000),
32
36
  this.extraDataController?.flush(),
33
37
  ]);
34
38
  this.emit("videoFileCompleted", { filename: this.outputFilePath });
@@ -38,6 +42,8 @@ export class Segment extends EventEmitter {
38
42
  type: "error",
39
43
  text: "videoFileCompleted error " + String(err),
40
44
  });
45
+ // 虽然重命名失败了,但是也当作完成处理,避免卡住录制流程
46
+ this.emit("videoFileCompleted", { filename: this.outputFilePath });
41
47
  }
42
48
  }
43
49
  async onSegmentStart(stderrLine, callBack) {
@@ -64,25 +70,28 @@ export class Segment extends EventEmitter {
64
70
  });
65
71
  ensureFolderExist(this.outputVideoFilePath);
66
72
  if (!this.disableDanma) {
67
- this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.json`);
73
+ this.extraDataController = createRecordExtraDataController(`${this.outputVideoFilePath}.xml`);
68
74
  }
69
75
  // 支持两种格式的正则表达式
70
76
  // 1. FFmpeg格式: Opening 'filename' for writing
71
77
  // 2. Mesio格式: Opening FLV segment path=filename Processing
72
78
  const ffmpegRegex = /'([^']+)'/;
73
- const mesioRegex = /segment path=([^\n]*)/i;
79
+ const mesioRegex = /segment path=(.+?\.(?:flv|ts|m4s))/is;
74
80
  let match = stderrLine.match(ffmpegRegex);
75
81
  if (!match) {
76
82
  match = cleanTerminalText(stderrLine).match(mesioRegex);
77
83
  }
84
+ this.emit("DebugLog", { type: "ffmpeg", text: `Segment start line: ${stderrLine}` });
78
85
  if (match) {
79
86
  const filename = match[1];
80
87
  this.rawRecordingVideoPath = filename;
81
88
  this.emit("videoFileCreated", {
89
+ rawFilename: filename,
82
90
  filename: this.outputFilePath,
83
91
  title: liveInfo?.title,
84
92
  cover: liveInfo?.cover,
85
93
  });
94
+ this.emit("DebugLog", { type: "ffmpeg", text: JSON.stringify(match, null, 2) });
86
95
  }
87
96
  else {
88
97
  this.emit("DebugLog", { type: "ffmpeg", text: "No match found" });
@@ -122,7 +131,7 @@ export class StreamManager extends EventEmitter {
122
131
  });
123
132
  }
124
133
  else {
125
- const extraDataSavePath = replaceExtName(recordSavePath, ".json");
134
+ const extraDataSavePath = replaceExtName(recordSavePath, ".xml");
126
135
  if (!disableDanma) {
127
136
  this.extraDataController = createRecordExtraDataController(extraDataSavePath);
128
137
  }
@@ -147,6 +156,15 @@ export class StreamManager extends EventEmitter {
147
156
  }
148
157
  else if (this.recorderType === "mesio") {
149
158
  if (this.segment && isMesioStartSegment(stderrLine)) {
159
+ for (let line of stderrLine.split("\n")) {
160
+ if (isMesioStartSegment(line)) {
161
+ await this.segment.onSegmentStart(line, this.callBack);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ else if (this.recorderType === "bililive") {
167
+ if (this.segment && isBililiveStartSegment(stderrLine)) {
150
168
  await this.segment.onSegmentStart(stderrLine, this.callBack);
151
169
  }
152
170
  }
@@ -168,6 +186,11 @@ export class StreamManager extends EventEmitter {
168
186
  await this.segment.handleSegmentEnd();
169
187
  }
170
188
  }
189
+ else if (this.recorderType === "bililive") {
190
+ if (this.segment) {
191
+ await this.segment.handleSegmentEnd();
192
+ }
193
+ }
171
194
  }
172
195
  getExtraDataController() {
173
196
  return this.segment?.extraDataController || this.extraDataController;
@@ -179,6 +202,9 @@ export class StreamManager extends EventEmitter {
179
202
  else if (this.recorderType === "mesio") {
180
203
  return this.videoFormat;
181
204
  }
205
+ else if (this.recorderType === "bililive") {
206
+ return "flv";
207
+ }
182
208
  else {
183
209
  throw new Error("Unknown recorderType");
184
210
  }
@@ -192,6 +218,9 @@ export class StreamManager extends EventEmitter {
192
218
  else if (this.recorderType === "mesio") {
193
219
  return `${this.recordSavePath}-PART%i.${this.videoExt}`;
194
220
  }
221
+ else if (this.recorderType === "bililive") {
222
+ return `${this.recordSavePath}.${this.videoExt}`;
223
+ }
195
224
  return `${this.recordSavePath}.${this.videoExt}`;
196
225
  }
197
226
  }
package/lib/recorder.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
10
10
  channelId: ChannelId;
11
11
  id?: string;
12
12
  remarks?: string;
13
+ weight?: number;
13
14
  disableAutoCheck?: boolean;
14
15
  disableProvideCommentsWhenRecording?: boolean;
15
16
  quality: Quality;
@@ -25,7 +26,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
25
26
  /** 身份验证 */
26
27
  auth?: string;
27
28
  /** cookie所有者uid,B站弹幕录制 */
28
- uid?: number;
29
+ uid?: number | string;
29
30
  /** 画质匹配重试次数 */
30
31
  qualityRetry?: number;
31
32
  /** 抖音是否使用双屏直播流,开启后如果是双屏直播,那么就使用拼接的流,默认为true */
@@ -38,14 +39,14 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
38
39
  formatName?: FormatName;
39
40
  /** 流编码 */
40
41
  codecName?: CodecName;
41
- /** 选择使用的api,虎牙支持: auto,web,mp,抖音支持:web,webHTML */
42
- api?: "auto" | "web" | "mp" | "webHTML";
42
+ /** 选择使用的api,虎牙支持: auto,web,mp,抖音支持:web,webHTML,mobile,userHTML */
43
+ api?: "auto" | "web" | "mp" | "webHTML" | "mobile" | "userHTML";
43
44
  /** 标题关键词,如果直播间标题包含这些关键词,则不会自动录制(仅对斗鱼有效),多个关键词用英文逗号分隔 */
44
45
  titleKeywords?: string;
45
46
  /** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
46
47
  videoFormat?: "auto" | "ts" | "mkv";
47
48
  /** 录制类型 */
48
- recorderType?: "auto" | "ffmpeg" | "mesio";
49
+ recorderType?: "auto" | "ffmpeg" | "mesio" | "bililive";
49
50
  /** 流格式优先级 */
50
51
  formatriorities?: Array<"flv" | "hls">;
51
52
  /** 只录制音频 */
@@ -55,10 +56,14 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
55
56
  /** 控制弹幕是否使用服务端时间戳 */
56
57
  useServerTimestamp?: boolean;
57
58
  extra?: Partial<E>;
59
+ /** 调试等级 */
60
+ debugLevel?: "none" | "basic" | "verbose";
61
+ /** 缓存 */
58
62
  cache: Cache;
59
63
  }
60
64
  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">;
61
- export type RecorderState = "idle" | "recording" | "stopping-record";
65
+ /** 录制状态,idle: 空闲中,recording: 录制中,stopping-record: 停止录制中,check-error: 检查错误,title-blocked: 标题黑名单 */
66
+ export type RecorderState = "idle" | "recording" | "stopping-record" | "check-error" | "title-blocked";
62
67
  export type Progress = {
63
68
  time: string | null;
64
69
  };
@@ -88,6 +93,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
88
93
  videoFileCreated: {
89
94
  filename: string;
90
95
  cover?: string;
96
+ rawFilename?: string;
91
97
  };
92
98
  videoFileCompleted: {
93
99
  filename: string;
@@ -110,7 +116,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
110
116
  state: RecorderState;
111
117
  qualityMaxRetry: number;
112
118
  qualityRetry: number;
113
- uid?: number;
119
+ uid?: number | string;
114
120
  liveInfo?: {
115
121
  living: boolean;
116
122
  owner: string;
package/lib/utils.d.ts CHANGED
@@ -37,6 +37,7 @@ 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
39
  export declare function isMesioStartSegment(line: string): boolean;
40
+ export declare function isBililiveStartSegment(line: string): boolean;
40
41
  export declare function isFfmpegStart(line: string): boolean;
41
42
  export declare function cleanTerminalText(text: string): string;
42
43
  export declare const formatTemplate: (string: string, ...args: any[]) => string;
@@ -72,6 +73,15 @@ export declare function sortByKeyOrder<T, K extends keyof T>(objects: T[], order
72
73
  */
73
74
  export declare function retry<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
74
75
  export declare const isBetweenTimeRange: (range: undefined | [] | [string | null, string | null]) => boolean;
76
+ export declare const sleep: (ms: number) => Promise<unknown>;
77
+ /**
78
+ * 检查标题是否包含黑名单关键词
79
+ */
80
+ declare function hasBlockedTitleKeywords(title: string, titleKeywords: string | undefined): boolean;
81
+ /**
82
+ * 检查是否需要进行标题关键词检查
83
+ */
84
+ declare function shouldCheckTitleKeywords(isManualStart: boolean | undefined, titleKeywords: string | undefined): boolean;
75
85
  declare const _default: {
76
86
  replaceExtName: typeof replaceExtName;
77
87
  singleton: typeof singleton;
@@ -91,5 +101,8 @@ declare const _default: {
91
101
  sortByKeyOrder: typeof sortByKeyOrder;
92
102
  retry: typeof retry;
93
103
  isBetweenTimeRange: (range: undefined | [] | [string | null, string | null]) => boolean;
104
+ hasBlockedTitleKeywords: typeof hasBlockedTitleKeywords;
105
+ shouldCheckTitleKeywords: typeof shouldCheckTitleKeywords;
106
+ sleep: (ms: number) => Promise<unknown>;
94
107
  };
95
108
  export default _default;
package/lib/utils.js CHANGED
@@ -127,7 +127,10 @@ export function isFfmpegStartSegment(line) {
127
127
  return line.includes("Opening ") && line.includes("for writing");
128
128
  }
129
129
  export function isMesioStartSegment(line) {
130
- return line.includes("Opening ") && line.includes("Opening segment");
130
+ return line.includes("Opening segment");
131
+ }
132
+ export function isBililiveStartSegment(line) {
133
+ return line.includes("创建录制文件");
131
134
  }
132
135
  export function isFfmpegStart(line) {
133
136
  return ((line.includes("frame=") && line.includes("fps=")) ||
@@ -323,6 +326,23 @@ function isBetweenTime(currentTime, timeRange) {
323
326
  }
324
327
  return start <= current && current <= end;
325
328
  }
329
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
330
+ /**
331
+ * 检查标题是否包含黑名单关键词
332
+ */
333
+ function hasBlockedTitleKeywords(title, titleKeywords) {
334
+ const keywords = (titleKeywords ?? "")
335
+ .split(",")
336
+ .map((k) => k.trim())
337
+ .filter((k) => k);
338
+ return keywords.some((keyword) => title.toLowerCase().includes(keyword.toLowerCase()));
339
+ }
340
+ /**
341
+ * 检查是否需要进行标题关键词检查
342
+ */
343
+ function shouldCheckTitleKeywords(isManualStart, titleKeywords) {
344
+ return (!isManualStart && !!titleKeywords && typeof titleKeywords === "string" && !!titleKeywords.trim());
345
+ }
326
346
  export default {
327
347
  replaceExtName,
328
348
  singleton,
@@ -342,4 +362,7 @@ export default {
342
362
  sortByKeyOrder,
343
363
  retry,
344
364
  isBetweenTimeRange,
365
+ hasBlockedTitleKeywords,
366
+ shouldCheckTitleKeywords,
367
+ sleep,
345
368
  };