@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 +1 -1
- package/lib/douyin_api.js +5 -5
- package/lib/index.js +42 -83
- package/lib/stream.d.ts +2 -1
- package/lib/stream.js +3 -1
- package/package.json +3 -3
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
404
|
-
liveId: room
|
|
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,
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
127
|
+
// 再检查一次,上一个接口可能不存在标题参数
|
|
128
|
+
if (utils.checkTitleKeywordsBeforeRecord(this.liveInfo.title, this, isManualStart))
|
|
129
|
+
return null;
|
|
170
130
|
}
|
|
171
131
|
catch (err) {
|
|
172
|
-
if (
|
|
173
|
-
this.
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
258
|
-
|
|
215
|
+
downloader.on("videoFileCreated", handleVideoCreated);
|
|
216
|
+
downloader.on("videoFileCompleted", ({ filename }) => {
|
|
259
217
|
this.emit("videoFileCompleted", { filename });
|
|
260
218
|
});
|
|
261
|
-
|
|
219
|
+
downloader.on("DebugLog", (data) => {
|
|
262
220
|
this.emit("DebugLog", data);
|
|
263
221
|
});
|
|
264
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
389
|
-
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
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 =
|
|
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
|
|
375
|
+
await downloader.stop();
|
|
418
376
|
}
|
|
419
377
|
catch (err) {
|
|
420
378
|
this.emit("DebugLog", {
|
|
421
379
|
type: "common",
|
|
422
|
-
text: `stop
|
|
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:
|
|
395
|
+
recorderType: downloader.type,
|
|
437
396
|
url: stream.url,
|
|
438
|
-
|
|
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.
|
|
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
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"@bililive-tools/manager": "^1.10.0",
|
|
42
|
+
"douyin-danma-listener": "0.2.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "*"
|