@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 +4 -2
- package/lib/index.js +61 -16
- package/lib/stream.d.ts +1 -0
- package/lib/stream.js +11 -0
- package/package.json +3 -3
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直播间接口 |
|
|
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
|
-
|
|
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
|
-
|
|
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
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.
|
|
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.
|
|
42
|
-
"douyin-danma-listener": "0.2.
|
|
41
|
+
"@bililive-tools/manager": "^1.8.0",
|
|
42
|
+
"douyin-danma-listener": "0.2.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "*"
|