@bililive-tools/douyin-recorder 1.7.1 → 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.
package/README.md CHANGED
@@ -45,10 +45,12 @@ interface Options {
45
45
  videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
46
46
  useServerTimestamp?: boolean; // 控制弹幕是否使用服务端时间戳,默认为true
47
47
  doubleScreen?: boolean; // 是否使用双屏直播流,开启后如果是双屏直播,那么就使用拼接的流,默认为true
48
- recorderType?: "auto" | "ffmpeg" | "mesio"; // 底层录制器,使用mesio时videoFormat参数无效
48
+ recorderType?: "auto" | "ffmpeg" | "mesio" | "bililive"; // 底层录制器,使用mesio和bililive时videoFormat参数无效
49
49
  auth?: string; // 传递cookie
50
50
  uid?: string; // 参数为 sec_user_uid 参数
51
51
  api?: "web" | "webHTML" | "mobile" | "userHTML" | "balance" | "random"; // 使用不同的接口,默认使用web,具体区别见文档
52
+ titleKeywords?: string; // 禁止录制的标题关键字,英文逗号分开多个
53
+ debugLevel?: `verbose` | "basic"; // verbose参数时,录制器会输出更加详细的log
52
54
  }
53
55
  ```
54
56
 
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import mitt from "mitt";
3
- import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, createBaseRecorder, } from "@bililive-tools/manager";
3
+ import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, createBaseRecorder, } from "@bililive-tools/manager";
4
4
  import { getInfo, getStream } from "./stream.js";
5
5
  import { ensureFolderExist, singleton } from "./utils.js";
6
6
  import { resolveShortURL, parseUser } from "./douyin_api.js";
@@ -67,19 +67,47 @@ const ffmpegOutputOptions = [
67
67
  "-min_frag_duration",
68
68
  "10000000",
69
69
  ];
70
- const ffmpegInputOptions = [
71
- "-reconnect",
72
- "1",
73
- "-reconnect_streamed",
74
- "1",
75
- "-reconnect_delay_max",
76
- "10",
77
- "-rw_timeout",
78
- "15000000",
79
- ];
70
+ const ffmpegInputOptions = ["-rw_timeout", "10000000", "-timeout", "10000000"];
80
71
  const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
81
- if (this.recordHandle != null)
72
+ // 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
73
+ if (this.recordHandle != null) {
74
+ // 只有当设置了标题关键词时,并且不是手动启动的录制,才获取最新的直播间信息
75
+ if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
76
+ const now = Date.now();
77
+ // 每5分钟检查一次标题变化
78
+ const titleCheckInterval = 5 * 60 * 1000; // 5分钟
79
+ // 获取上次检查时间
80
+ const lastCheckTime = typeof this.extra.lastTitleCheckTime === "number" ? this.extra.lastTitleCheckTime : 0;
81
+ // 如果距离上次检查时间不足指定间隔,则跳过检查
82
+ if (now - lastCheckTime < titleCheckInterval) {
83
+ return this.recordHandle;
84
+ }
85
+ // 更新检查时间
86
+ this.extra.lastTitleCheckTime = now;
87
+ // 获取直播间信息
88
+ const liveInfo = await getInfo(this.channelId, {
89
+ cookie: this.auth,
90
+ api: this.api,
91
+ uid: this.uid,
92
+ });
93
+ const { title } = liveInfo;
94
+ // 检查标题是否包含关键词
95
+ if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
96
+ this.state = "title-blocked";
97
+ this.emit("DebugLog", {
98
+ type: "common",
99
+ text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
100
+ });
101
+ // 停止录制
102
+ await this.recordHandle.stop("直播间标题包含关键词");
103
+ // 返回 null,停止录制
104
+ return null;
105
+ }
106
+ }
107
+ // 已经在录制中,直接返回
82
108
  return this.recordHandle;
109
+ }
110
+ // 获取直播间信息
83
111
  try {
84
112
  const liveInfo = await getInfo(this.channelId, {
85
113
  cookie: this.auth,
@@ -103,6 +131,18 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
103
131
  return null;
104
132
  if (!this.liveInfo.living)
105
133
  return null;
134
+ // 检查标题是否包含关键词,如果包含则不自动录制
135
+ // 手动开始录制时不检查标题关键词
136
+ if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
137
+ if (utils.hasBlockedTitleKeywords(this.liveInfo.title, this.titleKeywords)) {
138
+ this.state = "title-blocked";
139
+ this.emit("DebugLog", {
140
+ type: "common",
141
+ text: `跳过录制:直播间标题 "${this.liveInfo.title}" 包含关键词 "${this.titleKeywords}"`,
142
+ });
143
+ return null;
144
+ }
145
+ }
106
146
  let res;
107
147
  try {
108
148
  let strictQuality = false;
@@ -165,8 +205,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
165
205
  const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
166
206
  this.recordHandle?.stop(reason);
167
207
  };
168
- let recorderType = this.recorderType === "mesio" ? "mesio" : "ffmpeg";
169
- const recorder = createBaseRecorder(recorderType, {
208
+ const recorder = createBaseRecorder(this.recorderType, {
170
209
  url: stream.url,
171
210
  outputOptions: ffmpegOutputOptions,
172
211
  inputOptions: ffmpegInputOptions,
@@ -174,6 +213,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
174
213
  getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
175
214
  disableDanma: this.disableProvideCommentsWhenRecording,
176
215
  videoFormat: this.videoFormat ?? "auto",
216
+ debugLevel: this.debugLevel ?? "none",
217
+ onlyAudio: stream.onlyAudio,
177
218
  headers: {
178
219
  Cookie: this.auth,
179
220
  },
@@ -192,8 +233,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
192
233
  this.state = "idle";
193
234
  throw err;
194
235
  }
195
- const handleVideoCreated = async ({ filename, title, cover }) => {
196
- this.emit("videoFileCreated", { filename, cover });
236
+ const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
237
+ this.emit("videoFileCreated", { filename, cover, rawFilename });
197
238
  if (title && this?.liveInfo) {
198
239
  this.liveInfo.title = title;
199
240
  }
@@ -253,6 +294,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
253
294
  return;
254
295
  if (this.saveGiftDanma === false)
255
296
  return;
297
+ // repeatEnd 表示礼物连击完毕,只记录这个礼物
298
+ if (!msg.repeatEnd)
299
+ return;
256
300
  const serverTimestamp = Number(msg.common.createTime) > 9999999999
257
301
  ? Number(msg.common.createTime)
258
302
  : Number(msg.common.createTime) * 1000;
package/lib/stream.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" |
29
29
  name: string;
30
30
  source: string;
31
31
  url: string;
32
+ onlyAudio: boolean;
32
33
  };
33
34
  living: boolean;
34
35
  roomId: string;
package/lib/stream.js CHANGED
@@ -69,12 +69,23 @@ export async function getStream(opts) {
69
69
  if (!url) {
70
70
  throw new Error("未找到对应的流");
71
71
  }
72
+ let onlyAudio = false;
73
+ try {
74
+ const urlObj = new URL(url);
75
+ if (urlObj.searchParams.get("only_audio") == "1") {
76
+ onlyAudio = true;
77
+ }
78
+ }
79
+ catch (error) {
80
+ console.warn("解析流 URL 失败", error);
81
+ }
72
82
  return {
73
83
  ...info,
74
84
  currentStream: {
75
85
  name: qualityName,
76
86
  source: "自动",
77
87
  url: url,
88
+ onlyAudio,
78
89
  },
79
90
  };
80
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/douyin-recorder",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "@bililive-tools douyin recorder implemention",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -38,8 +38,8 @@
38
38
  "lodash-es": "^4.17.21",
39
39
  "mitt": "^3.0.1",
40
40
  "sm-crypto": "^0.3.13",
41
- "@bililive-tools/manager": "^1.6.1",
42
- "douyin-danma-listener": "0.2.0"
41
+ "@bililive-tools/manager": "^1.8.0",
42
+ "douyin-danma-listener": "0.2.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "*"