@bililive-tools/douyin-recorder 1.7.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.
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
 
@@ -85,7 +87,7 @@ const { id } = await provider.resolveChannelInfoFromURL(url);
85
87
  | 描述 | 备注 |
86
88
  | ---------------- | ---------------------------------------- |
87
89
  | web直播间接口 | 效果不错 |
88
- | mobile直播间接口 | 易风控,无验证码,海外IP可能无法使用 |
90
+ | mobile直播间接口 | 不易风控,无验证码,海外IP可能无法使用 |
89
91
  | 直播间web解析 | 易风控,有验证码,单个接口1M流量 |
90
92
  | 用户web解析 | 不易风控,海外IP无法使用,单个接口1M流量 |
91
93
  | 负载均衡 | 使用负载均衡算法来分摊防止风控 |
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,
@@ -87,6 +115,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
87
115
  uid: this.uid,
88
116
  });
89
117
  this.liveInfo = liveInfo;
118
+ this.state = "idle";
90
119
  }
91
120
  catch (error) {
92
121
  this.state = "check-error";
@@ -102,6 +131,18 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
102
131
  return null;
103
132
  if (!this.liveInfo.living)
104
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
+ }
105
146
  let res;
106
147
  try {
107
148
  let strictQuality = false;
@@ -164,8 +205,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
164
205
  const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
165
206
  this.recordHandle?.stop(reason);
166
207
  };
167
- let recorderType = this.recorderType === "mesio" ? "mesio" : "ffmpeg";
168
- const recorder = createBaseRecorder(recorderType, {
208
+ const recorder = createBaseRecorder(this.recorderType, {
169
209
  url: stream.url,
170
210
  outputOptions: ffmpegOutputOptions,
171
211
  inputOptions: ffmpegInputOptions,
@@ -173,6 +213,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
173
213
  getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
174
214
  disableDanma: this.disableProvideCommentsWhenRecording,
175
215
  videoFormat: this.videoFormat ?? "auto",
216
+ debugLevel: this.debugLevel ?? "none",
217
+ onlyAudio: stream.onlyAudio,
176
218
  headers: {
177
219
  Cookie: this.auth,
178
220
  },
@@ -191,8 +233,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
191
233
  this.state = "idle";
192
234
  throw err;
193
235
  }
194
- const handleVideoCreated = async ({ filename, title, cover }) => {
195
- this.emit("videoFileCreated", { filename, cover });
236
+ const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
237
+ this.emit("videoFileCreated", { filename, cover, rawFilename });
196
238
  if (title && this?.liveInfo) {
197
239
  this.liveInfo.title = title;
198
240
  }
@@ -252,6 +294,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
252
294
  return;
253
295
  if (this.saveGiftDanma === false)
254
296
  return;
297
+ // repeatEnd 表示礼物连击完毕,只记录这个礼物
298
+ if (!msg.repeatEnd)
299
+ return;
255
300
  const serverTimestamp = Number(msg.common.createTime) > 9999999999
256
301
  ? Number(msg.common.createTime)
257
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.0",
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": "*"