@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.
@@ -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: "webHTML",
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: "userHTML",
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({ owner, title: opts.title ?? title, startTime: opts.startTime }),
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
- this.emit("Message", gift);
320
- extraDataController.addMessage(gift);
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.8.0",
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
- "@bililive-tools/manager": "^1.8.0",
42
- "douyin-danma-listener": "0.2.1"
41
+ "douyin-danma-listener": "0.2.1",
42
+ "@bililive-tools/manager": "^1.9.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "*"