@bililive-tools/manager 1.3.0 → 1.4.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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ import { provider } from "@bililive-tools/bilibili-recorder";
28
28
 
29
29
  const manager = createRecorderManager({
30
30
  providers: [provider],
31
- savePathRule: "D:\\录制\\{platforme}}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}", // 保存路径,占位符见文档
31
+ savePathRule: "D:\\录制\\{platforme}}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}", // 保存路径,占位符见文档,支持 [ejs](https://ejs.co/) 模板引擎
32
32
  autoCheckInterval: 1000 * 60, // 自动检查间隔,单位秒
33
33
  autoRemoveSystemReservedChars: true, // 移除系统非法字符串
34
34
  biliBatchQuery: false, // B站检查使用批量接口
@@ -52,7 +52,7 @@ export class FFMPEGRecorder extends EventEmitter {
52
52
  }
53
53
  createCommand() {
54
54
  this.timeoutChecker?.start();
55
- const invalidCount = this.isHls ? 35 : 15;
55
+ const invalidCount = this.isHls ? 35 : 18;
56
56
  const isInvalidStream = createInvalidStreamChecker(invalidCount);
57
57
  const inputOptions = [
58
58
  ...this.inputOptions,
@@ -117,13 +117,18 @@ export class FFMPEGRecorder extends EventEmitter {
117
117
  async stop() {
118
118
  this.timeoutChecker.stop();
119
119
  try {
120
- this.command.kill("SIGINT");
121
- // @ts-ignore
122
- // this.command.ffmpegProc?.stdin?.write("q");
120
+ // ts文件使用write("q")需要十来秒进行处理,直接中断,其他格式使用sigint会导致缺少数据
121
+ if (this.streamManager.videoFilePath.endsWith(".ts")) {
122
+ this.command.kill("SIGINT");
123
+ }
124
+ else {
125
+ // @ts-ignore
126
+ this.command.ffmpegProc?.stdin?.write("q");
127
+ }
123
128
  await this.streamManager.handleVideoCompleted();
124
129
  }
125
130
  catch (err) {
126
- this.emit("DebugLog", { type: "common", text: String(err) });
131
+ this.emit("DebugLog", { type: "error", text: String(err) });
127
132
  }
128
133
  }
129
134
  getExtraDataController() {
package/lib/manager.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import mitt from "mitt";
3
+ import ejs from "ejs";
3
4
  import { omit, range } from "lodash-es";
4
5
  import { parseArgsStringToArgv } from "string-argv";
5
6
  import { getBiliStatusInfoByRoomIds } from "./api.js";
@@ -271,9 +272,16 @@ export function genSavePathFromRule(manager, recorder, extData) {
271
272
  };
272
273
  if (manager.autoRemoveSystemReservedChars) {
273
274
  for (const key in params) {
274
- params[key] = removeSystemReservedChars(String(params[key]));
275
+ params[key] = removeSystemReservedChars(String(params[key])).trim();
275
276
  }
276
277
  }
277
- return formatTemplate(manager.savePathRule, params);
278
+ let savePathRule = manager.savePathRule;
279
+ try {
280
+ savePathRule = ejs.render(savePathRule, params);
281
+ }
282
+ catch (error) {
283
+ console.error("模板解析错误", error);
284
+ }
285
+ return formatTemplate(savePathRule, params);
278
286
  }
279
287
  export { StreamManager };
@@ -14,6 +14,7 @@ export function createRecordExtraDataController(savePath) {
14
14
  },
15
15
  messages: [],
16
16
  };
17
+ let hasCompleted = false;
17
18
  const scheduleSave = asyncThrottle(() => save(), 30e3, {
18
19
  immediateRunWhenEndOfDefer: true,
19
20
  });
@@ -22,10 +23,14 @@ export function createRecordExtraDataController(savePath) {
22
23
  };
23
24
  // TODO: 将所有数据存放在内存中可能存在问题
24
25
  const addMessage = (comment) => {
26
+ if (hasCompleted)
27
+ return;
25
28
  data.messages.push(comment);
26
29
  scheduleSave();
27
30
  };
28
31
  const setMeta = (meta) => {
32
+ if (hasCompleted)
33
+ return;
29
34
  data.meta = {
30
35
  ...data.meta,
31
36
  ...meta,
@@ -33,13 +38,16 @@ export function createRecordExtraDataController(savePath) {
33
38
  scheduleSave();
34
39
  };
35
40
  const flush = async () => {
36
- scheduleSave.flush();
41
+ if (hasCompleted)
42
+ return;
43
+ hasCompleted = true;
37
44
  scheduleSave.cancel();
38
45
  const xmlContent = convert2Xml(data);
39
46
  const parsedPath = path.parse(savePath);
40
47
  const xmlPath = path.join(parsedPath.dir, parsedPath.name + ".xml");
41
48
  await fs.promises.writeFile(xmlPath, xmlContent);
42
49
  await fs.promises.rm(savePath);
50
+ data.messages = [];
43
51
  };
44
52
  return {
45
53
  data,
@@ -76,6 +84,7 @@ export function convert2Xml(data) {
76
84
  "@@weight": String(0),
77
85
  "@@user": String(ele.sender?.name),
78
86
  "@@uid": String(ele?.sender?.uid),
87
+ "@@timestamp": String(ele.timestamp),
79
88
  };
80
89
  data["@@p"] = [
81
90
  data["@@progress"],
@@ -88,7 +97,7 @@ export function convert2Xml(data) {
88
97
  data["@@uid"],
89
98
  data["@@weight"],
90
99
  ].join(",");
91
- return pick(data, ["@@p", "#text", "@@user", "@@uid"]);
100
+ return pick(data, ["@@p", "#text", "@@user", "@@uid", "@@timestamp"]);
92
101
  });
93
102
  const gifts = data.messages
94
103
  .filter((item) => item.type === "give_gift")
@@ -101,6 +110,7 @@ export function convert2Xml(data) {
101
110
  "@@price": String(ele.price * 1000),
102
111
  "@@user": String(ele.sender?.name),
103
112
  "@@uid": String(ele?.sender?.uid),
113
+ "@@timestamp": String(ele.timestamp),
104
114
  // "@@raw": JSON.stringify(ele),
105
115
  };
106
116
  return data;
@@ -115,6 +125,7 @@ export function convert2Xml(data) {
115
125
  "#text": String(ele.text),
116
126
  "@@user": String(ele.sender?.name),
117
127
  "@@uid": String(ele?.sender?.uid),
128
+ "@@timestamp": String(ele.timestamp),
118
129
  // "@@raw": JSON.stringify(ele),
119
130
  };
120
131
  return data;
@@ -131,6 +142,7 @@ export function convert2Xml(data) {
131
142
  "@@level": String(ele.level),
132
143
  "@@user": String(ele.sender?.name),
133
144
  "@@uid": String(ele?.sender?.uid),
145
+ "@@timestamp": String(ele.timestamp),
134
146
  // "@@raw": JSON.stringify(ele),
135
147
  };
136
148
  return data;
package/lib/recorder.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
27
27
  uid?: number;
28
28
  /** 画质匹配重试次数 */
29
29
  qualityRetry?: number;
30
+ /** 抖音是否使用双屏直播流,开启后如果是双屏直播,那么就使用拼接的流,默认为true */
31
+ doubleScreen?: boolean;
30
32
  /** B站是否使用m3u8代理 */
31
33
  useM3U8Proxy?: boolean;
32
34
  /**B站m3u8代理url */
@@ -43,6 +45,10 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
43
45
  videoFormat?: "auto" | "ts" | "mkv";
44
46
  /** 流格式优先级 */
45
47
  formatriorities?: Array<"flv" | "hls">;
48
+ /** 只录制音频 */
49
+ onlyAudio?: boolean;
50
+ /** 控制弹幕是否使用服务端时间戳 */
51
+ useServerTimestamp?: boolean;
46
52
  extra?: Partial<E>;
47
53
  }
48
54
  export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id">;
@@ -62,7 +68,7 @@ export interface RecordHandle {
62
68
  cut: (this: RecordHandle) => Promise<void>;
63
69
  }
64
70
  export interface DebugLog {
65
- type: string | "common" | "ffmpeg";
71
+ type: string | "common" | "ffmpeg" | "error";
66
72
  text: string;
67
73
  }
68
74
  export type GetSavePath = (data: {
@@ -21,21 +21,21 @@ export class Segment extends EventEmitter {
21
21
  async handleSegmentEnd() {
22
22
  if (!this.outputVideoFilePath) {
23
23
  this.emit("DebugLog", {
24
- type: "common",
24
+ type: "error",
25
25
  text: "Should call onSegmentStart first",
26
26
  });
27
27
  return;
28
28
  }
29
29
  try {
30
30
  await Promise.all([
31
- retry(() => fs.rename(this.rawRecordingVideoPath, this.outputFilePath)),
31
+ retry(() => fs.rename(this.rawRecordingVideoPath, this.outputFilePath), 10, 2000),
32
32
  this.extraDataController?.flush(),
33
33
  ]);
34
34
  this.emit("videoFileCompleted", { filename: this.outputFilePath });
35
35
  }
36
36
  catch (err) {
37
37
  this.emit("DebugLog", {
38
- type: "common",
38
+ type: "error",
39
39
  text: "videoFileCompleted error " + String(err),
40
40
  });
41
41
  }
@@ -53,7 +53,7 @@ export class Segment extends EventEmitter {
53
53
  }
54
54
  catch (err) {
55
55
  this.emit("DebugLog", {
56
- type: "common",
56
+ type: "error",
57
57
  text: "onUpdateLiveInfo error " + String(err),
58
58
  });
59
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/manager",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Batch scheduling recorders",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -39,7 +39,8 @@
39
39
  "string-argv": "^0.3.2",
40
40
  "lodash-es": "^4.17.21",
41
41
  "axios": "^1.7.8",
42
- "fs-extra": "^11.2.0"
42
+ "fs-extra": "^11.2.0",
43
+ "ejs": "^3.1.10"
43
44
  },
44
45
  "scripts": {
45
46
  "build": "pnpm run test && tsc",