@bililive-tools/manager 1.9.0 → 1.11.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,8 +1,8 @@
1
1
  import path from "node:path";
2
2
  import EventEmitter from "node:events";
3
3
  import { spawn } from "node:child_process";
4
+ import { DEFAULT_USER_AGENT } from "./index.js";
4
5
  import { StreamManager, getMesioPath } from "../index.js";
5
- // Mesio command builder class similar to ffmpeg
6
6
  class MesioCommand extends EventEmitter {
7
7
  _input = "";
8
8
  _output = "";
@@ -74,9 +74,10 @@ class MesioCommand extends EventEmitter {
74
74
  }
75
75
  });
76
76
  }
77
- kill(signal = "SIGTERM") {
77
+ kill() {
78
78
  if (this.process) {
79
- this.process.kill(signal);
79
+ this.process.stdin?.write("q");
80
+ this.process.stdin?.end();
80
81
  }
81
82
  }
82
83
  }
@@ -84,7 +85,7 @@ class MesioCommand extends EventEmitter {
84
85
  export const createMesioBuilder = () => {
85
86
  return new MesioCommand();
86
87
  };
87
- export class MesioRecorder extends EventEmitter {
88
+ export class mesioDownloader extends EventEmitter {
88
89
  onEnd;
89
90
  onUpdateLiveInfo;
90
91
  type = "mesio";
@@ -102,7 +103,9 @@ export class MesioRecorder extends EventEmitter {
102
103
  super();
103
104
  this.onEnd = onEnd;
104
105
  this.onUpdateLiveInfo = onUpdateLiveInfo;
106
+ // 存在自动分段,永远为true
105
107
  const hasSegment = true;
108
+ this.hasSegment = hasSegment;
106
109
  this.disableDanma = opts.disableDanma ?? false;
107
110
  this.debugLevel = opts.debugLevel ?? "none";
108
111
  let videoFormat = "flv";
@@ -121,12 +124,14 @@ export class MesioRecorder extends EventEmitter {
121
124
  this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "mesio", videoFormat, {
122
125
  onUpdateLiveInfo: this.onUpdateLiveInfo,
123
126
  });
124
- this.hasSegment = hasSegment;
125
127
  this.getSavePath = opts.getSavePath;
126
128
  this.inputOptions = [];
127
129
  this.url = opts.url;
128
130
  this.segment = opts.segment;
129
- this.headers = opts.headers;
131
+ this.headers = {
132
+ "User-Agent": DEFAULT_USER_AGENT,
133
+ ...(opts.headers || {}),
134
+ };
130
135
  this.command = this.createCommand();
131
136
  this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
132
137
  this.emit("videoFileCreated", { filename, cover, rawFilename, title });
@@ -139,13 +144,7 @@ export class MesioRecorder extends EventEmitter {
139
144
  });
140
145
  }
141
146
  createCommand() {
142
- const inputOptions = [
143
- ...this.inputOptions,
144
- "--fix",
145
- "-H",
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",
147
- "--no-proxy",
148
- ];
147
+ const inputOptions = [...this.inputOptions, "--fix", "--no-proxy"];
149
148
  if (this.debugLevel === "verbose") {
150
149
  inputOptions.push("-v");
151
150
  }
@@ -156,8 +155,13 @@ export class MesioRecorder extends EventEmitter {
156
155
  inputOptions.push("-H", `${key}: ${value}`);
157
156
  });
158
157
  }
159
- if (this.hasSegment) {
160
- inputOptions.push("-d", `${this.segment * 60}s`);
158
+ if (this.segment) {
159
+ if (typeof this.segment === "number") {
160
+ inputOptions.push("-d", `${this.segment * 60}s`);
161
+ }
162
+ else if (typeof this.segment === "string") {
163
+ inputOptions.push("-m", this.segment);
164
+ }
161
165
  }
162
166
  const command = createMesioBuilder()
163
167
  .input(this.url)
@@ -179,8 +183,8 @@ export class MesioRecorder extends EventEmitter {
179
183
  }
180
184
  async stop() {
181
185
  try {
182
- // 直接发送SIGINT信号,会导致数据丢失
183
- this.command.kill("SIGINT");
186
+ this.command.kill();
187
+ await new Promise((resolve) => setTimeout(resolve, 2000));
184
188
  await this.streamManager.handleVideoCompleted();
185
189
  }
186
190
  catch (err) {
@@ -190,4 +194,10 @@ export class MesioRecorder extends EventEmitter {
190
194
  getExtraDataController() {
191
195
  return this.streamManager?.getExtraDataController();
192
196
  }
197
+ get videoFilePath() {
198
+ return this.streamManager.videoFilePath;
199
+ }
200
+ cut() {
201
+ throw new Error("Mesio downloader does not support cut operation.");
202
+ }
193
203
  }
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from "node:events";
2
2
  import { createRecordExtraDataController } from "../xml_stream_controller.js";
3
3
  import type { RecorderCreateOpts } from "../recorder.js";
4
- import type { VideoFormat } from "../index.js";
4
+ import type { TrueVideoFormat } from "../index.js";
5
5
  export type GetSavePath = (data: {
6
6
  startTime: number;
7
7
  title?: string;
@@ -16,8 +16,8 @@ export declare class Segment extends EventEmitter {
16
16
  /** 输出文件名名,不包含拓展名 */
17
17
  outputVideoFilePath: string;
18
18
  disableDanma: boolean;
19
- videoExt: VideoFormat;
20
- constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: VideoFormat);
19
+ videoExt: TrueVideoFormat;
20
+ constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: TrueVideoFormat);
21
21
  handleSegmentEnd(): Promise<void>;
22
22
  onSegmentStart(stderrLine: string, callBack?: {
23
23
  onUpdateLiveInfo: () => Promise<{
@@ -36,7 +36,7 @@ export declare class StreamManager extends EventEmitter {
36
36
  recorderType: RecorderType;
37
37
  private videoFormat;
38
38
  private callBack?;
39
- constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, recorderType: RecorderType, videoFormat: VideoFormat, callBack?: {
39
+ constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, recorderType: RecorderType, videoFormat: TrueVideoFormat, callBack?: {
40
40
  onUpdateLiveInfo: () => Promise<{
41
41
  title?: string;
42
42
  cover?: string;
@@ -45,7 +45,7 @@ export declare class StreamManager extends EventEmitter {
45
45
  handleVideoStarted(stderrLine: string): Promise<void>;
46
46
  handleVideoCompleted(): Promise<void>;
47
47
  getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
48
- get videoExt(): VideoFormat;
48
+ get videoExt(): TrueVideoFormat;
49
49
  get videoFilePath(): string;
50
50
  }
51
51
  export {};
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from "node:events";
2
2
  import fs from "fs/promises";
3
3
  import { createRecordExtraDataController } from "../xml_stream_controller.js";
4
- import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isMesioStartSegment, isBililiveStartSegment, isFfmpegStart, retry, cleanTerminalText, } from "../utils.js";
4
+ import { 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;
@@ -107,14 +107,14 @@ export class StreamManager extends EventEmitter {
107
107
  recordSavePath;
108
108
  recordStartTime;
109
109
  hasSegment;
110
- recorderType = "ffmpeg";
110
+ recorderType;
111
111
  videoFormat;
112
112
  callBack;
113
113
  constructor(getSavePath, hasSegment, disableDanma, recorderType, videoFormat, callBack) {
114
114
  super();
115
115
  const recordSavePath = getSavePath({ startTime: Date.now() });
116
116
  this.recordSavePath = recordSavePath;
117
- this.videoFormat = videoFormat ?? "auto";
117
+ this.videoFormat = videoFormat;
118
118
  this.recorderType = recorderType;
119
119
  this.hasSegment = hasSegment;
120
120
  this.callBack = callBack;
@@ -131,7 +131,7 @@ export class StreamManager extends EventEmitter {
131
131
  });
132
132
  }
133
133
  else {
134
- const extraDataSavePath = replaceExtName(recordSavePath, ".xml");
134
+ const extraDataSavePath = `${recordSavePath}.xml`;
135
135
  if (!disableDanma) {
136
136
  this.extraDataController = createRecordExtraDataController(extraDataSavePath);
137
137
  }
@@ -196,18 +196,7 @@ export class StreamManager extends EventEmitter {
196
196
  return this.segment?.extraDataController || this.extraDataController;
197
197
  }
198
198
  get videoExt() {
199
- if (this.recorderType === "ffmpeg") {
200
- return this.videoFormat;
201
- }
202
- else if (this.recorderType === "mesio") {
203
- return this.videoFormat;
204
- }
205
- else if (this.recorderType === "bililive") {
206
- return "flv";
207
- }
208
- else {
209
- throw new Error("Unknown recorderType");
210
- }
199
+ return this.videoFormat;
211
200
  }
212
201
  get videoFilePath() {
213
202
  if (this.recorderType === "ffmpeg") {
package/lib/index.d.ts CHANGED
@@ -6,9 +6,11 @@ import utils from "./utils.js";
6
6
  export * from "./common.js";
7
7
  export * from "./recorder.js";
8
8
  export * from "./manager.js";
9
+ export * from "./cache.js";
9
10
  export * from "./record_extra_data_controller.js";
10
- export * from "./recorder/FFMPEGRecorder.js";
11
- export { createBaseRecorder } from "./recorder/index.js";
11
+ export * from "./downloader/FFmpegDownloader.js";
12
+ export { createDownloader } from "./downloader/index.js";
13
+ export { checkTitleKeywordsWhileRecording, checkTitleKeywordsBeforeRecord } from "./utils.js";
12
14
  export { utils };
13
15
  /**
14
16
  * 提供一些 utils
@@ -25,3 +27,4 @@ export declare function setBililivePath(newPath: string): void;
25
27
  export declare function getBililivePath(): string;
26
28
  export declare function getDataFolderPath<E extends AnyObject>(provider: RecorderProvider<E>): string;
27
29
  export type VideoFormat = "auto" | "ts" | "mkv" | "flv" | "mp4" | "m4s";
30
+ export type TrueVideoFormat = Exclude<VideoFormat, "auto">;
package/lib/index.js CHANGED
@@ -5,9 +5,11 @@ import utils from "./utils.js";
5
5
  export * from "./common.js";
6
6
  export * from "./recorder.js";
7
7
  export * from "./manager.js";
8
+ export * from "./cache.js";
8
9
  export * from "./record_extra_data_controller.js";
9
- export * from "./recorder/FFMPEGRecorder.js";
10
- export { createBaseRecorder } from "./recorder/index.js";
10
+ export * from "./downloader/FFmpegDownloader.js";
11
+ export { createDownloader } from "./downloader/index.js";
12
+ export { checkTitleKeywordsWhileRecording, checkTitleKeywordsBeforeRecord } from "./utils.js";
11
13
  export { utils };
12
14
  /**
13
15
  * 提供一些 utils
package/lib/manager.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Emitter } from "mitt";
2
2
  import { ChannelId, Message } from "./common.js";
3
+ import { RecorderCache } from "./cache.js";
3
4
  import { RecorderCreateOpts, Recorder, SerializedRecorder, RecordHandle, DebugLog, Progress } from "./recorder.js";
4
5
  import { AnyObject, UnknownObject } from "./utils.js";
5
- import { StreamManager } from "./recorder/streamManager.js";
6
- import { Cache } from "./cache.js";
6
+ import { StreamManager } from "./downloader/streamManager.js";
7
7
  export interface RecorderProvider<E extends AnyObject> {
8
8
  id: string;
9
9
  name: string;
@@ -75,8 +75,9 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
75
75
  providers: P[];
76
76
  getChannelURLMatchedRecorderProviders: (this: RecorderManager<ME, P, PE, E>, channelURL: string) => P[];
77
77
  recorders: Recorder<E>[];
78
- addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: Omit<RecorderCreateOpts<E>, "cache">) => Recorder<E>;
78
+ addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: RecorderCreateOpts<E>) => Recorder<E>;
79
79
  removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
80
+ getRecorder: (this: RecorderManager<ME, P, PE, E>, id: string) => Recorder<E> | null;
80
81
  startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
81
82
  stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
82
83
  cutRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
@@ -91,13 +92,15 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
91
92
  ffmpegOutputArgs: string;
92
93
  /** b站使用批量查询接口 */
93
94
  biliBatchQuery: boolean;
94
- /** 测试:录制错误立即重试 */
95
+ /** 下播延迟检查 */
95
96
  recordRetryImmediately: boolean;
96
- /** 缓存实例 */
97
- cache: Cache;
97
+ /** 缓存系统 */
98
+ cache: RecorderCache;
98
99
  }
99
100
  export type RecorderManagerCreateOpts<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> = Partial<Pick<RecorderManager<ME, P, PE, E>, ConfigurableProp>> & {
100
101
  providers: P[];
102
+ /** 自定义缓存实现,不提供则使用默认的内存缓存 */
103
+ cache?: RecorderCache;
101
104
  };
102
105
  export declare function createRecorderManager<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE>(opts: RecorderManagerCreateOpts<ME, P, PE, E>): RecorderManager<ME, P, PE, E>;
103
106
  export declare function genSavePathFromRule<ME extends AnyObject, P extends RecorderProvider<AnyObject>, PE extends AnyObject, E extends AnyObject>(manager: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>, extData: {
@@ -108,4 +111,4 @@ export declare function genSavePathFromRule<ME extends AnyObject, P extends Reco
108
111
  recordStartTime: Date;
109
112
  }): string;
110
113
  export type GetProviderExtra<P> = P extends RecorderProvider<infer E> ? E : never;
111
- export { StreamManager, Cache };
114
+ export { StreamManager };
package/lib/manager.js CHANGED
@@ -3,10 +3,10 @@ import mitt from "mitt";
3
3
  import ejs from "ejs";
4
4
  import { omit, range } from "lodash-es";
5
5
  import { parseArgsStringToArgv } from "string-argv";
6
+ import { RecorderCacheImpl, MemoryCacheStore } from "./cache.js";
6
7
  import { getBiliStatusInfoByRoomIds } from "./api.js";
7
8
  import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, sleep, } from "./utils.js";
8
- import { StreamManager } from "./recorder/streamManager.js";
9
- import { Cache } from "./cache.js";
9
+ import { StreamManager } from "./downloader/streamManager.js";
10
10
  const configurableProps = [
11
11
  "savePathRule",
12
12
  "autoRemoveSystemReservedChars",
@@ -97,8 +97,6 @@ export function createRecorderManager(opts) {
97
97
  const liveStartObj = {};
98
98
  // 用于记录触发重试直播场次的次数
99
99
  const retryCountObj = {};
100
- // 获取缓存单例
101
- const cache = Cache.getInstance();
102
100
  const manager = {
103
101
  // @ts-ignore
104
102
  ...mitt(),
@@ -115,8 +113,10 @@ export function createRecorderManager(opts) {
115
113
  // provider.createRecorder 能返回 Recorder<PE> 才能进一步优化。
116
114
  const recorder = provider.createRecorder({
117
115
  ...omit(opts, ["providerId"]),
118
- cache,
116
+ // cache,
119
117
  });
118
+ // 为录制器注入独立的缓存命名空间
119
+ recorder.cache = this.cache.createNamespace(recorder.id);
120
120
  this.recorders.push(recorder);
121
121
  recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder: recorder.toJSON(), recordHandle }));
122
122
  recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder: recorder.toJSON(), recordHandle }));
@@ -133,29 +133,32 @@ export function createRecorderManager(opts) {
133
133
  recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
134
134
  recorder.on("RecordStop", ({ recordHandle, reason }) => {
135
135
  this.emit("RecordStop", { recorder: recorder.toJSON(), recordHandle, reason });
136
- // 如果reason中存在"invalid stream",说明直播由于某些原因中断了,虽然会在下一次周期检查中继续,但是会遗漏一段时间。
137
- // 这时候可以触发一次检查,但出于直播可能抽风的原因,为避免风控,一场直播最多触发五次。
138
- // 测试阶段,还需要一个开关,默认关闭,几个版本后转正使用
136
+ const maxRetryCount = 10;
137
+ // 默认策略下,如果录制被中断,那么会在下一个检查周期时重新检查直播状态并重新开始录制,这种策略的问题就是一部分时间会被漏掉。
138
+ // 如果开启了该选项,且录制开始时间与结束时间相差在一分钟以上(某些平台下播会扔会有重复流),那么会立即进行一次检查。
139
139
  // 也许之后还能链接复用,但也会引入更多复杂度,需要谨慎考虑
140
140
  // 虎牙直播结束后可能额外触发导致错误,忽略虎牙直播间:https://www.huya.com/910323
141
141
  if (manager.recordRetryImmediately &&
142
- recorder.providerId !== "HuYa" &&
143
- reason &&
144
- reason.includes("invalid stream") &&
145
- recorder?.liveInfo?.liveId) {
142
+ recorder?.liveInfo?.liveId &&
143
+ reason !== "manual stop") {
146
144
  const key = `${recorder.channelId}-${recorder.liveInfo?.liveId}`;
147
- if (retryCountObj[key] > 5)
145
+ const recordStartTime = recorder.liveInfo?.recordStartTime.getTime() ?? 0;
146
+ const recordStopTime = Date.now();
147
+ // 录制时间差在一分钟以上
148
+ if (recordStopTime - recordStartTime < 60 * 1000)
149
+ return;
150
+ if (retryCountObj[key] > maxRetryCount)
148
151
  return;
149
152
  if (!retryCountObj[key]) {
150
153
  retryCountObj[key] = 0;
151
154
  }
152
- if (retryCountObj[key] < 5) {
155
+ if (retryCountObj[key] < maxRetryCount) {
153
156
  retryCountObj[key]++;
154
157
  }
155
158
  this.emit("RecorderDebugLog", {
156
159
  recorder,
157
160
  type: "common",
158
- text: `录制${recorder?.channelId}因“${reason}”中断,触发重试直播(${retryCountObj[key]})`,
161
+ text: `录制${recorder.channelId}中断,立即触发重试(${retryCountObj[key]}/${maxRetryCount})`,
159
162
  });
160
163
  // 触发一次检查,等待一秒使状态清理完毕
161
164
  setTimeout(() => {
@@ -191,6 +194,10 @@ export function createRecorderManager(opts) {
191
194
  delete tempBanObj[recorder.channelId];
192
195
  this.emit("RecorderRemoved", recorder.toJSON());
193
196
  },
197
+ getRecorder(id) {
198
+ const recorder = this.recorders.find((item) => item.id === id);
199
+ return recorder ?? null;
200
+ },
194
201
  async startRecord(id) {
195
202
  const recorder = this.recorders.find((item) => item.id === id);
196
203
  if (recorder == null)
@@ -214,7 +221,7 @@ export function createRecorderManager(opts) {
214
221
  if (recorder.recordHandle == null)
215
222
  return;
216
223
  const liveId = recorder.liveInfo?.liveId;
217
- await recorder.recordHandle.stop("manual stop", true);
224
+ await recorder.recordHandle.stop("manual stop");
218
225
  if (liveId) {
219
226
  tempBanObj[recorder.channelId] = liveId;
220
227
  recorder.tempStopIntervalCheck = true;
@@ -269,6 +276,7 @@ export function createRecorderManager(opts) {
269
276
  autoRemoveSystemReservedChars: opts.autoRemoveSystemReservedChars ?? true,
270
277
  biliBatchQuery: opts.biliBatchQuery ?? false,
271
278
  recordRetryImmediately: opts.recordRetryImmediately ?? false,
279
+ cache: opts.cache ?? new RecorderCacheImpl(new MemoryCacheStore()),
272
280
  ffmpegOutputArgs: opts.ffmpegOutputArgs ??
273
281
  "-c copy" +
274
282
  /**
@@ -276,18 +284,7 @@ export function createRecorderManager(opts) {
276
284
  * 最后一个片段,而 FLV 格式如果录制中 KILL 了需要手动修复下 keyframes。所以默认使用 fmp4 格式。
277
285
  */
278
286
  " -movflags faststart+frag_keyframe+empty_moov" +
279
- /**
280
- * 浏览器加载 FragmentMP4 会需要先把它所有的 moof boxes 都加载完成后才能播放,
281
- * 默认的分段时长很小,会产生大量的 moof,导致加载很慢,所以这里设置一个分段的最小时长。
282
- *
283
- * TODO: 这个浏览器行为或许是可以优化的,比如试试给 fmp4 在录制完成后设置或者录制过程中实时更新 mvhd.duration。
284
- * https://stackoverflow.com/questions/55887980/how-to-use-media-source-extension-mse-low-latency-mode
285
- * https://stackoverflow.com/questions/61803136/ffmpeg-fragmented-mp4-takes-long-time-to-start-playing-on-chrome
286
- *
287
- * TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
288
- */
289
287
  " -min_frag_duration 10000000",
290
- cache,
291
288
  };
292
289
  const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
293
290
  const args = parseArgsStringToArgv(ffmpegOutputArgs);
@@ -334,11 +331,10 @@ export function genSavePathFromRule(manager, recorder, extData) {
334
331
  let savePathRule = manager.savePathRule;
335
332
  try {
336
333
  savePathRule = ejs.render(savePathRule, params);
337
- console.log("解析后保存路径模板:", savePathRule, params);
338
334
  }
339
335
  catch (error) {
340
336
  console.error("模板解析错误", error, savePathRule, params);
341
337
  }
342
338
  return formatTemplate(savePathRule, params);
343
339
  }
344
- export { StreamManager, Cache };
340
+ export { StreamManager };
@@ -5,7 +5,7 @@ export interface RecordExtraData {
5
5
  recordStartTimestamp: number;
6
6
  recordStopTimestamp?: number;
7
7
  liveStartTimestamp?: number;
8
- ffmpegArgs?: string[];
8
+ downloaderArgs?: string[];
9
9
  platform?: string;
10
10
  user_name?: string;
11
11
  room_id?: string;
package/lib/recorder.d.ts CHANGED
@@ -2,8 +2,8 @@ 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";
6
- import type { RecorderType } from "./recorder/index.js";
5
+ import type { NamespacedCache } from "./cache.js";
6
+ import type { DownloaderType } from "./downloader/index.js";
7
7
  type FormatName = "auto" | "flv" | "hls" | "fmp4" | "flv_only" | "hls_only" | "fmp4_only";
8
8
  type CodecName = "auto" | "avc" | "hevc" | "avc_only" | "hevc_only";
9
9
  export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
@@ -19,7 +19,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
19
19
  sourcePriorities: string[];
20
20
  formatPriorities?: Array<"flv" | "hls">;
21
21
  source?: string;
22
- segment?: number;
22
+ segment?: string;
23
23
  saveGiftDanma?: boolean;
24
24
  saveSCDanma?: boolean;
25
25
  /** 保存封面 */
@@ -40,8 +40,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
40
40
  formatName?: FormatName;
41
41
  /** 流编码 */
42
42
  codecName?: CodecName;
43
- /** 选择使用的api,虎牙支持: auto,web,mp,抖音支持:web,webHTML,mobile,userHTML */
44
- api?: "auto" | "web" | "mp" | "webHTML" | "mobile" | "userHTML";
43
+ /** 选择使用的api,虎牙支持: auto,web,mp,wup,抖音支持:web,webHTML,mobile,userHTML */
44
+ api?: "auto" | "web" | "mp" | "wup" | "webHTML" | "mobile" | "userHTML" | "balance" | "random" | string;
45
45
  /** 标题关键词,如果直播间标题包含这些关键词,则不会自动录制(仅对斗鱼有效),多个关键词用英文逗号分隔 */
46
46
  titleKeywords?: string;
47
47
  /** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
@@ -59,8 +59,6 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
59
59
  extra?: Partial<E>;
60
60
  /** 调试等级 */
61
61
  debugLevel?: "none" | "basic" | "verbose";
62
- /** 缓存 */
63
- cache: Cache;
64
62
  }
65
63
  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">;
66
64
  /** 录制状态,idle: 空闲中,recording: 录制中,stopping-record: 停止录制中,check-error: 检查错误,title-blocked: 标题黑名单 */
@@ -72,12 +70,12 @@ export interface RecordHandle {
72
70
  id: string;
73
71
  stream: string;
74
72
  source: string;
75
- recorderType?: RecorderType;
73
+ recorderType?: DownloaderType;
76
74
  url: string;
77
- ffmpegArgs?: string[];
75
+ downloaderArgs?: string[];
78
76
  progress?: Progress;
79
77
  savePath: string;
80
- stop: (this: RecordHandle, reason?: string, tempStopIntervalCheck?: boolean) => Promise<void>;
78
+ stop: (this: RecordHandle, reason?: string) => Promise<void>;
81
79
  cut: (this: RecordHandle) => Promise<void>;
82
80
  }
83
81
  export interface DebugLog {
@@ -118,21 +116,21 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
118
116
  usedStream?: string;
119
117
  usedSource?: string;
120
118
  state: RecorderState;
121
- qualityMaxRetry: number;
122
119
  qualityRetry: number;
123
120
  uid?: number | string;
124
121
  liveInfo?: {
125
122
  living: boolean;
126
123
  owner: string;
127
124
  title: string;
128
- startTime: Date;
125
+ liveStartTime: Date;
129
126
  avatar: string;
130
127
  cover: string;
131
128
  liveId?: string;
129
+ recordStartTime: Date;
132
130
  };
133
131
  tempStopIntervalCheck?: boolean;
134
- /** 缓存实例引用,由 manager 设置 */
135
- cache: Cache;
132
+ /** 缓存实例(命名空间) */
133
+ cache: NamespacedCache;
136
134
  getChannelURL: (this: Recorder<E>) => string;
137
135
  checkLiveStatusAndRecord: (this: Recorder<E>, opts: {
138
136
  getSavePath: GetSavePath;
@@ -148,7 +146,7 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
148
146
  cover: string;
149
147
  channelId: ChannelId;
150
148
  living: boolean;
151
- startTime: Date;
149
+ liveStartTime: Date;
152
150
  }>;
153
151
  getStream: (this: Recorder<E>) => Promise<{
154
152
  source: string;
package/lib/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DebouncedFunc } from "lodash-es";
2
+ import type { Recorder } from "./recorder.js";
2
3
  export type AnyObject = Record<string, any>;
3
4
  export type UnknownObject = Record<string, unknown>;
4
5
  export type PickRequired<T, K extends keyof T> = T & Pick<Required<T>, K>;
@@ -74,6 +75,14 @@ export declare function sortByKeyOrder<T, K extends keyof T>(objects: T[], order
74
75
  export declare function retry<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
75
76
  export declare const isBetweenTimeRange: (range: undefined | [] | [string | null, string | null]) => boolean;
76
77
  export declare const sleep: (ms: number) => Promise<unknown>;
78
+ /**
79
+ * 判断是否应该使用严格画质模式
80
+ * @param qualityRetryLeft 剩余的画质重试次数
81
+ * @param qualityRetry 初始画质重试次数配置
82
+ * @param isManualStart 是否手动启动
83
+ * @returns 是否使用严格画质模式
84
+ */
85
+ export declare function shouldUseStrictQuality(qualityRetryLeft: number, qualityRetry: number, isManualStart?: boolean): boolean;
77
86
  /**
78
87
  * 检查标题是否包含黑名单关键词
79
88
  */
@@ -82,6 +91,24 @@ declare function hasBlockedTitleKeywords(title: string, titleKeywords: string |
82
91
  * 检查是否需要进行标题关键词检查
83
92
  */
84
93
  declare function shouldCheckTitleKeywords(isManualStart: boolean | undefined, titleKeywords: string | undefined): boolean;
94
+ /**
95
+ * 逆向格式化"xxxB", "xxxKB", "xxxMB", "xxxGB"为字节数,如果值为空返回0,如果为数字则直接返回数字,如果带单位则转换为字节数
96
+ * @param sizeStr 大小字符串
97
+ * @returns 字节数
98
+ */
99
+ export declare function parseSizeToBytes(sizeStr: string): number | string;
100
+ export declare const byte2MB: (bytes: number) => number;
101
+ export declare function checkTitleKeywordsWhileRecording(recorder: Recorder, isManualStart: boolean | undefined, getInfo: (channelId: string) => Promise<{
102
+ title: string;
103
+ }>): Promise<boolean>;
104
+ /**
105
+ * 检查开始录制前的标题关键词
106
+ * @param title 直播间标题
107
+ * @param recorder 录制器实例
108
+ * @param isManualStart 是否手动启动
109
+ * @returns 如果标题包含关键词返回 true(不应录制),否则返回 false
110
+ */
111
+ export declare function checkTitleKeywordsBeforeRecord(title: string, recorder: Recorder, isManualStart: boolean | undefined): boolean;
85
112
  declare const _default: {
86
113
  replaceExtName: typeof replaceExtName;
87
114
  singleton: typeof singleton;
@@ -103,6 +130,9 @@ declare const _default: {
103
130
  isBetweenTimeRange: (range: undefined | [] | [string | null, string | null]) => boolean;
104
131
  hasBlockedTitleKeywords: typeof hasBlockedTitleKeywords;
105
132
  shouldCheckTitleKeywords: typeof shouldCheckTitleKeywords;
133
+ shouldUseStrictQuality: typeof shouldUseStrictQuality;
106
134
  sleep: (ms: number) => Promise<unknown>;
135
+ checkTitleKeywordsWhileRecording: typeof checkTitleKeywordsWhileRecording;
136
+ checkTitleKeywordsBeforeRecord: typeof checkTitleKeywordsBeforeRecord;
107
137
  };
108
138
  export default _default;