@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 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
- // console.log("json", json, {
50
- // ...signed,
51
- // cdn: opts.cdn ?? "",
52
- // // 相当于清晰度类型的 id,给 -1 会由后端决定,0 为原画
53
- // rate: String(opts.rate ?? 0),
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: json.data.rtmp_cdn,
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: `${json.data.rtmp_url}/${json.data.rtmp_live}`,
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, FFMPEGRecorder, } from "@bililive-tools/manager";
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
- const liveInfo = await getInfo(this.channelId);
110
- const { living, owner, title } = liveInfo;
111
- this.liveInfo = liveInfo;
112
- if (liveInfo.liveId === banLiveId) {
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.state = "idle";
164
- this.qualityRetry -= 1;
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: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
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 = new FFMPEGRecorder({
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.startTime?.getTime(),
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: opts.source === "auto" ? undefined : opts.source,
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.5.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.4.1"
44
+ "@bililive-tools/manager": "^1.6.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/ws": "^8.5.13"