@bililive-tools/douyin-recorder 1.9.0 → 1.10.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
@@ -38,7 +38,7 @@ interface Options {
38
38
  sourcePriorities: []; // 废弃
39
39
  formatPriorities?: string[]; // 支持,`flv`和`hls` 参数,默认为['flv','hls']
40
40
  disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
41
- segment?: number; // 分段参数,单位分钟
41
+ segment?: number | string; // 分段参数,单位分钟,如果以"B","KB","MB","GB"结尾,会尝试使用文件大小分段,仅推荐在使用mesio录制引擎时使用
42
42
  disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
43
43
  saveGiftDanma?: boolean; // 保存礼物弹幕
44
44
  saveCover?: boolean; // 保存封面
package/lib/douyin_api.js CHANGED
@@ -121,7 +121,7 @@ export function selectRandomAPI(exclude) {
121
121
  return availableAPIs[randomIndex];
122
122
  }
123
123
  /**
124
- * 通过解析直播html页面来获取房间数据
124
+ * 通过解析用户html页面来获取房间数据
125
125
  * @param secUserId
126
126
  * @param opts
127
127
  */
@@ -212,7 +212,7 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
212
212
  }
213
213
  }
214
214
  /**
215
- * 通过解析用户html页面来获取房间数据
215
+ * 通过解析直播html页面来获取房间数据
216
216
  * @param webRoomId
217
217
  * @param opts
218
218
  */
@@ -390,7 +390,6 @@ export async function getRoomInfo(webRoomId, opts = {}) {
390
390
  }
391
391
  // console.log(JSON.stringify(data, null, 2));
392
392
  const room = data.room;
393
- assert(room, `No room data, id ${webRoomId}`);
394
393
  if (api === "userHTML") {
395
394
  return {
396
395
  living: data.living,
@@ -400,12 +399,13 @@ export async function getRoomInfo(webRoomId, opts = {}) {
400
399
  streams: [],
401
400
  sources: [],
402
401
  avatar: data.avatar,
403
- cover: room.cover,
404
- liveId: room.id_str,
402
+ cover: room?.cover ?? "",
403
+ liveId: room?.id_str ?? "",
405
404
  uid: data.sec_uid,
406
405
  api: data.api,
407
406
  };
408
407
  }
408
+ assert(room, `No room data, id ${webRoomId}`);
409
409
  if (room?.stream_url == null) {
410
410
  return {
411
411
  living: false,
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, utils, createBaseRecorder, } from "@bililive-tools/manager";
3
+ import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, createDownloader, } 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";
@@ -16,10 +16,10 @@ function createRecorder(opts) {
16
16
  ...opts,
17
17
  availableStreams: [],
18
18
  availableSources: [],
19
- qualityMaxRetry: opts.qualityRetry ?? 0,
20
19
  qualityRetry: opts.qualityRetry ?? 0,
21
20
  useServerTimestamp: opts.useServerTimestamp ?? true,
22
21
  state: "idle",
22
+ cache: null,
23
23
  getChannelURL() {
24
24
  return `https://live.douyin.com/${this.channelId}`;
25
25
  },
@@ -62,40 +62,15 @@ function createRecorder(opts) {
62
62
  const ffmpegOutputOptions = [];
63
63
  const ffmpegInputOptions = ["-rw_timeout", "10000000", "-timeout", "10000000"];
64
64
  const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
65
- // 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
65
+ // 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
66
66
  if (this.recordHandle != null) {
67
- // 只有当设置了标题关键词时,并且不是手动启动的录制,才获取最新的直播间信息
68
- if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
69
- const now = Date.now();
70
- // 每5分钟检查一次标题变化
71
- const titleCheckInterval = 5 * 60 * 1000; // 5分钟
72
- // 获取上次检查时间
73
- const lastCheckTime = typeof this.extra.lastTitleCheckTime === "number" ? this.extra.lastTitleCheckTime : 0;
74
- // 如果距离上次检查时间不足指定间隔,则跳过检查
75
- if (now - lastCheckTime < titleCheckInterval) {
76
- return this.recordHandle;
77
- }
78
- // 更新检查时间
79
- this.extra.lastTitleCheckTime = now;
80
- // 获取直播间信息
81
- const liveInfo = await getInfo(this.channelId, {
82
- cookie: this.auth,
83
- api: this.api,
84
- uid: this.uid,
85
- });
86
- const { title } = liveInfo;
87
- // 检查标题是否包含关键词
88
- if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
89
- this.state = "title-blocked";
90
- this.emit("DebugLog", {
91
- type: "common",
92
- text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
93
- });
94
- // 停止录制
95
- await this.recordHandle.stop("直播间标题包含关键词");
96
- // 返回 null,停止录制
97
- return null;
98
- }
67
+ const shouldStop = await utils.checkTitleKeywordsWhileRecording(this, isManualStart, (channelId) => getInfo(channelId, {
68
+ cookie: this.auth,
69
+ api: this.api,
70
+ uid: this.uid,
71
+ }));
72
+ if (shouldStop) {
73
+ return null;
99
74
  }
100
75
  // 已经在录制中,直接返回
101
76
  return this.recordHandle;
@@ -124,30 +99,13 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
124
99
  return null;
125
100
  if (!this.liveInfo.living)
126
101
  return null;
127
- // 检查标题是否包含关键词,如果包含则不自动录制
128
- // 手动开始录制时不检查标题关键词
129
- if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
130
- if (utils.hasBlockedTitleKeywords(this.liveInfo.title, this.titleKeywords)) {
131
- this.state = "title-blocked";
132
- this.emit("DebugLog", {
133
- type: "common",
134
- text: `跳过录制:直播间标题 "${this.liveInfo.title}" 包含关键词 "${this.titleKeywords}"`,
135
- });
136
- return null;
137
- }
138
- }
102
+ // 检查标题是否包含关键词
103
+ if (utils.checkTitleKeywordsBeforeRecord(this.liveInfo.title, this, isManualStart))
104
+ return null;
105
+ const qualityRetryLeft = (await this.cache.get("qualityRetryLeft")) ?? this.qualityRetry;
106
+ const strictQuality = utils.shouldUseStrictQuality(qualityRetryLeft, this.qualityRetry, isManualStart);
139
107
  let res;
140
108
  try {
141
- let strictQuality = false;
142
- if (this.qualityRetry > 0) {
143
- strictQuality = true;
144
- }
145
- if (this.qualityMaxRetry < 0) {
146
- strictQuality = true;
147
- }
148
- if (isManualStart) {
149
- strictQuality = false;
150
- }
151
109
  // TODO: 检查mobile接口处理双屏录播流
152
110
  res = await getStream({
153
111
  channelId: this.channelId,
@@ -166,15 +124,17 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
166
124
  this.liveInfo.cover = res.cover;
167
125
  this.liveInfo.liveId = res.liveId;
168
126
  this.liveInfo.avatar = res.avatar;
169
- this.liveInfo.startTime = new Date();
127
+ // 再检查一次,上一个接口可能不存在标题参数
128
+ if (utils.checkTitleKeywordsBeforeRecord(this.liveInfo.title, this, isManualStart))
129
+ return null;
170
130
  }
171
131
  catch (err) {
172
- if (this.qualityRetry > 0)
173
- this.qualityRetry -= 1;
132
+ if (qualityRetryLeft > 0)
133
+ await this.cache.set("qualityRetryLeft", qualityRetryLeft - 1);
174
134
  this.state = "check-error";
175
135
  throw err;
176
136
  }
177
- const { owner, title, startTime } = this.liveInfo;
137
+ const { owner, title, liveStartTime, recordStartTime } = this.liveInfo;
178
138
  this.state = "recording";
179
139
  const { currentStream: stream, sources: availableSources, streams: availableStreams } = res;
180
140
  this.availableStreams = availableStreams.map((s) => s.desc);
@@ -198,8 +158,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
198
158
  const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
199
159
  this.recordHandle?.stop(reason);
200
160
  };
201
- const recordStartTime = new Date();
202
- const recorder = createBaseRecorder(this.recorderType, {
161
+ const downloader = createDownloader(this.recorderType, {
203
162
  url: stream.url,
204
163
  outputOptions: ffmpegOutputOptions,
205
164
  inputOptions: ffmpegInputOptions,
@@ -208,10 +167,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
208
167
  owner,
209
168
  title: opts.title ?? title,
210
169
  startTime: opts.startTime,
211
- liveStartTime: startTime,
170
+ liveStartTime: liveStartTime,
212
171
  recordStartTime,
213
172
  }),
214
- disableDanma: this.disableProvideCommentsWhenRecording,
215
173
  videoFormat: this.videoFormat ?? "auto",
216
174
  debugLevel: this.debugLevel ?? "none",
217
175
  onlyAudio: stream.onlyAudio,
@@ -226,7 +184,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
226
184
  owner,
227
185
  title,
228
186
  startTime: Date.now(),
229
- liveStartTime: startTime,
187
+ liveStartTime,
230
188
  recordStartTime,
231
189
  });
232
190
  try {
@@ -244,7 +202,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
244
202
  if (cover && this?.liveInfo) {
245
203
  this.liveInfo.cover = cover;
246
204
  }
247
- const extraDataController = recorder.getExtraDataController();
205
+ const extraDataController = downloader.getExtraDataController();
248
206
  extraDataController?.setMeta({
249
207
  room_id: this.channelId,
250
208
  platform: provider?.id,
@@ -254,14 +212,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
254
212
  user_name: owner,
255
213
  });
256
214
  };
257
- recorder.on("videoFileCreated", handleVideoCreated);
258
- recorder.on("videoFileCompleted", ({ filename }) => {
215
+ downloader.on("videoFileCreated", handleVideoCreated);
216
+ downloader.on("videoFileCompleted", ({ filename }) => {
259
217
  this.emit("videoFileCompleted", { filename });
260
218
  });
261
- recorder.on("DebugLog", (data) => {
219
+ downloader.on("DebugLog", (data) => {
262
220
  this.emit("DebugLog", data);
263
221
  });
264
- recorder.on("progress", (progress) => {
222
+ downloader.on("progress", (progress) => {
265
223
  if (this.recordHandle) {
266
224
  this.recordHandle.progress = progress;
267
225
  }
@@ -275,7 +233,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
275
233
  cookie: this.auth,
276
234
  });
277
235
  client.on("chat", (msg) => {
278
- const extraDataController = recorder.getExtraDataController();
236
+ const extraDataController = downloader.getExtraDataController();
279
237
  if (!extraDataController)
280
238
  return;
281
239
  const comment = {
@@ -296,7 +254,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
296
254
  extraDataController.addMessage(comment);
297
255
  });
298
256
  client.on("gift", (msg) => {
299
- const extraDataController = recorder.getExtraDataController();
257
+ const extraDataController = downloader.getExtraDataController();
300
258
  if (!extraDataController)
301
259
  return;
302
260
  if (this.saveGiftDanma === false)
@@ -385,17 +343,17 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
385
343
  if (!this.disableProvideCommentsWhenRecording) {
386
344
  client.connect();
387
345
  }
388
- const ffmpegArgs = recorder.getArguments();
389
- recorder.run();
346
+ const downloaderArgs = downloader.getArguments();
347
+ downloader.run();
390
348
  const cut = singleton(async () => {
391
349
  if (!this.recordHandle)
392
350
  return;
393
351
  if (isCutting)
394
352
  return;
395
353
  isCutting = true;
396
- await recorder.stop();
397
- recorder.createCommand();
398
- recorder.run();
354
+ await downloader.stop();
355
+ downloader.createCommand();
356
+ downloader.run();
399
357
  });
400
358
  const stop = singleton(async (reason) => {
401
359
  if (!this.recordHandle)
@@ -406,7 +364,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
406
364
  for (const [_groupId, cached] of giftMessageCache.entries()) {
407
365
  clearTimeout(cached.timer);
408
366
  // 立即添加剩余的礼物消息
409
- const extraDataController = recorder.getExtraDataController();
367
+ const extraDataController = downloader.getExtraDataController();
410
368
  if (extraDataController) {
411
369
  this.emit("Message", cached.gift);
412
370
  extraDataController.addMessage(cached.gift);
@@ -414,12 +372,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
414
372
  }
415
373
  giftMessageCache.clear();
416
374
  client.close();
417
- await recorder.stop();
375
+ await downloader.stop();
418
376
  }
419
377
  catch (err) {
420
378
  this.emit("DebugLog", {
421
379
  type: "common",
422
- text: `stop ffmpeg error: ${String(err)}`,
380
+ text: `stop record error: ${String(err)}`,
423
381
  });
424
382
  }
425
383
  this.usedStream = undefined;
@@ -428,14 +386,15 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
428
386
  this.recordHandle = undefined;
429
387
  this.liveInfo = undefined;
430
388
  this.state = "idle";
389
+ this.cache.set("qualityRetryLeft", this.qualityRetry);
431
390
  });
432
391
  this.recordHandle = {
433
392
  id: genRecordUUID(),
434
393
  stream: stream.name,
435
394
  source: stream.source,
436
- recorderType: recorder.type,
395
+ recorderType: downloader.type,
437
396
  url: stream.url,
438
- ffmpegArgs,
397
+ downloaderArgs,
439
398
  savePath: savePath,
440
399
  stop,
441
400
  cut,
package/lib/stream.d.ts CHANGED
@@ -11,10 +11,11 @@ export declare function getInfo(channelId: string, opts?: {
11
11
  roomId: string;
12
12
  avatar: string;
13
13
  cover: string;
14
- startTime: Date;
15
14
  liveId: string;
16
15
  uid: string;
17
16
  api: RealAPIType;
17
+ liveStartTime: Date;
18
+ recordStartTime: Date;
18
19
  }>;
19
20
  export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities"> & {
20
21
  rejectCache?: boolean;
package/lib/stream.js CHANGED
@@ -12,6 +12,7 @@ export async function getInfo(channelId, opts) {
12
12
  else {
13
13
  info = await getRoomInfo(channelId, opts ?? {});
14
14
  }
15
+ const startTime = new Date();
15
16
  return {
16
17
  living: info.living,
17
18
  owner: info.owner,
@@ -19,10 +20,11 @@ export async function getInfo(channelId, opts) {
19
20
  roomId: info.roomId,
20
21
  avatar: info.avatar,
21
22
  cover: info.cover,
22
- startTime: new Date(),
23
23
  liveId: info.liveId,
24
24
  uid: info.uid,
25
25
  api: info.api,
26
+ liveStartTime: startTime,
27
+ recordStartTime: startTime,
26
28
  };
27
29
  }
28
30
  export async function getStream(opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/douyin-recorder",
3
- "version": "1.9.0",
3
+ "version": "1.10.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
- "douyin-danma-listener": "0.2.1",
42
- "@bililive-tools/manager": "^1.9.0"
41
+ "@bililive-tools/manager": "^1.10.0",
42
+ "douyin-danma-listener": "0.2.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "*"