@bililive-tools/manager 1.2.1 → 1.3.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,12 +1,14 @@
1
1
  import EventEmitter from "node:events";
2
2
  export declare class FFMPEGRecorder extends EventEmitter {
3
3
  private onEnd;
4
+ private onUpdateLiveInfo;
4
5
  private command;
5
6
  private streamManager;
6
7
  private timeoutChecker;
7
8
  hasSegment: boolean;
8
9
  getSavePath: (data: {
9
10
  startTime: number;
11
+ title?: string;
10
12
  }) => string;
11
13
  segment: number;
12
14
  ffmpegOutputOptions: string[];
@@ -14,10 +16,14 @@ export declare class FFMPEGRecorder extends EventEmitter {
14
16
  isHls: boolean;
15
17
  disableDanma: boolean;
16
18
  url: string;
19
+ headers: {
20
+ [key: string]: string | undefined;
21
+ } | undefined;
17
22
  constructor(opts: {
18
23
  url: string;
19
24
  getSavePath: (data: {
20
25
  startTime: number;
26
+ title?: string;
21
27
  }) => string;
22
28
  segment: number;
23
29
  outputOptions: string[];
@@ -25,8 +31,14 @@ export declare class FFMPEGRecorder extends EventEmitter {
25
31
  isHls?: boolean;
26
32
  disableDanma?: boolean;
27
33
  videoFormat?: "auto" | "ts" | "mkv";
28
- }, onEnd: (...args: unknown[]) => void);
29
- private createCommand;
34
+ headers?: {
35
+ [key: string]: string | undefined;
36
+ };
37
+ }, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
38
+ title?: string;
39
+ cover?: string;
40
+ }>);
41
+ createCommand(): import("@renmu/fluent-ffmpeg").FfmpegCommand;
30
42
  formatLine(line: string): {
31
43
  time: string | null;
32
44
  } | null;
@@ -3,6 +3,7 @@ import { createFFMPEGBuilder, StreamManager, utils } from "./index.js";
3
3
  import { createInvalidStreamChecker, assert } from "./utils.js";
4
4
  export class FFMPEGRecorder extends EventEmitter {
5
5
  onEnd;
6
+ onUpdateLiveInfo;
6
7
  command;
7
8
  streamManager;
8
9
  timeoutChecker;
@@ -11,26 +12,36 @@ export class FFMPEGRecorder extends EventEmitter {
11
12
  segment;
12
13
  ffmpegOutputOptions = [];
13
14
  inputOptions = [];
14
- isHls = false;
15
+ isHls;
15
16
  disableDanma = false;
16
17
  url;
17
- constructor(opts, onEnd) {
18
+ headers;
19
+ constructor(opts, onEnd, onUpdateLiveInfo) {
18
20
  super();
19
21
  this.onEnd = onEnd;
22
+ this.onUpdateLiveInfo = onUpdateLiveInfo;
20
23
  const hasSegment = !!opts.segment;
21
24
  this.disableDanma = opts.disableDanma ?? false;
22
- this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat);
23
- this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3);
25
+ this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat, {
26
+ onUpdateLiveInfo: this.onUpdateLiveInfo,
27
+ });
28
+ this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
24
29
  this.hasSegment = hasSegment;
25
30
  this.getSavePath = opts.getSavePath;
26
31
  this.ffmpegOutputOptions = opts.outputOptions;
27
32
  this.inputOptions = opts.inputOptions ?? [];
28
33
  this.url = opts.url;
29
34
  this.segment = opts.segment;
30
- this.isHls = opts.isHls ?? false;
35
+ this.headers = opts.headers;
36
+ if (opts.isHls === undefined) {
37
+ this.isHls = this.url.includes("m3u8");
38
+ }
39
+ else {
40
+ this.isHls = opts.isHls;
41
+ }
31
42
  this.command = this.createCommand();
32
- this.streamManager.on("videoFileCreated", ({ filename }) => {
33
- this.emit("videoFileCreated", { filename });
43
+ this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
44
+ this.emit("videoFileCreated", { filename, cover });
34
45
  });
35
46
  this.streamManager.on("videoFileCompleted", ({ filename }) => {
36
47
  this.emit("videoFileCompleted", { filename });
@@ -40,15 +51,28 @@ export class FFMPEGRecorder extends EventEmitter {
40
51
  });
41
52
  }
42
53
  createCommand() {
54
+ this.timeoutChecker?.start();
43
55
  const invalidCount = this.isHls ? 35 : 15;
44
56
  const isInvalidStream = createInvalidStreamChecker(invalidCount);
45
- const command = createFFMPEGBuilder()
46
- .input(this.url)
47
- .inputOptions([
57
+ const inputOptions = [
48
58
  ...this.inputOptions,
49
59
  "-user_agent",
50
60
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
51
- ])
61
+ ];
62
+ if (this.headers) {
63
+ const headers = [];
64
+ Object.entries(this.headers).forEach(([key, value]) => {
65
+ if (!value)
66
+ return;
67
+ headers.push(`${key}:${value}`);
68
+ });
69
+ if (headers.length) {
70
+ inputOptions.push("-headers", headers.join("\\r\\n"));
71
+ }
72
+ }
73
+ const command = createFFMPEGBuilder()
74
+ .input(this.url)
75
+ .inputOptions(inputOptions)
52
76
  .outputOptions(this.ffmpegOutputOptions)
53
77
  .output(this.streamManager.videoFilePath)
54
78
  .on("error", this.onEnd)
@@ -56,7 +80,6 @@ export class FFMPEGRecorder extends EventEmitter {
56
80
  .on("stderr", async (stderrLine) => {
57
81
  assert(typeof stderrLine === "string");
58
82
  await this.streamManager.handleVideoStarted(stderrLine);
59
- // TODO:解析时间
60
83
  this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
61
84
  const info = this.formatLine(stderrLine);
62
85
  if (info) {
@@ -66,7 +89,7 @@ export class FFMPEGRecorder extends EventEmitter {
66
89
  this.onEnd("invalid stream");
67
90
  }
68
91
  })
69
- .on("stderr", this.timeoutChecker.update);
92
+ .on("stderr", this.timeoutChecker?.update);
70
93
  if (this.hasSegment) {
71
94
  command.outputOptions("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
72
95
  }
@@ -94,8 +117,9 @@ export class FFMPEGRecorder extends EventEmitter {
94
117
  async stop() {
95
118
  this.timeoutChecker.stop();
96
119
  try {
120
+ this.command.kill("SIGINT");
97
121
  // @ts-ignore
98
- this.command.ffmpegProc?.stdin?.write("q");
122
+ // this.command.ffmpegProc?.stdin?.write("q");
99
123
  await this.streamManager.handleVideoCompleted();
100
124
  }
101
125
  catch (err) {
package/lib/common.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare const Qualities: readonly ["lowest", "low", "medium", "high", "hi
4
4
  export declare const BiliQualities: readonly [30000, 20000, 10000, 400, 250, 150, 80];
5
5
  export declare const DouyuQualities: readonly [0, 2, 3, 4, 8];
6
6
  export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
7
- export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao"];
7
+ export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
8
8
  export type Quality = (typeof Qualities)[number] | (typeof BiliQualities)[number] | (typeof DouyuQualities)[number] | (typeof HuYaQualities)[number] | (typeof DouYinQualities)[number];
9
9
  export interface MessageSender<E extends AnyObject = UnknownObject> {
10
10
  uid?: string;
package/lib/common.js CHANGED
@@ -3,4 +3,4 @@ export const BiliQualities = [30000, 20000, 10000, 400, 250, 150, 80];
3
3
  export const DouyuQualities = [0, 2, 3, 4, 8];
4
4
  // 14100: 2K HDR;14000:2K;4200:HDR(10M);0:原画;8000:蓝光8M;4000:蓝光4M;2000:超清;500:流畅
5
5
  export const HuYaQualities = [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
6
- export const DouYinQualities = ["origin", "uhd", "hd", "sd", "ld", "ao"];
6
+ export const DouYinQualities = ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
package/lib/manager.d.ts CHANGED
@@ -36,6 +36,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
36
36
  videoFileCreated: {
37
37
  recorder: Recorder<E>;
38
38
  filename: string;
39
+ cover?: string;
39
40
  };
40
41
  videoFileCompleted: {
41
42
  recorder: Recorder<E>;
@@ -75,6 +76,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
75
76
  removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
76
77
  startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
77
78
  stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
79
+ cutRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
78
80
  autoCheckInterval: number;
79
81
  isCheckLoopRunning: boolean;
80
82
  startCheckLoop: (this: RecorderManager<ME, P, PE, E>) => void;
package/lib/manager.js CHANGED
@@ -104,10 +104,10 @@ export function createRecorderManager(opts) {
104
104
  this.recorders.push(recorder);
105
105
  recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder, recordHandle }));
106
106
  recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder, recordHandle }));
107
- recorder.on("videoFileCreated", ({ filename }) => {
107
+ recorder.on("videoFileCreated", ({ filename, cover }) => {
108
108
  if (recorder.saveCover && recorder?.liveInfo?.cover) {
109
109
  const coverPath = replaceExtName(filename, ".jpg");
110
- downloadImage(recorder?.liveInfo?.cover, coverPath);
110
+ downloadImage(cover ?? recorder?.liveInfo?.cover, coverPath);
111
111
  }
112
112
  this.emit("videoFileCreated", { recorder, filename });
113
113
  });
@@ -119,8 +119,10 @@ export function createRecorderManager(opts) {
119
119
  recorder.on("progress", (progress) => {
120
120
  this.emit("RecorderProgress", { recorder, progress });
121
121
  });
122
- recorder.on("LiveStart", ({ liveId }) => {
123
- const key = `${recorder.channelId}-${liveId}`;
122
+ recorder.on("videoFileCreated", () => {
123
+ if (!recorder.liveInfo?.liveId)
124
+ return;
125
+ const key = `${recorder.channelId}-${recorder.liveInfo?.liveId}`;
124
126
  if (liveStartObj[key])
125
127
  return;
126
128
  liveStartObj[key] = true;
@@ -168,6 +170,15 @@ export function createRecorderManager(opts) {
168
170
  }
169
171
  return recorder;
170
172
  },
173
+ async cutRecord(id) {
174
+ const recorder = this.recorders.find((item) => item.id === id);
175
+ if (recorder == null)
176
+ return;
177
+ if (recorder.recordHandle == null)
178
+ return;
179
+ await recorder.recordHandle.cut();
180
+ return recorder;
181
+ },
171
182
  autoCheckInterval: opts.autoCheckInterval ?? 1000,
172
183
  isCheckLoopRunning: false,
173
184
  startCheckLoop() {
@@ -221,7 +232,7 @@ export function createRecorderManager(opts) {
221
232
  *
222
233
  * TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
223
234
  */
224
- " -min_frag_duration 60000000",
235
+ " -min_frag_duration 10000000",
225
236
  };
226
237
  const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
227
238
  const args = parseArgsStringToArgv(ffmpegOutputArgs);
@@ -62,7 +62,7 @@ export function convert2Xml(data) {
62
62
  const comments = data.messages
63
63
  .filter((item) => item.type === "comment")
64
64
  .map((ele) => {
65
- const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
65
+ const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
66
66
  const data = {
67
67
  "@@p": "",
68
68
  "@@progress": progress,
@@ -93,7 +93,7 @@ export function convert2Xml(data) {
93
93
  const gifts = data.messages
94
94
  .filter((item) => item.type === "give_gift")
95
95
  .map((ele) => {
96
- const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
96
+ const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
97
97
  const data = {
98
98
  "@@ts": progress,
99
99
  "@@giftname": String(ele.name),
@@ -108,7 +108,7 @@ export function convert2Xml(data) {
108
108
  const superChats = data.messages
109
109
  .filter((item) => item.type === "super_chat")
110
110
  .map((ele) => {
111
- const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
111
+ const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
112
112
  const data = {
113
113
  "@@ts": progress,
114
114
  "@@price": String(ele.price * 1000),
@@ -122,7 +122,7 @@ export function convert2Xml(data) {
122
122
  const guardGift = data.messages
123
123
  .filter((item) => item.type === "guard")
124
124
  .map((ele) => {
125
- const progress = (ele.timestamp - metadata?.recordStartTimestamp) / 1000;
125
+ const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
126
126
  const data = {
127
127
  "@@ts": progress,
128
128
  "@@price": String(ele.price * 1000),
package/lib/recorder.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
14
14
  quality: Quality;
15
15
  streamPriorities: string[];
16
16
  sourcePriorities: string[];
17
- formatPriorities?: string[];
17
+ formatPriorities?: Array<"flv" | "hls">;
18
18
  source?: string;
19
19
  segment?: number;
20
20
  saveGiftDanma?: boolean;
@@ -41,6 +41,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
41
41
  titleKeywords?: string;
42
42
  /** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
43
43
  videoFormat?: "auto" | "ts" | "mkv";
44
+ /** 流格式优先级 */
45
+ formatriorities?: Array<"flv" | "hls">;
44
46
  extra?: Partial<E>;
45
47
  }
46
48
  export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id">;
@@ -57,6 +59,7 @@ export interface RecordHandle {
57
59
  progress?: Progress;
58
60
  savePath: string;
59
61
  stop: (this: RecordHandle, reason?: string, tempStopIntervalCheck?: boolean) => Promise<void>;
62
+ cut: (this: RecordHandle) => Promise<void>;
60
63
  }
61
64
  export interface DebugLog {
62
65
  type: string | "common" | "ffmpeg";
@@ -72,14 +75,12 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
72
75
  RecordSegment?: RecordHandle;
73
76
  videoFileCreated: {
74
77
  filename: string;
78
+ cover?: string;
75
79
  };
76
80
  videoFileCompleted: {
77
81
  filename: string;
78
82
  };
79
83
  progress: Progress;
80
- LiveStart: {
81
- liveId: string;
82
- };
83
84
  RecordStop: {
84
85
  recordHandle: RecordHandle;
85
86
  reason?: string;
@@ -2,6 +2,7 @@ import EventEmitter from "node:events";
2
2
  import { createRecordExtraDataController } from "./record_extra_data_controller.js";
3
3
  export type GetSavePath = (data: {
4
4
  startTime: number;
5
+ title?: string;
5
6
  }) => string;
6
7
  export declare class Segment extends EventEmitter {
7
8
  extraDataController: ReturnType<typeof createRecordExtraDataController> | null;
@@ -15,7 +16,12 @@ export declare class Segment extends EventEmitter {
15
16
  videoExt: "ts" | "mkv" | "mp4";
16
17
  constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: "ts" | "mkv" | "mp4");
17
18
  handleSegmentEnd(): Promise<void>;
18
- onSegmentStart(stderrLine: string): Promise<void>;
19
+ onSegmentStart(stderrLine: string, callBack?: {
20
+ onUpdateLiveInfo: () => Promise<{
21
+ title?: string;
22
+ cover?: string;
23
+ }>;
24
+ }): Promise<void>;
19
25
  get outputFilePath(): string;
20
26
  }
21
27
  export declare class StreamManager extends EventEmitter {
@@ -25,7 +31,13 @@ export declare class StreamManager extends EventEmitter {
25
31
  recordStartTime?: number;
26
32
  hasSegment: boolean;
27
33
  private videoFormat?;
28
- constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, videoFormat?: "auto" | "ts" | "mkv");
34
+ private callBack?;
35
+ constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, videoFormat?: "auto" | "ts" | "mkv", callBack?: {
36
+ onUpdateLiveInfo: () => Promise<{
37
+ title?: string;
38
+ cover?: string;
39
+ }>;
40
+ });
29
41
  handleVideoStarted(stderrLine: string): Promise<void>;
30
42
  handleVideoCompleted(): Promise<void>;
31
43
  getExtraDataController(): import("./record_extra_data_controller.js").RecordExtraDataController | null;
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from "node:events";
2
- import fs from "fs-extra";
2
+ import fs from "fs/promises";
3
3
  import { createRecordExtraDataController } from "./record_extra_data_controller.js";
4
- import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isFfmpegStart } from "./utils.js";
4
+ import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isFfmpegStart, retry, } from "./utils.js";
5
5
  export class Segment extends EventEmitter {
6
6
  extraDataController = null;
7
7
  init = true;
@@ -28,7 +28,7 @@ export class Segment extends EventEmitter {
28
28
  }
29
29
  try {
30
30
  await Promise.all([
31
- fs.rename(this.rawRecordingVideoPath, this.outputFilePath),
31
+ retry(() => fs.rename(this.rawRecordingVideoPath, this.outputFilePath)),
32
32
  this.extraDataController?.flush(),
33
33
  ]);
34
34
  this.emit("videoFileCompleted", { filename: this.outputFilePath });
@@ -40,14 +40,27 @@ export class Segment extends EventEmitter {
40
40
  });
41
41
  }
42
42
  }
43
- async onSegmentStart(stderrLine) {
43
+ async onSegmentStart(stderrLine, callBack) {
44
44
  if (!this.init) {
45
45
  await this.handleSegmentEnd();
46
46
  }
47
47
  this.init = false;
48
48
  const startTime = Date.now();
49
+ let liveInfo = { title: "", cover: "" };
50
+ if (callBack?.onUpdateLiveInfo) {
51
+ try {
52
+ liveInfo = await callBack.onUpdateLiveInfo();
53
+ }
54
+ catch (err) {
55
+ this.emit("DebugLog", {
56
+ type: "common",
57
+ text: "onUpdateLiveInfo error " + String(err),
58
+ });
59
+ }
60
+ }
49
61
  this.outputVideoFilePath = this.getSavePath({
50
62
  startTime: startTime,
63
+ title: liveInfo?.title,
51
64
  });
52
65
  ensureFolderExist(this.outputVideoFilePath);
53
66
  if (!this.disableDanma) {
@@ -58,7 +71,11 @@ export class Segment extends EventEmitter {
58
71
  if (match) {
59
72
  const filename = match[1];
60
73
  this.rawRecordingVideoPath = filename;
61
- this.emit("videoFileCreated", { filename: this.outputFilePath });
74
+ this.emit("videoFileCreated", {
75
+ filename: this.outputFilePath,
76
+ title: liveInfo?.title,
77
+ cover: liveInfo?.cover,
78
+ });
62
79
  }
63
80
  else {
64
81
  this.emit("DebugLog", { type: "ffmpeg", text: "No match found" });
@@ -75,12 +92,14 @@ export class StreamManager extends EventEmitter {
75
92
  recordStartTime;
76
93
  hasSegment;
77
94
  videoFormat;
78
- constructor(getSavePath, hasSegment, disableDanma, videoFormat) {
95
+ callBack;
96
+ constructor(getSavePath, hasSegment, disableDanma, videoFormat, callBack) {
79
97
  super();
80
98
  const recordSavePath = getSavePath({ startTime: Date.now() });
81
99
  this.recordSavePath = recordSavePath;
82
100
  this.videoFormat = videoFormat;
83
101
  this.hasSegment = hasSegment;
102
+ this.callBack = callBack;
84
103
  if (hasSegment) {
85
104
  this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
86
105
  this.segment.on("DebugLog", (data) => {
@@ -103,7 +122,7 @@ export class StreamManager extends EventEmitter {
103
122
  async handleVideoStarted(stderrLine) {
104
123
  if (this.segment) {
105
124
  if (isFfmpegStartSegment(stderrLine)) {
106
- await this.segment.onSegmentStart(stderrLine);
125
+ await this.segment.onSegmentStart(stderrLine, this.callBack);
107
126
  }
108
127
  }
109
128
  else {
package/lib/utils.d.ts CHANGED
@@ -39,9 +39,10 @@ export declare function isFfmpegStartSegment(line: string): boolean;
39
39
  export declare function isFfmpegStart(line: string): boolean;
40
40
  export declare const formatTemplate: (string: string, ...args: any[]) => string;
41
41
  export declare function createInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => boolean;
42
- export declare function createTimeoutChecker(onTimeout: () => void, time: number): {
42
+ export declare function createTimeoutChecker(onTimeout: () => void, time: number, autoStart?: boolean): {
43
43
  update: () => void;
44
44
  stop: () => void;
45
+ start: () => void;
45
46
  };
46
47
  export declare function downloadImage(imageUrl: string, savePath: string): Promise<void>;
47
48
  /**
@@ -52,6 +53,14 @@ export declare function downloadImage(imageUrl: string, savePath: string): Promi
52
53
  * @returns 排序后的对象数组
53
54
  */
54
55
  export declare function sortByKeyOrder<T, K extends keyof T>(objects: T[], order: T[K][], key: K): T[];
56
+ /**
57
+ * 重试执行异步函数
58
+ * @param fn 要重试的异步函数
59
+ * @param retries 重试次数,默认为3次
60
+ * @param delay 重试延迟时间(毫秒),默认为1000ms
61
+ * @returns Promise
62
+ */
63
+ export declare function retry<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
55
64
  declare const _default: {
56
65
  replaceExtName: typeof replaceExtName;
57
66
  singleton: typeof singleton;
@@ -69,5 +78,6 @@ declare const _default: {
69
78
  md5: (str: string) => string;
70
79
  uuid: () => `${string}-${string}-${string}-${string}-${string}`;
71
80
  sortByKeyOrder: typeof sortByKeyOrder;
81
+ retry: typeof retry;
72
82
  };
73
83
  export default _default;
package/lib/utils.js CHANGED
@@ -177,7 +177,7 @@ export function createInvalidStreamChecker(count = 15) {
177
177
  return false;
178
178
  };
179
179
  }
180
- export function createTimeoutChecker(onTimeout, time) {
180
+ export function createTimeoutChecker(onTimeout, time, autoStart = true) {
181
181
  let timer = null;
182
182
  let stopped = false;
183
183
  const update = () => {
@@ -190,7 +190,13 @@ export function createTimeoutChecker(onTimeout, time) {
190
190
  onTimeout();
191
191
  }, time);
192
192
  };
193
- update();
193
+ const start = () => {
194
+ stopped = false;
195
+ update();
196
+ };
197
+ if (autoStart) {
198
+ start();
199
+ }
194
200
  return {
195
201
  update,
196
202
  stop() {
@@ -199,6 +205,7 @@ export function createTimeoutChecker(onTimeout, time) {
199
205
  clearTimeout(timer);
200
206
  timer = null;
201
207
  },
208
+ start,
202
209
  };
203
210
  }
204
211
  export async function downloadImage(imageUrl, savePath) {
@@ -231,6 +238,25 @@ export function sortByKeyOrder(objects, order, key) {
231
238
  return indexA - indexB;
232
239
  });
233
240
  }
241
+ /**
242
+ * 重试执行异步函数
243
+ * @param fn 要重试的异步函数
244
+ * @param retries 重试次数,默认为3次
245
+ * @param delay 重试延迟时间(毫秒),默认为1000ms
246
+ * @returns Promise
247
+ */
248
+ export async function retry(fn, retries = 3, delay = 1000) {
249
+ try {
250
+ return await fn();
251
+ }
252
+ catch (err) {
253
+ if (retries <= 0) {
254
+ throw err;
255
+ }
256
+ await new Promise((resolve) => setTimeout(resolve, delay));
257
+ return retry(fn, retries - 1, delay);
258
+ }
259
+ }
234
260
  export default {
235
261
  replaceExtName,
236
262
  singleton,
@@ -248,4 +274,5 @@ export default {
248
274
  md5,
249
275
  uuid,
250
276
  sortByKeyOrder,
277
+ retry,
251
278
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/manager",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Batch scheduling recorders",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",