@bililive-tools/douyu-recorder 1.1.0 → 1.3.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 +16 -0
- package/lib/dy_client/index.d.ts +1 -0
- package/lib/dy_client/index.js +7 -2
- package/lib/index.js +34 -8
- package/lib/stream.d.ts +1 -0
- package/lib/stream.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ interface Options {
|
|
|
36
36
|
channelId: string; // 直播间ID,具体解析见文档,也可自行解析
|
|
37
37
|
quality: number; // 见画质参数
|
|
38
38
|
qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
|
|
39
|
+
source?: string; // 指定 cdn,见文档,不传为自动
|
|
39
40
|
streamPriorities: []; // 废弃
|
|
40
41
|
sourcePriorities: []; // 废弃
|
|
41
42
|
disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
|
|
@@ -45,6 +46,7 @@ interface Options {
|
|
|
45
46
|
saveGiftDanma?: boolean; // 保存礼物弹幕
|
|
46
47
|
saveSCDanma?: boolean; // 保存高能弹幕
|
|
47
48
|
saveCover?: boolean; // 保存封面
|
|
49
|
+
videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
|
|
48
50
|
}
|
|
49
51
|
```
|
|
50
52
|
|
|
@@ -71,6 +73,20 @@ const url = "https://www.douyu.com/2140934";
|
|
|
71
73
|
const { id } = await provider.resolveChannelInfoFromURL(url);
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
## cdn
|
|
77
|
+
|
|
78
|
+
如果有更多线路或者错误,请发issue
|
|
79
|
+
|
|
80
|
+
| 线路 | 值 |
|
|
81
|
+
| ------ | --------- |
|
|
82
|
+
| 自动 | auto |
|
|
83
|
+
| 线路1 | scdnctshh |
|
|
84
|
+
| 线路4 | tctc-h5 |
|
|
85
|
+
| 线路5 | tct-h5 |
|
|
86
|
+
| 线路6 | ali-h5 |
|
|
87
|
+
| 线路7 | hw-h5 |
|
|
88
|
+
| 线路13 | hs-h5 |
|
|
89
|
+
|
|
74
90
|
# 协议
|
|
75
91
|
|
|
76
92
|
与原项目保存一致为 LGPL
|
package/lib/dy_client/index.d.ts
CHANGED
package/lib/dy_client/index.js
CHANGED
|
@@ -9,13 +9,18 @@ import { BufferCoder } from "./buffer_coder.js";
|
|
|
9
9
|
import { STT } from "./stt.js";
|
|
10
10
|
export function createDYClient(channelId, opts = {}) {
|
|
11
11
|
let ws = null;
|
|
12
|
-
let maxRetry =
|
|
12
|
+
let maxRetry = 10;
|
|
13
13
|
let coder = new BufferCoder();
|
|
14
14
|
let heartbeatTimer = null;
|
|
15
15
|
const send = (message) => ws?.send(coder.encode(STT.serialize(message)));
|
|
16
16
|
const sendLogin = () => send({ type: "loginreq", roomid: channelId });
|
|
17
17
|
const sendJoinGroup = () => send({ type: "joingroup", rid: channelId, gid: -9999 });
|
|
18
|
-
const sendHeartbeat = () =>
|
|
18
|
+
const sendHeartbeat = () => {
|
|
19
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
send({ type: "mrkl" });
|
|
23
|
+
};
|
|
19
24
|
const sendLogout = () => send({ type: "logout" });
|
|
20
25
|
const onOpen = () => {
|
|
21
26
|
sendLogin();
|
package/lib/index.js
CHANGED
|
@@ -60,7 +60,7 @@ const ffmpegOutputOptions = [
|
|
|
60
60
|
"-movflags",
|
|
61
61
|
"faststart+frag_keyframe+empty_moov",
|
|
62
62
|
"-min_frag_duration",
|
|
63
|
-
"
|
|
63
|
+
"10000000",
|
|
64
64
|
];
|
|
65
65
|
const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
|
|
66
66
|
// 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
|
|
@@ -106,9 +106,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
106
106
|
}
|
|
107
107
|
// 获取直播间信息
|
|
108
108
|
const liveInfo = await getInfo(this.channelId);
|
|
109
|
-
const { living, owner, title
|
|
109
|
+
const { living, owner, title } = liveInfo;
|
|
110
110
|
this.liveInfo = liveInfo;
|
|
111
|
-
this.emit("LiveStart", { liveId });
|
|
112
111
|
if (liveInfo.liveId === banLiveId) {
|
|
113
112
|
this.tempStopIntervalCheck = true;
|
|
114
113
|
}
|
|
@@ -154,6 +153,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
154
153
|
res = await getStream({
|
|
155
154
|
channelId: this.channelId,
|
|
156
155
|
quality: this.quality,
|
|
156
|
+
source: this.source,
|
|
157
157
|
strictQuality,
|
|
158
158
|
});
|
|
159
159
|
}
|
|
@@ -169,6 +169,10 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
169
169
|
this.usedStream = stream.name;
|
|
170
170
|
this.usedSource = stream.source;
|
|
171
171
|
const onEnd = (...args) => {
|
|
172
|
+
if (isCutting) {
|
|
173
|
+
isCutting = false;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
172
176
|
if (isEnded)
|
|
173
177
|
return;
|
|
174
178
|
isEnded = true;
|
|
@@ -180,13 +184,18 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
180
184
|
this.recordHandle?.stop(reason);
|
|
181
185
|
};
|
|
182
186
|
let isEnded = false;
|
|
187
|
+
let isCutting = false;
|
|
183
188
|
const recorder = new FFMPEGRecorder({
|
|
184
189
|
url: stream.url,
|
|
185
190
|
outputOptions: ffmpegOutputOptions,
|
|
186
191
|
segment: this.segment ?? 0,
|
|
187
|
-
getSavePath: (opts) => getSavePath({ owner, title, startTime: opts.startTime }),
|
|
192
|
+
getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
|
|
188
193
|
disableDanma: this.disableProvideCommentsWhenRecording,
|
|
189
|
-
|
|
194
|
+
videoFormat: this.videoFormat ?? "auto",
|
|
195
|
+
}, onEnd, async () => {
|
|
196
|
+
const info = await getInfo(this.channelId);
|
|
197
|
+
return info;
|
|
198
|
+
});
|
|
190
199
|
const savePath = getSavePath({
|
|
191
200
|
owner,
|
|
192
201
|
title,
|
|
@@ -198,8 +207,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
198
207
|
this.state = "idle";
|
|
199
208
|
throw err;
|
|
200
209
|
}
|
|
201
|
-
const handleVideoCreated = async ({ filename }) => {
|
|
202
|
-
this.emit("videoFileCreated", { filename });
|
|
210
|
+
const handleVideoCreated = async ({ filename, title, cover }) => {
|
|
211
|
+
this.emit("videoFileCreated", { filename, cover });
|
|
212
|
+
if (title && this?.liveInfo) {
|
|
213
|
+
this.liveInfo.title = title;
|
|
214
|
+
}
|
|
215
|
+
if (cover && this?.liveInfo) {
|
|
216
|
+
this.liveInfo.cover = cover;
|
|
217
|
+
}
|
|
203
218
|
const extraDataController = recorder.getExtraDataController();
|
|
204
219
|
extraDataController?.setMeta({
|
|
205
220
|
room_id: this.channelId,
|
|
@@ -234,7 +249,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
234
249
|
case "chatmsg": {
|
|
235
250
|
const comment = {
|
|
236
251
|
type: "comment",
|
|
237
|
-
timestamp:
|
|
252
|
+
timestamp: Number(msg.cst),
|
|
238
253
|
text: msg.txt,
|
|
239
254
|
color: colorTab[msg.col] ?? "#ffffff",
|
|
240
255
|
sender: {
|
|
@@ -368,6 +383,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
368
383
|
const ffmpegArgs = recorder.getArguments();
|
|
369
384
|
recorder.run();
|
|
370
385
|
// TODO: 需要一个机制防止空录制,比如检查文件的大小变化、ffmpeg 的输出、直播状态等
|
|
386
|
+
const cut = utils.singleton(async () => {
|
|
387
|
+
if (!this.recordHandle)
|
|
388
|
+
return;
|
|
389
|
+
if (isCutting)
|
|
390
|
+
return;
|
|
391
|
+
isCutting = true;
|
|
392
|
+
await recorder.stop();
|
|
393
|
+
recorder.createCommand();
|
|
394
|
+
recorder.run();
|
|
395
|
+
});
|
|
371
396
|
const stop = utils.singleton(async (reason) => {
|
|
372
397
|
if (!this.recordHandle)
|
|
373
398
|
return;
|
|
@@ -398,6 +423,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
398
423
|
ffmpegArgs,
|
|
399
424
|
savePath: savePath,
|
|
400
425
|
stop,
|
|
426
|
+
cut,
|
|
401
427
|
};
|
|
402
428
|
this.emit("RecordStart", this.recordHandle);
|
|
403
429
|
return this.recordHandle;
|
package/lib/stream.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function getInfo(channelId: string): Promise<{
|
|
|
11
11
|
export declare function getStream(opts: Pick<Recorder, "channelId" | "quality"> & {
|
|
12
12
|
rejectCache?: boolean;
|
|
13
13
|
strictQuality?: boolean;
|
|
14
|
+
source?: string;
|
|
14
15
|
}): Promise<{
|
|
15
16
|
living: true;
|
|
16
17
|
sources: import("./dy_api.js").SourceProfile[];
|
package/lib/stream.js
CHANGED
|
@@ -47,6 +47,7 @@ export async function getStream(opts) {
|
|
|
47
47
|
let liveInfo = await getLiveInfo({
|
|
48
48
|
channelId: opts.channelId,
|
|
49
49
|
rate: qn,
|
|
50
|
+
cdn: opts.source === "auto" ? undefined : opts.source,
|
|
50
51
|
});
|
|
51
52
|
if (!liveInfo.living)
|
|
52
53
|
throw new Error("It must be called getStream when living");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/douyu-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "bililive-tools douyu recorder implemention",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"lodash-es": "^4.17.21",
|
|
42
42
|
"axios": "^1.7.8",
|
|
43
43
|
"douyu-api": "^0.1.0",
|
|
44
|
-
"@bililive-tools/manager": "^1.
|
|
44
|
+
"@bililive-tools/manager": "^1.3.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.5.13"
|