@bililive-tools/douyu-recorder 1.5.1 → 1.7.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 +3 -0
- package/lib/danma.js +53 -0
- package/lib/dy_api.js +11 -8
- package/lib/dy_client/buffer_coder.js +2 -0
- package/lib/index.js +20 -12
- package/lib/stream.d.ts +1 -0
- package/lib/stream.js +6 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ interface Options {
|
|
|
49
49
|
saveCover?: boolean; // 保存封面
|
|
50
50
|
videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
|
|
51
51
|
onlyAudio?: boolean; // 只录制音频,默认为否
|
|
52
|
+
recorderType?: "auto" | "ffmpeg" | "mesio"; // 底层录制器,使用mesio时videoFormat参数无效
|
|
52
53
|
}
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -77,6 +78,8 @@ const { id } = await provider.resolveChannelInfoFromURL(url);
|
|
|
77
78
|
|
|
78
79
|
## cdn
|
|
79
80
|
|
|
81
|
+
在 `cdn=auto` 且 `recorderType=mesio` 时,默认使用 `hw-h5` 线路
|
|
82
|
+
|
|
80
83
|
如果有更多线路或者错误,请发issue
|
|
81
84
|
|
|
82
85
|
| 线路 | 值 |
|
package/lib/danma.js
CHANGED
|
@@ -11,6 +11,59 @@ export const colorTab = {
|
|
|
11
11
|
*/
|
|
12
12
|
// 粉丝荧光棒被手动置为0了
|
|
13
13
|
export const giftMap = {
|
|
14
|
+
"20000": { name: "100鱼丸", pc: 0 },
|
|
15
|
+
"20001": { name: "弱鸡", pc: 20 },
|
|
16
|
+
"20002": { name: "办卡", pc: 600 },
|
|
17
|
+
"20003": { name: "飞机", pc: 10000 },
|
|
18
|
+
"20004": { name: "火箭", pc: 50000 },
|
|
19
|
+
"20005": { name: "超级火箭", pc: 200000 },
|
|
20
|
+
"20006": { name: "赞", pc: 10 },
|
|
21
|
+
"20008": { name: "超大丸星", pc: 0 },
|
|
22
|
+
"20541": { name: "大气", pc: 10 },
|
|
23
|
+
"20542": { name: "666", pc: 100 },
|
|
24
|
+
"23335": { name: "宇宙飞船", pc: 500000 },
|
|
25
|
+
"23338": { name: "告白卡", pc: 600 },
|
|
26
|
+
"23509": { name: "为爱发电", pc: 500 },
|
|
27
|
+
"23515": { name: "星际飞车", pc: 5000 },
|
|
28
|
+
"23622": { name: "至尊飞船", pc: 500000 },
|
|
29
|
+
"23623": { name: "至尊超火", pc: 200000 },
|
|
30
|
+
"23624": { name: "至尊火箭", pc: 50000 },
|
|
31
|
+
"23625": { name: "至尊飞机", pc: 10000 },
|
|
32
|
+
"23669": { name: "钻粉灯牌", pc: 600 },
|
|
33
|
+
"23670": { name: "粉丝灯牌", pc: 600 },
|
|
34
|
+
"24468": { name: "粉丝卡", pc: 600 },
|
|
35
|
+
"24470": { name: "爱的CD", pc: 100 },
|
|
36
|
+
"24472": { name: "怦然心动", pc: 600 },
|
|
37
|
+
"24473": { name: "浪漫旅行车", pc: 6600 },
|
|
38
|
+
"24474": { name: "童话马车", pc: 16600 },
|
|
39
|
+
"24475": { name: "星空丘比特", pc: 131400 },
|
|
40
|
+
"24478": { name: "全力守护", pc: 10 },
|
|
41
|
+
"24489": { name: "老司机", pc: 600 },
|
|
42
|
+
"24490": { name: "牛啤", pc: 600 },
|
|
43
|
+
"24491": { name: "小心心", pc: 10 },
|
|
44
|
+
"24492": { name: "GG", pc: 10 },
|
|
45
|
+
"24493": { name: "炒CP", pc: 5000 },
|
|
46
|
+
"24494": { name: "爱神丘比特", pc: 6600 },
|
|
47
|
+
"24495": { name: "陪伴飞机", pc: 10000 },
|
|
48
|
+
"24496": { name: "挚爱之吻", pc: 100000 },
|
|
49
|
+
"24497": { name: "斗鱼666号", pc: 100000 },
|
|
50
|
+
"24503": { name: "城堡气球", pc: 200000 },
|
|
51
|
+
"24504": { name: "挚爱之心", pc: 300000 },
|
|
52
|
+
"24505": { name: "珍珠奶茶", pc: 100 },
|
|
53
|
+
"24506": { name: "翻车了", pc: 20 },
|
|
54
|
+
"24507": { name: "古堡公主", pc: 18800 },
|
|
55
|
+
"24532": { name: "穿越千年", pc: 200000 },
|
|
56
|
+
"24533": { name: "青瓷絮语", pc: 30000 },
|
|
57
|
+
"24534": { name: "千里江山", pc: 10000 },
|
|
58
|
+
"24535": { name: "烽面惊鸿", pc: 600 },
|
|
59
|
+
"24553": { name: "能量魔方", pc: 5000 },
|
|
60
|
+
"24554": { name: "超能战舰", pc: 200000 },
|
|
61
|
+
"24560": { name: "破空飞机", pc: 10000 },
|
|
62
|
+
"24561": { name: "星际卡", pc: 600 },
|
|
63
|
+
"24597": { name: "高能弹幕", pc: 1000 },
|
|
64
|
+
"24623": { name: "带宽券", pc: 10 },
|
|
65
|
+
"24625": { name: "梦境小熊", pc: 10000 },
|
|
66
|
+
"24626": { name: "星跃猫娘", pc: 50000 },
|
|
14
67
|
"192": { name: "赞", pc: 10 },
|
|
15
68
|
"193": { name: "弱鸡", pc: 20 },
|
|
16
69
|
"194": { name: "666", pc: 600 },
|
package/lib/dy_api.js
CHANGED
|
@@ -46,12 +46,15 @@ export async function getLiveInfo(opts) {
|
|
|
46
46
|
delete signCaches[opts.channelId];
|
|
47
47
|
throw new Error("Unexpected error code, " + json.error);
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
const streamUrl = `${json.data.rtmp_url}/${json.data.rtmp_live}`;
|
|
50
|
+
let cdn = json.data.rtmp_cdn;
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(streamUrl);
|
|
53
|
+
cdn = url.searchParams.get("fcdn") ?? "";
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.warn("解析 rtmp_url 失败", error);
|
|
57
|
+
}
|
|
55
58
|
return {
|
|
56
59
|
living: true,
|
|
57
60
|
sources: json.data.cdnsWithName,
|
|
@@ -59,12 +62,12 @@ export async function getLiveInfo(opts) {
|
|
|
59
62
|
isSupportRateSwitch: json.data.rateSwitch === 1,
|
|
60
63
|
isOriginalStream: json.data.rateSwitch !== 1,
|
|
61
64
|
currentStream: {
|
|
62
|
-
source:
|
|
65
|
+
source: cdn,
|
|
63
66
|
name: json.data.rateSwitch !== 1
|
|
64
67
|
? "原画"
|
|
65
68
|
: (json.data.multirates.find(({ rate }) => rate === json.data.rate)?.name ?? "未知"),
|
|
66
69
|
rate: json.data.rate,
|
|
67
|
-
url:
|
|
70
|
+
url: streamUrl,
|
|
68
71
|
},
|
|
69
72
|
};
|
|
70
73
|
}
|
|
@@ -18,6 +18,7 @@ export class BufferCoder {
|
|
|
18
18
|
if (littleEndian == null) {
|
|
19
19
|
littleEndian = this.littleEndian;
|
|
20
20
|
}
|
|
21
|
+
// @ts-ignore
|
|
21
22
|
this.buffer = this.concat(this.buffer, newBuffer).buffer;
|
|
22
23
|
while (this.buffer && this.buffer.byteLength > 0) {
|
|
23
24
|
if (this.readLength === 0) {
|
|
@@ -38,6 +39,7 @@ export class BufferCoder {
|
|
|
38
39
|
if (littleEndian == null) {
|
|
39
40
|
littleEndian = this.littleEndian;
|
|
40
41
|
}
|
|
42
|
+
// @ts-ignore
|
|
41
43
|
const out = this.concat(this.encoder.encode(msg), Uint8Array.of(0));
|
|
42
44
|
const formatBodySize = 8 + out.length;
|
|
43
45
|
const dv = new DataView(new ArrayBuffer(formatBodySize + 4));
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import mitt from "mitt";
|
|
2
|
-
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils,
|
|
2
|
+
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, createBaseRecorder, } from "@bililive-tools/manager";
|
|
3
3
|
import { getInfo, getStream } from "./stream.js";
|
|
4
4
|
import { getRoomInfo } from "./dy_api.js";
|
|
5
5
|
import { ensureFolderExist } from "./utils.js";
|
|
@@ -106,10 +106,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
106
106
|
return this.recordHandle;
|
|
107
107
|
}
|
|
108
108
|
// 获取直播间信息
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
try {
|
|
110
|
+
const liveInfo = await getInfo(this.channelId);
|
|
111
|
+
this.liveInfo = liveInfo;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.state = "check-error";
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const { living, owner, title } = this.liveInfo;
|
|
118
|
+
if (this.liveInfo.liveId === banLiveId) {
|
|
113
119
|
this.tempStopIntervalCheck = true;
|
|
114
120
|
}
|
|
115
121
|
else {
|
|
@@ -138,8 +144,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
138
144
|
return null;
|
|
139
145
|
}
|
|
140
146
|
}
|
|
147
|
+
let recorderType = this.recorderType === "mesio" ? "mesio" : "ffmpeg";
|
|
141
148
|
let res;
|
|
142
|
-
// TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
|
|
143
149
|
try {
|
|
144
150
|
let strictQuality = false;
|
|
145
151
|
if (this.qualityRetry > 0) {
|
|
@@ -157,11 +163,13 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
157
163
|
source: this.source,
|
|
158
164
|
strictQuality,
|
|
159
165
|
onlyAudio: this.onlyAudio,
|
|
166
|
+
avoidEdgeCDN: recorderType === "mesio",
|
|
160
167
|
});
|
|
161
168
|
}
|
|
162
169
|
catch (err) {
|
|
163
|
-
this.
|
|
164
|
-
|
|
170
|
+
if (this.qualityRetry > 0)
|
|
171
|
+
this.qualityRetry -= 1;
|
|
172
|
+
this.state = "check-error";
|
|
165
173
|
throw err;
|
|
166
174
|
}
|
|
167
175
|
this.state = "recording";
|
|
@@ -180,15 +188,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
180
188
|
isEnded = true;
|
|
181
189
|
this.emit("DebugLog", {
|
|
182
190
|
type: "common",
|
|
183
|
-
text: `
|
|
191
|
+
text: `record end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
|
|
184
192
|
});
|
|
185
193
|
const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
186
194
|
this.recordHandle?.stop(reason);
|
|
187
195
|
};
|
|
188
196
|
let isEnded = false;
|
|
189
197
|
let isCutting = false;
|
|
190
|
-
const recorder =
|
|
198
|
+
const recorder = createBaseRecorder(recorderType, {
|
|
191
199
|
url: stream.url,
|
|
200
|
+
// @ts-ignore
|
|
192
201
|
outputOptions: ffmpegOutputOptions,
|
|
193
202
|
segment: this.segment ?? 0,
|
|
194
203
|
getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
|
|
@@ -221,7 +230,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
221
230
|
extraDataController?.setMeta({
|
|
222
231
|
room_id: this.channelId,
|
|
223
232
|
platform: provider?.id,
|
|
224
|
-
liveStartTimestamp: liveInfo
|
|
233
|
+
liveStartTimestamp: this?.liveInfo?.startTime?.getTime(),
|
|
225
234
|
// recordStopTimestamp: Date.now(),
|
|
226
235
|
title: title,
|
|
227
236
|
user_name: owner,
|
|
@@ -379,7 +388,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
379
388
|
client.on("error", (err) => {
|
|
380
389
|
this.emit("DebugLog", { type: "common", text: String(err) });
|
|
381
390
|
});
|
|
382
|
-
// console.log("this.disableProvideCommentsWhenRecording", this.disableProvideCommentsWhenRecording);
|
|
383
391
|
if (!this.disableProvideCommentsWhenRecording) {
|
|
384
392
|
client.start();
|
|
385
393
|
}
|
package/lib/stream.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality">
|
|
|
13
13
|
strictQuality?: boolean;
|
|
14
14
|
source?: string;
|
|
15
15
|
onlyAudio?: boolean;
|
|
16
|
+
avoidEdgeCDN?: boolean;
|
|
16
17
|
}): Promise<{
|
|
17
18
|
living: true;
|
|
18
19
|
sources: import("./dy_api.js").SourceProfile[];
|
package/lib/stream.js
CHANGED
|
@@ -44,10 +44,15 @@ export async function getInfo(channelId) {
|
|
|
44
44
|
}
|
|
45
45
|
export async function getStream(opts) {
|
|
46
46
|
const qn = (DouyuQualities.includes(opts.quality) ? opts.quality : 0);
|
|
47
|
+
let cdn = opts.source === "auto" ? undefined : opts.source;
|
|
48
|
+
if (opts.source === "auto" && opts.avoidEdgeCDN) {
|
|
49
|
+
// TODO: 如果不存在 cdn=hw-h5 的源,那么还是可能默认到边缘节点,就先这样吧
|
|
50
|
+
cdn = "hw-h5";
|
|
51
|
+
}
|
|
47
52
|
let liveInfo = await getLiveInfo({
|
|
48
53
|
channelId: opts.channelId,
|
|
49
54
|
rate: qn,
|
|
50
|
-
cdn
|
|
55
|
+
cdn,
|
|
51
56
|
onlyAudio: opts.onlyAudio,
|
|
52
57
|
});
|
|
53
58
|
if (!liveInfo.living)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/douyu-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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.6.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.5.13"
|