@bililive-tools/douyin-recorder 1.8.0 → 1.9.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/lib/douyin_api.d.ts +5 -0
- package/lib/douyin_api.js +11 -3
- package/lib/index.js +48 -15
- package/lib/stream.js +4 -1
- package/package.json +3 -3
package/lib/douyin_api.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ import type { APIType, RealAPIType } from "./types.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function resolveShortURL(shortURL: string): Promise<string>;
|
|
8
8
|
export declare const getCookie: () => Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* 随机选择一个可用的 API 接口
|
|
11
|
+
* @returns 随机选择的 API 类型
|
|
12
|
+
*/
|
|
13
|
+
export declare function selectRandomAPI(exclude?: RealAPIType[]): RealAPIType;
|
|
9
14
|
export declare function getRoomInfo(webRoomId: string, opts?: {
|
|
10
15
|
auth?: string;
|
|
11
16
|
doubleScreen?: boolean;
|
package/lib/douyin_api.js
CHANGED
|
@@ -107,8 +107,16 @@ function generateNonce() {
|
|
|
107
107
|
* 随机选择一个可用的 API 接口
|
|
108
108
|
* @returns 随机选择的 API 类型
|
|
109
109
|
*/
|
|
110
|
-
function selectRandomAPI() {
|
|
110
|
+
export function selectRandomAPI(exclude) {
|
|
111
111
|
const availableAPIs = ["web", "webHTML", "mobile", "userHTML"];
|
|
112
|
+
if (exclude && exclude.length > 0) {
|
|
113
|
+
for (const api of exclude) {
|
|
114
|
+
const index = availableAPIs.indexOf(api);
|
|
115
|
+
if (index !== -1) {
|
|
116
|
+
availableAPIs.splice(index, 1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
112
120
|
const randomIndex = Math.floor(Math.random() * availableAPIs.length);
|
|
113
121
|
return availableAPIs[randomIndex];
|
|
114
122
|
}
|
|
@@ -151,7 +159,7 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
|
|
|
151
159
|
nickname: "",
|
|
152
160
|
sec_uid: "",
|
|
153
161
|
avatar: "",
|
|
154
|
-
api: "
|
|
162
|
+
api: "userHTML",
|
|
155
163
|
room: null,
|
|
156
164
|
};
|
|
157
165
|
}
|
|
@@ -244,7 +252,7 @@ async function getRoomInfoByHtml(webRoomId, opts = {}) {
|
|
|
244
252
|
nickname: roomInfo?.anchor?.nickname ?? "",
|
|
245
253
|
sec_uid: roomInfo?.anchor?.sec_uid ?? "",
|
|
246
254
|
avatar: roomInfo?.anchor?.avatar_thumb?.url_list?.[0] ?? "",
|
|
247
|
-
api: "
|
|
255
|
+
api: "webHTML",
|
|
248
256
|
room: {
|
|
249
257
|
title: roomInfo?.room?.title ?? "",
|
|
250
258
|
cover: roomInfo?.room?.cover?.url_list?.[0] ?? "",
|
package/lib/index.js
CHANGED
|
@@ -59,14 +59,7 @@ function createRecorder(opts) {
|
|
|
59
59
|
});
|
|
60
60
|
return recorderWithSupportUpdatedEvent;
|
|
61
61
|
}
|
|
62
|
-
const ffmpegOutputOptions = [
|
|
63
|
-
"-c",
|
|
64
|
-
"copy",
|
|
65
|
-
"-movflags",
|
|
66
|
-
"faststart+frag_keyframe+empty_moov",
|
|
67
|
-
"-min_frag_duration",
|
|
68
|
-
"10000000",
|
|
69
|
-
];
|
|
62
|
+
const ffmpegOutputOptions = [];
|
|
70
63
|
const ffmpegInputOptions = ["-rw_timeout", "10000000", "-timeout", "10000000"];
|
|
71
64
|
const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
|
|
72
65
|
// 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
|
|
@@ -181,7 +174,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
181
174
|
this.state = "check-error";
|
|
182
175
|
throw err;
|
|
183
176
|
}
|
|
184
|
-
const { owner, title } = this.liveInfo;
|
|
177
|
+
const { owner, title, startTime } = this.liveInfo;
|
|
185
178
|
this.state = "recording";
|
|
186
179
|
const { currentStream: stream, sources: availableSources, streams: availableStreams } = res;
|
|
187
180
|
this.availableStreams = availableStreams.map((s) => s.desc);
|
|
@@ -205,12 +198,19 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
205
198
|
const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
206
199
|
this.recordHandle?.stop(reason);
|
|
207
200
|
};
|
|
201
|
+
const recordStartTime = new Date();
|
|
208
202
|
const recorder = createBaseRecorder(this.recorderType, {
|
|
209
203
|
url: stream.url,
|
|
210
204
|
outputOptions: ffmpegOutputOptions,
|
|
211
205
|
inputOptions: ffmpegInputOptions,
|
|
212
206
|
segment: this.segment ?? 0,
|
|
213
|
-
getSavePath: (opts) => getSavePath({
|
|
207
|
+
getSavePath: (opts) => getSavePath({
|
|
208
|
+
owner,
|
|
209
|
+
title: opts.title ?? title,
|
|
210
|
+
startTime: opts.startTime,
|
|
211
|
+
liveStartTime: startTime,
|
|
212
|
+
recordStartTime,
|
|
213
|
+
}),
|
|
214
214
|
disableDanma: this.disableProvideCommentsWhenRecording,
|
|
215
215
|
videoFormat: this.videoFormat ?? "auto",
|
|
216
216
|
debugLevel: this.debugLevel ?? "none",
|
|
@@ -225,6 +225,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
225
225
|
const savePath = getSavePath({
|
|
226
226
|
owner,
|
|
227
227
|
title,
|
|
228
|
+
startTime: Date.now(),
|
|
229
|
+
liveStartTime: startTime,
|
|
230
|
+
recordStartTime,
|
|
228
231
|
});
|
|
229
232
|
try {
|
|
230
233
|
ensureFolderExist(savePath);
|
|
@@ -264,6 +267,10 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
264
267
|
}
|
|
265
268
|
this.emit("progress", progress);
|
|
266
269
|
});
|
|
270
|
+
// 礼物消息缓存管理
|
|
271
|
+
const giftMessageCache = new Map();
|
|
272
|
+
// 礼物延迟处理时间(毫秒),可根据实际情况调整
|
|
273
|
+
const GIFT_DELAY = 5000;
|
|
267
274
|
const client = new DouYinDanmaClient(this?.liveInfo?.liveId, {
|
|
268
275
|
cookie: this.auth,
|
|
269
276
|
});
|
|
@@ -294,9 +301,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
294
301
|
return;
|
|
295
302
|
if (this.saveGiftDanma === false)
|
|
296
303
|
return;
|
|
297
|
-
// repeatEnd 表示礼物连击完毕,只记录这个礼物
|
|
298
|
-
if (!msg.repeatEnd)
|
|
299
|
-
return;
|
|
300
304
|
const serverTimestamp = Number(msg.common.createTime) > 9999999999
|
|
301
305
|
? Number(msg.common.createTime)
|
|
302
306
|
: Number(msg.common.createTime) * 1000;
|
|
@@ -316,8 +320,25 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
316
320
|
// },
|
|
317
321
|
},
|
|
318
322
|
};
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
// 单独使用groupId并不可靠
|
|
324
|
+
const groupId = `${msg.groupId}_${msg.user.id}_${msg.giftId}`;
|
|
325
|
+
// 如果已存在相同 groupId 的礼物,清除旧的定时器
|
|
326
|
+
const existing = giftMessageCache.get(groupId);
|
|
327
|
+
if (existing) {
|
|
328
|
+
clearTimeout(existing.timer);
|
|
329
|
+
}
|
|
330
|
+
// 创建新的定时器
|
|
331
|
+
const timer = setTimeout(() => {
|
|
332
|
+
const cachedGift = giftMessageCache.get(groupId);
|
|
333
|
+
if (cachedGift) {
|
|
334
|
+
// 延迟时间到,添加最终的礼物消息
|
|
335
|
+
this.emit("Message", cachedGift.gift);
|
|
336
|
+
extraDataController.addMessage(cachedGift.gift);
|
|
337
|
+
giftMessageCache.delete(groupId);
|
|
338
|
+
}
|
|
339
|
+
}, GIFT_DELAY);
|
|
340
|
+
// 更新缓存
|
|
341
|
+
giftMessageCache.set(groupId, { gift, timer });
|
|
321
342
|
});
|
|
322
343
|
client.on("reconnect", (attempts) => {
|
|
323
344
|
this.emit("DebugLog", {
|
|
@@ -381,6 +402,17 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
381
402
|
return;
|
|
382
403
|
this.state = "stopping-record";
|
|
383
404
|
try {
|
|
405
|
+
// 清理所有礼物缓存定时器
|
|
406
|
+
for (const [_groupId, cached] of giftMessageCache.entries()) {
|
|
407
|
+
clearTimeout(cached.timer);
|
|
408
|
+
// 立即添加剩余的礼物消息
|
|
409
|
+
const extraDataController = recorder.getExtraDataController();
|
|
410
|
+
if (extraDataController) {
|
|
411
|
+
this.emit("Message", cached.gift);
|
|
412
|
+
extraDataController.addMessage(cached.gift);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
giftMessageCache.clear();
|
|
384
416
|
client.close();
|
|
385
417
|
await recorder.stop();
|
|
386
418
|
}
|
|
@@ -401,6 +433,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
401
433
|
id: genRecordUUID(),
|
|
402
434
|
stream: stream.name,
|
|
403
435
|
source: stream.source,
|
|
436
|
+
recorderType: recorder.type,
|
|
404
437
|
url: stream.url,
|
|
405
438
|
ffmpegArgs,
|
|
406
439
|
savePath: savePath,
|
package/lib/stream.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRoomInfo } from "./douyin_api.js";
|
|
1
|
+
import { getRoomInfo, selectRandomAPI } from "./douyin_api.js";
|
|
2
2
|
import { globalLoadBalancer } from "./loadBalancer/loadBalancer.js";
|
|
3
3
|
export async function getInfo(channelId, opts) {
|
|
4
4
|
let info;
|
|
@@ -31,6 +31,9 @@ export async function getStream(opts) {
|
|
|
31
31
|
// userHTML 接口只能用于状态检测
|
|
32
32
|
api = "web";
|
|
33
33
|
}
|
|
34
|
+
else if (api === "random") {
|
|
35
|
+
api = selectRandomAPI(["userHTML"]);
|
|
36
|
+
}
|
|
34
37
|
const info = await getRoomInfo(opts.channelId, {
|
|
35
38
|
doubleScreen: opts.doubleScreen ?? true,
|
|
36
39
|
auth: opts.cookie,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/douyin-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"douyin-danma-listener": "0.2.1",
|
|
42
|
+
"@bililive-tools/manager": "^1.9.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "*"
|