@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 +3 -1
- package/lib/index.js +60 -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
|
|
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,
|
|
@@ -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
|
-
|
|
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
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": "*"
|