@bililive-tools/douyu-recorder 1.7.1 → 1.8.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,7 +49,8 @@ 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
+ recorderType?: "auto" | "ffmpeg" | "mesio" | "bililive"; // 底层录制器,使用mesio和bililive时videoFormat参数无效
53
+ debugLevel?: `verbose` | "basic"; // verbose参数时,录制器会输出更加详细的log
53
54
  }
54
55
  ```
55
56
 
package/lib/dy_api.d.ts CHANGED
@@ -16,6 +16,7 @@ export declare function getLiveInfo(opts: {
16
16
  isSupportRateSwitch: boolean;
17
17
  isOriginalStream: boolean;
18
18
  currentStream: {
19
+ onlyAudio: boolean;
19
20
  source: string;
20
21
  name: string;
21
22
  rate: number;
package/lib/dy_api.js CHANGED
@@ -46,11 +46,16 @@ 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.stringify(json, null, 2));
49
50
  const streamUrl = `${json.data.rtmp_url}/${json.data.rtmp_live}`;
50
51
  let cdn = json.data.rtmp_cdn;
52
+ let onlyAudio = false;
51
53
  try {
52
54
  const url = new URL(streamUrl);
53
55
  cdn = url.searchParams.get("fcdn") ?? "";
56
+ if (url.searchParams.get("only-audio") == "1") {
57
+ onlyAudio = true;
58
+ }
54
59
  }
55
60
  catch (error) {
56
61
  console.warn("解析 rtmp_url 失败", error);
@@ -62,6 +67,7 @@ export async function getLiveInfo(opts) {
62
67
  isSupportRateSwitch: json.data.rateSwitch === 1,
63
68
  isOriginalStream: json.data.rateSwitch !== 1,
64
69
  currentStream: {
70
+ onlyAudio,
65
71
  source: cdn,
66
72
  name: json.data.rateSwitch !== 1
67
73
  ? "原画"
@@ -12,7 +12,11 @@ export function createDYClient(channelId, opts = {}) {
12
12
  let maxRetry = 10;
13
13
  let coder = new BufferCoder();
14
14
  let heartbeatTimer = null;
15
- const send = (message) => ws?.send(coder.encode(STT.serialize(message)));
15
+ const send = (message) => {
16
+ if (ws && ws.readyState === WebSocket.OPEN) {
17
+ ws?.send(coder.encode(STT.serialize(message)));
18
+ }
19
+ };
16
20
  const sendLogin = () => send({ type: "loginreq", roomid: channelId });
17
21
  const sendJoinGroup = () => send({ type: "joingroup", rid: channelId, gid: -9999 });
18
22
  const sendHeartbeat = () => {
package/lib/index.js CHANGED
@@ -67,10 +67,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
67
67
  // 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
68
68
  if (this.recordHandle != null) {
69
69
  // 只有当设置了标题关键词时,并且不是手动启动的录制,才获取最新的直播间信息
70
- if (!isManualStart &&
71
- this.titleKeywords &&
72
- typeof this.titleKeywords === "string" &&
73
- this.titleKeywords.trim()) {
70
+ if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
74
71
  const now = Date.now();
75
72
  // 每5分钟检查一次标题变化
76
73
  const titleCheckInterval = 5 * 60 * 1000; // 5分钟
@@ -86,12 +83,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
86
83
  const liveInfo = await getInfo(this.channelId);
87
84
  const { title } = liveInfo;
88
85
  // 检查标题是否包含关键词
89
- const keywords = this.titleKeywords
90
- .split(",")
91
- .map((k) => k.trim())
92
- .filter((k) => k);
93
- const hasTitleKeyword = keywords.some((keyword) => title.toLowerCase().includes(keyword.toLowerCase()));
94
- if (hasTitleKeyword) {
86
+ if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
87
+ this.state = "title-blocked";
95
88
  this.emit("DebugLog", {
96
89
  type: "common",
97
90
  text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
@@ -128,16 +121,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
128
121
  return null;
129
122
  // 检查标题是否包含关键词,如果包含则不自动录制
130
123
  // 手动开始录制时不检查标题关键词
131
- if (!isManualStart &&
132
- this.titleKeywords &&
133
- typeof this.titleKeywords === "string" &&
134
- this.titleKeywords.trim()) {
135
- const keywords = this.titleKeywords
136
- .split(",")
137
- .map((k) => k.trim())
138
- .filter((k) => k);
139
- const hasTitleKeyword = keywords.some((keyword) => title.toLowerCase().includes(keyword.toLowerCase()));
140
- if (hasTitleKeyword) {
124
+ if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
125
+ if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
126
+ this.state = "title-blocked";
141
127
  this.emit("DebugLog", {
142
128
  type: "common",
143
129
  text: `跳过录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
@@ -145,7 +131,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
145
131
  return null;
146
132
  }
147
133
  }
148
- let recorderType = this.recorderType === "mesio" ? "mesio" : "ffmpeg";
149
134
  let res;
150
135
  try {
151
136
  let strictQuality = false;
@@ -164,7 +149,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
164
149
  source: this.source,
165
150
  strictQuality,
166
151
  onlyAudio: this.onlyAudio,
167
- avoidEdgeCDN: recorderType === "mesio",
152
+ avoidEdgeCDN: true,
168
153
  });
169
154
  }
170
155
  catch (err) {
@@ -196,7 +181,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
196
181
  };
197
182
  let isEnded = false;
198
183
  let isCutting = false;
199
- const recorder = createBaseRecorder(recorderType, {
184
+ const recorder = createBaseRecorder(this.recorderType, {
200
185
  url: stream.url,
201
186
  // @ts-ignore
202
187
  outputOptions: ffmpegOutputOptions,
@@ -204,6 +189,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
204
189
  getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
205
190
  disableDanma: this.disableProvideCommentsWhenRecording,
206
191
  videoFormat: this.videoFormat ?? "auto",
192
+ debugLevel: this.debugLevel ?? "none",
193
+ onlyAudio: stream.onlyAudio,
207
194
  }, onEnd, async () => {
208
195
  const info = await getInfo(this.channelId);
209
196
  return info;
@@ -219,8 +206,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
219
206
  this.state = "idle";
220
207
  throw err;
221
208
  }
222
- const handleVideoCreated = async ({ filename, title, cover }) => {
223
- this.emit("videoFileCreated", { filename, cover });
209
+ const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
210
+ this.emit("videoFileCreated", { filename, cover, rawFilename });
224
211
  if (title && this?.liveInfo) {
225
212
  this.liveInfo.title = title;
226
213
  }
package/lib/stream.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality">
21
21
  isSupportRateSwitch: boolean;
22
22
  isOriginalStream: boolean;
23
23
  currentStream: {
24
+ onlyAudio: boolean;
24
25
  source: string;
25
26
  name: string;
26
27
  rate: number;
package/lib/stream.js CHANGED
@@ -57,7 +57,20 @@ export async function getStream(opts) {
57
57
  });
58
58
  if (!liveInfo.living)
59
59
  throw new Error("It must be called getStream when living");
60
- // console.log(JSON.stringify(liveInfo, null, 2));
60
+ //如果是scdn,那么找到第一个非scdn的源,重新请求一次
61
+ if (liveInfo.currentStream.source === "scdn") {
62
+ const nonScdnSource = liveInfo.sources.find((source) => source.cdn !== "scdnctshh");
63
+ if (nonScdnSource) {
64
+ liveInfo = await getLiveInfo({
65
+ channelId: opts.channelId,
66
+ rate: qn,
67
+ cdn: nonScdnSource?.cdn,
68
+ onlyAudio: opts.onlyAudio,
69
+ });
70
+ }
71
+ }
72
+ if (!liveInfo.living)
73
+ throw new Error("It must be called getStream when living");
61
74
  if (liveInfo.currentStream.rate !== qn && opts.strictQuality) {
62
75
  throw new Error("Can not get expect quality because of strictQuality");
63
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/douyu-recorder",
3
- "version": "1.7.1",
3
+ "version": "1.8.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.6.1"
44
+ "@bililive-tools/manager": "^1.8.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/ws": "^8.5.13"