@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 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
@@ -17,6 +17,7 @@ interface Message$Chat {
17
17
  dc: string;
18
18
  bdlv: string;
19
19
  ic: string;
20
+ cst: string;
20
21
  }
21
22
  interface Message$Gift {
22
23
  type: "dgb";
@@ -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 = 5;
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 = () => send({ type: "mrkl" });
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
- "60000000",
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, liveId } = liveInfo;
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
- }, onEnd);
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: Date.now(),
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.1.0",
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.1.0"
44
+ "@bililive-tools/manager": "^1.3.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/ws": "^8.5.13"