@bililive-tools/huya-recorder 1.7.1 → 1.9.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
@@ -37,6 +37,7 @@ interface Options {
37
37
  quality: number; // 见画质参数
38
38
  qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
39
39
  streamPriorities: []; // 废弃
40
+ titleKeywords?: string; // 禁止录制的标题关键字,英文逗号分开多个
40
41
  sourcePriorities: []; // 按提供的源优先级去给CDN列表排序,并过滤掉不在优先级配置中的源,在未匹配到的情况下会优先使用TX的CDN,具体参数见 CDN 参数
41
42
  formatPriorities?: string[]; // 支持,`flv`和`hls` 参数,默认为['flv','hls']
42
43
  disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
@@ -47,6 +48,7 @@ interface Options {
47
48
  api?: "auto" | "mp" | "web"; // 默认为auto,在星秀区使用mp接口,其他使用web接口,你也可以强制指定
48
49
  videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
49
50
  recorderType?: "auto" | "ffmpeg" | "mesio"; // 底层录制器,使用mesio时videoFormat参数无效
51
+ debugLevel?: `verbose` | "basic"; // verbose参数时,录制器会输出更加详细的log
50
52
  }
51
53
  ```
52
54
 
package/lib/huya_api.js CHANGED
@@ -1,10 +1,7 @@
1
- import axios from "axios";
1
+ import { requester } from "./requester.js";
2
2
  import { utils } from "@bililive-tools/manager";
3
3
  import { assert, getFormatSources } from "./utils.js";
4
4
  import { initInfo } from "./anticode.js";
5
- const requester = axios.create({
6
- timeout: 10e3,
7
- });
8
5
  export async function getRoomInfo(roomIdOrShortId, opts = {}) {
9
6
  const res = await requester.get(`https://www.huya.com/${roomIdOrShortId}`);
10
7
  const html = res.data;
@@ -1,16 +1,14 @@
1
1
  // import { createHash, randomInt } from "node:crypto";
2
2
  // import { URLSearchParams } from "node:url";
3
- import axios from "axios";
3
+ import { requester } from "./requester.js";
4
4
  import { utils } from "@bililive-tools/manager";
5
5
  import { assert, getFormatSources } from "./utils.js";
6
- const requester = axios.create({
7
- timeout: 10e3,
8
- headers: {
9
- "User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko)",
10
- },
11
- });
12
6
  export async function getRoomInfo(roomIdOrShortId, formatPriorities = ["flv", "hls"]) {
13
- const res = await requester.get(`https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=${roomIdOrShortId}`);
7
+ const res = await requester.get(`https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=${roomIdOrShortId}`, {
8
+ headers: {
9
+ "User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko)",
10
+ },
11
+ });
14
12
  const html = res.data;
15
13
  assert(html, `Unexpected resp, hyPlayerConfig is null`);
16
14
  if (res.status !== 200) {
package/lib/index.js CHANGED
@@ -56,27 +56,44 @@ function createRecorder(opts) {
56
56
  });
57
57
  return recorderWithSupportUpdatedEvent;
58
58
  }
59
- const ffmpegOutputOptions = [
60
- "-c",
61
- "copy",
62
- "-movflags",
63
- "faststart+frag_keyframe+empty_moov",
64
- "-min_frag_duration",
65
- "10000000",
66
- ];
67
- const ffmpegInputOptions = [
68
- "-reconnect",
69
- "1",
70
- "-reconnect_streamed",
71
- "1",
72
- "-reconnect_delay_max",
73
- "10",
74
- "-rw_timeout",
75
- "15000000",
76
- ];
59
+ const ffmpegOutputOptions = [];
60
+ const ffmpegInputOptions = ["-rw_timeout", "10000000", "-timeout", "10000000"];
77
61
  const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
78
- if (this.recordHandle != null)
62
+ // 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
63
+ if (this.recordHandle != null) {
64
+ // 只有当设置了标题关键词时,并且不是手动启动的录制,才获取最新的直播间信息
65
+ if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
66
+ const now = Date.now();
67
+ // 每5分钟检查一次标题变化
68
+ const titleCheckInterval = 5 * 60 * 1000; // 5分钟
69
+ // 获取上次检查时间
70
+ const lastCheckTime = typeof this.extra.lastTitleCheckTime === "number" ? this.extra.lastTitleCheckTime : 0;
71
+ // 如果距离上次检查时间不足指定间隔,则跳过检查
72
+ if (now - lastCheckTime < titleCheckInterval) {
73
+ return this.recordHandle;
74
+ }
75
+ // 更新检查时间
76
+ this.extra.lastTitleCheckTime = now;
77
+ // 获取直播间信息
78
+ const liveInfo = await getInfo(this.channelId);
79
+ const { title } = liveInfo;
80
+ // 检查标题是否包含关键词
81
+ if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
82
+ this.state = "title-blocked";
83
+ this.emit("DebugLog", {
84
+ type: "common",
85
+ text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
86
+ });
87
+ // 停止录制
88
+ await this.recordHandle.stop("直播间标题包含关键词");
89
+ // 返回 null,停止录制
90
+ return null;
91
+ }
92
+ }
93
+ // 已经在录制中,直接返回
79
94
  return this.recordHandle;
95
+ }
96
+ // 获取直播间信息
80
97
  try {
81
98
  const liveInfo = await getInfo(this.channelId);
82
99
  this.liveInfo = liveInfo;
@@ -86,7 +103,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
86
103
  this.state = "check-error";
87
104
  throw error;
88
105
  }
89
- const { living, owner, title } = this.liveInfo;
106
+ const { living, owner, title, startTime } = this.liveInfo;
90
107
  if (this.liveInfo.liveId === banLiveId) {
91
108
  this.tempStopIntervalCheck = true;
92
109
  }
@@ -97,6 +114,18 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
97
114
  return null;
98
115
  if (!living)
99
116
  return null;
117
+ // 检查标题是否包含关键词,如果包含则不自动录制
118
+ // 手动开始录制时不检查标题关键词
119
+ if (utils.shouldCheckTitleKeywords(isManualStart, this.titleKeywords)) {
120
+ if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
121
+ this.state = "title-blocked";
122
+ this.emit("DebugLog", {
123
+ type: "common",
124
+ text: `跳过录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
125
+ });
126
+ return null;
127
+ }
128
+ }
100
129
  let res;
101
130
  // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
102
131
  try {
@@ -149,15 +178,22 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
149
178
  const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
150
179
  this.recordHandle?.stop(reason);
151
180
  };
152
- let recorderType = this.recorderType === "mesio" ? "mesio" : "ffmpeg";
153
- const recorder = createBaseRecorder(recorderType, {
181
+ const recordStartTime = new Date();
182
+ const recorder = createBaseRecorder(this.recorderType, {
154
183
  url: stream.url,
155
184
  outputOptions: ffmpegOutputOptions,
156
185
  inputOptions: ffmpegInputOptions,
157
186
  segment: this.segment ?? 0,
158
- getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
187
+ getSavePath: (opts) => getSavePath({
188
+ owner,
189
+ title: opts.title ?? title,
190
+ startTime: opts.startTime,
191
+ liveStartTime: startTime,
192
+ recordStartTime,
193
+ }),
159
194
  disableDanma: this.disableProvideCommentsWhenRecording,
160
195
  videoFormat: this.videoFormat ?? "auto",
196
+ debugLevel: this.debugLevel ?? "none",
161
197
  }, onEnd, async () => {
162
198
  const info = await getInfo(this.channelId);
163
199
  return info;
@@ -165,6 +201,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
165
201
  const savePath = getSavePath({
166
202
  owner,
167
203
  title,
204
+ startTime: Date.now(),
205
+ liveStartTime: startTime,
206
+ recordStartTime,
168
207
  });
169
208
  try {
170
209
  ensureFolderExist(savePath);
@@ -173,8 +212,8 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
173
212
  this.state = "idle";
174
213
  throw err;
175
214
  }
176
- const handleVideoCreated = async ({ filename, title, cover }) => {
177
- this.emit("videoFileCreated", { filename, cover });
215
+ const handleVideoCreated = async ({ filename, title, cover, rawFilename }) => {
216
+ this.emit("videoFileCreated", { filename, cover, rawFilename });
178
217
  if (title && this?.liveInfo) {
179
218
  this.liveInfo.title = title;
180
219
  }
@@ -255,7 +294,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
255
294
  client.on("retry", (e) => {
256
295
  this.emit("DebugLog", {
257
296
  type: "common",
258
- text: `huya danmu retry: ${e.count}/${e.max}`,
297
+ text: `${this?.liveInfo?.owner}:${this.channelId} huya danmu retry: ${e.count}/${e.max}`,
259
298
  });
260
299
  });
261
300
  client.start();
@@ -297,6 +336,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
297
336
  id: genRecordUUID(),
298
337
  stream: stream.name,
299
338
  source: stream.source,
339
+ recorderType: recorder.type,
300
340
  url: stream.url,
301
341
  ffmpegArgs,
302
342
  savePath: savePath,
@@ -0,0 +1,2 @@
1
+ import axios from "axios";
2
+ export declare const requester: axios.AxiosInstance;
@@ -0,0 +1,7 @@
1
+ import axios from "axios";
2
+ export const requester = axios.create({
3
+ timeout: 10e3,
4
+ // axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,
5
+ // 但会导致请求报错 "Client network socket disconnected before secure TLS connection was established"。
6
+ proxy: false,
7
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/huya-recorder",
3
- "version": "1.7.1",
3
+ "version": "1.9.0",
4
4
  "description": "bililive-tools huya recorder implemention",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -37,8 +37,8 @@
37
37
  "mitt": "^3.0.1",
38
38
  "lodash-es": "^4.17.21",
39
39
  "axios": "^1.7.8",
40
- "@bililive-tools/manager": "^1.6.1",
41
- "huya-danma-listener": "0.1.2"
40
+ "huya-danma-listener": "0.1.3",
41
+ "@bililive-tools/manager": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {},
44
44
  "scripts": {