@bililive-tools/huya-recorder 1.10.0 → 1.11.1

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
@@ -45,7 +45,7 @@ interface Options {
45
45
  disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
46
46
  saveGiftDanma?: boolean; // 保存礼物弹幕
47
47
  saveCover?: boolean; // 保存封面
48
- api?: "auto" | "mp" | "web"; // 默认为auto,在星秀区使用mp接口,其他使用web接口,你也可以强制指定
48
+ api?: "auto" | "mp" | "web" | "wup"; // 默认为auto,在星秀区使用mp接口,其他使用web接口,你也可以强制指定
49
49
  videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
50
50
  recorderType?: "auto" | "ffmpeg" | "mesio"; // 底层录制器,使用mesio时videoFormat参数无效
51
51
  debugLevel?: `verbose` | "basic"; // verbose参数时,录制器会输出更加详细的log
@@ -100,6 +100,17 @@ const url = "https://www.huya.com/910323";
100
100
  const { id } = await provider.resolveChannelInfoFromURL(url);
101
101
  ```
102
102
 
103
+ ### API 接口选择
104
+
105
+ 虎牙提供多种API接口,可以根据情况选择:
106
+
107
+ | 接口类型 | 说明 |
108
+ | -------- | ---------------------------------------- |
109
+ | auto | 一般使用web接口,星秀区使用wup接口 |
110
+ | web | 你web看到的东西,星秀区会两分钟分段 |
111
+ | wup | 神奇的接口 |
112
+ | mp | 小程序接口,也可以录星秀区,但画质差了点 |
113
+
103
114
  # 协议
104
115
 
105
116
  与原项目保存一致为 LGPL
package/lib/huya_api.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare function getRoomInfo(roomIdOrShortId: string, opts?: {
5
5
  quality?: Recorder["quality"];
6
6
  }): Promise<{
7
7
  living: boolean;
8
+ api: string;
8
9
  id: number;
9
10
  owner: string;
10
11
  title: string;
package/lib/huya_api.js CHANGED
@@ -47,6 +47,11 @@ export async function getRoomInfo(roomIdOrShortId, opts = {}) {
47
47
  sources.flv.push({
48
48
  name: item.sCdnType,
49
49
  url,
50
+ streamName: sStreamName,
51
+ presenterUid: item.lPresenterUid,
52
+ subChannelId: item.lSubChannelId,
53
+ channelId: item.lChannelId,
54
+ suffix: item.sFlvUrlSuffix,
50
55
  });
51
56
  }
52
57
  if (item.sHlsAntiCode && item.sHlsAntiCode.length > 0) {
@@ -64,6 +69,11 @@ export async function getRoomInfo(roomIdOrShortId, opts = {}) {
64
69
  sources.hls.push({
65
70
  name: item.sCdnType,
66
71
  url,
72
+ streamName: sStreamName,
73
+ presenterUid: item.lPresenterUid,
74
+ subChannelId: item.lSubChannelId,
75
+ channelId: item.lChannelId,
76
+ suffix: item.sHlsUrlSuffix,
67
77
  });
68
78
  }
69
79
  }
@@ -71,6 +81,7 @@ export async function getRoomInfo(roomIdOrShortId, opts = {}) {
71
81
  const formatSources = getFormatSources(sources, opts.formatPriorities);
72
82
  return {
73
83
  living: vMultiStreamInfo.length > 0 && data.gameStreamInfoList.length > 0,
84
+ api: "web",
74
85
  id: data.gameLiveInfo.profileRoom,
75
86
  owner: data.gameLiveInfo.nick,
76
87
  title: data.gameLiveInfo.introduction,
@@ -1,6 +1,7 @@
1
1
  import type { StreamProfile } from "./types.js";
2
2
  export declare function getRoomInfo(roomIdOrShortId: string, formatPriorities?: Array<"flv" | "hls">): Promise<{
3
3
  living: boolean;
4
+ api: string;
4
5
  id: number;
5
6
  owner: string;
6
7
  title: string;
@@ -19,6 +19,7 @@ export async function getRoomInfo(roomIdOrShortId, formatPriorities = ["flv", "h
19
19
  flv: [],
20
20
  hls: [],
21
21
  };
22
+ // console.log("profile", JSON.stringify(profile, null, 2));
22
23
  // const uid = await getAnonymousUid();
23
24
  for (const item of profile?.stream?.baseSteamInfoList ?? []) {
24
25
  if (item.sFlvAntiCode && item.sFlvAntiCode.length > 0) {
@@ -32,6 +33,11 @@ export async function getRoomInfo(roomIdOrShortId, formatPriorities = ["flv", "h
32
33
  sources.flv.push({
33
34
  name: item.sCdnType,
34
35
  url,
36
+ streamName: item.sStreamName,
37
+ presenterUid: item.lPresenterUid,
38
+ subChannelId: item.lSubChannelId,
39
+ channelId: item.lChannelId,
40
+ suffix: item.sFlvUrlSuffix,
35
41
  });
36
42
  }
37
43
  if (item.sHlsAntiCode && item.sHlsAntiCode.length > 0) {
@@ -39,6 +45,11 @@ export async function getRoomInfo(roomIdOrShortId, formatPriorities = ["flv", "h
39
45
  sources.hls.push({
40
46
  name: item.sCdnType,
41
47
  url,
48
+ streamName: item.sStreamName,
49
+ presenterUid: item.lPresenterUid,
50
+ subChannelId: item.lSubChannelId,
51
+ channelId: item.lChannelId,
52
+ suffix: item.sHlsUrlSuffix,
42
53
  });
43
54
  }
44
55
  }
@@ -59,6 +70,7 @@ export async function getRoomInfo(roomIdOrShortId, formatPriorities = ["flv", "h
59
70
  }
60
71
  return {
61
72
  living: profile.liveStatus === "ON",
73
+ api: "mp",
62
74
  id: profile.liveData.profileRoom,
63
75
  owner: profile.liveData.nick,
64
76
  title: profile.liveData.introduction,
@@ -0,0 +1,125 @@
1
+ /**
2
+ * 虎牙 WUP 协议客户端
3
+ * 参考 https://github.com/hua0512/rust-srec 实现
4
+ */
5
+ declare const WUP_URL = "https://wup.huya.com";
6
+ declare const WUP_UA = "HYSDK(Windows, 30000002)_APP(pc_exe&7030003&official)_SDK(trans&2.29.0.5493)";
7
+ declare const HUYA_ORIGIN = "https://www.huya.com";
8
+ /**
9
+ * 流信息接口
10
+ */
11
+ export interface StreamInfo {
12
+ url: string;
13
+ streamFormat: string;
14
+ extras?: StreamExtras;
15
+ }
16
+ /**
17
+ * 流扩展信息接口
18
+ */
19
+ export interface StreamExtras {
20
+ cdn?: string;
21
+ stream_name: string;
22
+ presenter_uid?: number;
23
+ }
24
+ /**
25
+ * CDN Token 信息接口
26
+ */
27
+ export interface CdnTokenInfo {
28
+ url: string;
29
+ cdnType: string;
30
+ streamName: string;
31
+ presenterUid: number;
32
+ antiCode: string;
33
+ sTime: string;
34
+ flvAntiCode: string;
35
+ hlsAntiCode: string;
36
+ }
37
+ /**
38
+ * GetCdnTokenInfoReq 请求结构体
39
+ * 对应 Rust 代码中的 GetCdnTokenInfoReq
40
+ */
41
+ declare class GetCdnTokenInfoReq {
42
+ url: string;
43
+ cdnType: string;
44
+ streamName: string;
45
+ presenterUid: number;
46
+ constructor(url?: string, streamName?: string, cdnType?: string, presenterUid?: number);
47
+ /**
48
+ * TARS 编码方法 - 将结构体写入输出流
49
+ * @param os - TARS 输出流
50
+ */
51
+ _writeTo(os: any): void;
52
+ /**
53
+ * TARS 解码方法 - 从输入流读取结构体
54
+ * @param is - TARS 输入流
55
+ */
56
+ _readFrom(is: any): void;
57
+ }
58
+ /**
59
+ * HuyaGetTokenResp 响应结构体
60
+ * 对应 Rust 代码中的 HuyaGetTokenResp
61
+ */
62
+ declare class HuyaGetTokenResp {
63
+ url: string;
64
+ cdnType: string;
65
+ streamName: string;
66
+ presenterUid: number;
67
+ antiCode: string;
68
+ sTime: string;
69
+ flvAntiCode: string;
70
+ hlsAntiCode: string;
71
+ constructor();
72
+ /**
73
+ * TARS 编码方法 - 将结构体写入输出流
74
+ * @param os - TARS 输出流
75
+ */
76
+ _writeTo(os: any): void;
77
+ /**
78
+ * TARS 解码方法 - 从输入流读取结构体
79
+ * @param is - TARS 输入流
80
+ */
81
+ _readFrom(is: any): void;
82
+ }
83
+ /**
84
+ * 构建 getCdnTokenInfo 请求
85
+ * 对应 Rust 中的 build_get_cdn_token_info_request 函数
86
+ *
87
+ * @param streamName - 流名称
88
+ * @param cdnType - CDN 类型 (例如: "AL")
89
+ * @param presenterUid - 主播 UID
90
+ * @returns TARS 编码的请求体
91
+ */
92
+ declare function buildGetCdnTokenInfoRequest(streamName: string, cdnType: string, presenterUid: number): Buffer;
93
+ /**
94
+ * 解码 getCdnTokenInfo 响应
95
+ * 对应 Rust 中的 decode_get_cdn_token_info_response 函数
96
+ *
97
+ * @param responseBytes - 响应二进制数据
98
+ * @returns 解码后的响应对象
99
+ */
100
+ declare function decodeGetCdnTokenInfoResponse(responseBytes: Buffer): HuyaGetTokenResp;
101
+ /**
102
+ * 发送 WUP 请求到虎牙服务器
103
+ *
104
+ * @param requestBody - 请求体
105
+ * @returns 响应体
106
+ */
107
+ declare function sendWupRequest(requestBody: Buffer): Promise<Buffer>;
108
+ /**
109
+ * 获取虎牙流的真实 URL(使用 WUP 协议)
110
+ * 对应 Rust 中的 get_stream_url_wup 方法
111
+ *
112
+ * @param streamInfo - 流信息对象
113
+ * @returns 真实流 URL
114
+ */
115
+ export declare function getStreamUrlWup(streamInfo: StreamInfo): Promise<string>;
116
+ /**
117
+ * 简化版本:直接获取防盗链参数
118
+ *
119
+ * @param streamName - 流名称
120
+ * @param cdnType - CDN 类型
121
+ * @param presenterUid - 主播 UID
122
+ * @returns 包含 flvAntiCode 和 hlsAntiCode
123
+ */
124
+ export declare function getCdnTokenInfo(streamName: string, cdnType?: string, presenterUid?: number): Promise<CdnTokenInfo>;
125
+ export { GetCdnTokenInfoReq, HuyaGetTokenResp, buildGetCdnTokenInfoRequest, decodeGetCdnTokenInfoResponse, sendWupRequest, WUP_URL, WUP_UA, HUYA_ORIGIN, };
@@ -0,0 +1,227 @@
1
+ /**
2
+ * 虎牙 WUP 协议客户端
3
+ * 参考 https://github.com/hua0512/rust-srec 实现
4
+ */
5
+ import { requester } from "./requester.js";
6
+ import TarsStream from "@tars/stream";
7
+ const Tup = TarsStream.Tup;
8
+ const WUP_URL = "https://wup.huya.com";
9
+ const WUP_UA = "HYSDK(Windows, 30000002)_APP(pc_exe&7030003&official)_SDK(trans&2.29.0.5493)";
10
+ const HUYA_ORIGIN = "https://www.huya.com";
11
+ // ============================================================================
12
+ // TARS 结构体定义
13
+ // ============================================================================
14
+ /**
15
+ * GetCdnTokenInfoReq 请求结构体
16
+ * 对应 Rust 代码中的 GetCdnTokenInfoReq
17
+ */
18
+ class GetCdnTokenInfoReq {
19
+ url;
20
+ cdnType;
21
+ streamName;
22
+ presenterUid;
23
+ constructor(url = "", streamName = "", cdnType = "", presenterUid = 0) {
24
+ this.url = url; // tag=0, String
25
+ this.cdnType = cdnType; // tag=1, String
26
+ this.streamName = streamName; // tag=2, String
27
+ this.presenterUid = presenterUid; // tag=3, Long/Int64
28
+ }
29
+ /**
30
+ * TARS 编码方法 - 将结构体写入输出流
31
+ * @param os - TARS 输出流
32
+ */
33
+ _writeTo(os) {
34
+ os.writeString(0, this.url);
35
+ os.writeString(1, this.cdnType);
36
+ os.writeString(2, this.streamName);
37
+ os.writeInt64(3, this.presenterUid);
38
+ }
39
+ /**
40
+ * TARS 解码方法 - 从输入流读取结构体
41
+ * @param is - TARS 输入流
42
+ */
43
+ _readFrom(is) {
44
+ this.url = is.readString(0, false, "");
45
+ this.cdnType = is.readString(1, false, "");
46
+ this.streamName = is.readString(2, false, "");
47
+ this.presenterUid = is.readInt64(3, false, 0);
48
+ }
49
+ }
50
+ /**
51
+ * HuyaGetTokenResp 响应结构体
52
+ * 对应 Rust 代码中的 HuyaGetTokenResp
53
+ */
54
+ class HuyaGetTokenResp {
55
+ url;
56
+ cdnType;
57
+ streamName;
58
+ presenterUid;
59
+ antiCode;
60
+ sTime;
61
+ flvAntiCode;
62
+ hlsAntiCode;
63
+ constructor() {
64
+ this.url = ""; // tag=0
65
+ this.cdnType = ""; // tag=1
66
+ this.streamName = ""; // tag=2
67
+ this.presenterUid = 0; // tag=3
68
+ this.antiCode = ""; // tag=4
69
+ this.sTime = ""; // tag=5
70
+ this.flvAntiCode = ""; // tag=6
71
+ this.hlsAntiCode = ""; // tag=7
72
+ }
73
+ /**
74
+ * TARS 编码方法 - 将结构体写入输出流
75
+ * @param os - TARS 输出流
76
+ */
77
+ _writeTo(os) {
78
+ os.writeString(0, this.url);
79
+ os.writeString(1, this.cdnType);
80
+ os.writeString(2, this.streamName);
81
+ os.writeInt64(3, this.presenterUid);
82
+ os.writeString(4, this.antiCode);
83
+ os.writeString(5, this.sTime);
84
+ os.writeString(6, this.flvAntiCode);
85
+ os.writeString(7, this.hlsAntiCode);
86
+ }
87
+ /**
88
+ * TARS 解码方法 - 从输入流读取结构体
89
+ * @param is - TARS 输入流
90
+ */
91
+ _readFrom(is) {
92
+ this.url = is.readString(0, false, "");
93
+ this.cdnType = is.readString(1, false, "");
94
+ this.streamName = is.readString(2, false, "");
95
+ this.presenterUid = is.readInt64(3, false, 0);
96
+ this.antiCode = is.readString(4, false, "");
97
+ this.sTime = is.readString(5, false, "");
98
+ this.flvAntiCode = is.readString(6, false, "");
99
+ this.hlsAntiCode = is.readString(7, false, "");
100
+ }
101
+ }
102
+ // ============================================================================
103
+ // WUP 协议处理
104
+ // ============================================================================
105
+ /**
106
+ * 构建 getCdnTokenInfo 请求
107
+ * 对应 Rust 中的 build_get_cdn_token_info_request 函数
108
+ *
109
+ * @param streamName - 流名称
110
+ * @param cdnType - CDN 类型 (例如: "AL")
111
+ * @param presenterUid - 主播 UID
112
+ * @returns TARS 编码的请求体
113
+ */
114
+ function buildGetCdnTokenInfoRequest(streamName, cdnType, presenterUid) {
115
+ // 1. 创建请求对象
116
+ const req = new GetCdnTokenInfoReq("", streamName, cdnType, presenterUid);
117
+ // 2. 创建 TUP 实例
118
+ const tup = new Tup();
119
+ // 3. 设置请求头信息
120
+ tup.tupVersion = 3; // version: 3
121
+ tup.requestId = 1; // request_id: 1
122
+ tup.servantName = "liveui"; // servant_name: "liveui"
123
+ tup.funcName = "getCdnTokenInfo"; // func_name: "getCdnTokenInfo"
124
+ // 4. 写入请求结构体到 body["tReq"]
125
+ tup.writeStruct("tReq", req);
126
+ // 5. 编码为二进制
127
+ const binBuffer = tup.encode();
128
+ return binBuffer.toNodeBuffer();
129
+ }
130
+ /**
131
+ * 解码 getCdnTokenInfo 响应
132
+ * 对应 Rust 中的 decode_get_cdn_token_info_response 函数
133
+ *
134
+ * @param responseBytes - 响应二进制数据
135
+ * @returns 解码后的响应对象
136
+ */
137
+ function decodeGetCdnTokenInfoResponse(responseBytes) {
138
+ // 1. 将 Node.js Buffer 转换为 BinBuffer
139
+ const binBuffer = new TarsStream.BinBuffer();
140
+ binBuffer.writeNodeBuffer(responseBytes);
141
+ // 2. 创建 TUP 实例并解码
142
+ const tup = new Tup();
143
+ tup.decode(binBuffer);
144
+ // 3. 读取响应结构体 body["tRsp"]
145
+ const resp = new HuyaGetTokenResp();
146
+ tup.readStruct("tRsp", resp);
147
+ // 4. 返回响应对象
148
+ return resp;
149
+ }
150
+ /**
151
+ * 发送 WUP 请求到虎牙服务器
152
+ *
153
+ * @param requestBody - 请求体
154
+ * @returns 响应体
155
+ */
156
+ async function sendWupRequest(requestBody) {
157
+ const response = await requester.post(WUP_URL, requestBody, {
158
+ headers: {
159
+ "User-Agent": WUP_UA,
160
+ Origin: HUYA_ORIGIN,
161
+ Referer: HUYA_ORIGIN,
162
+ "Content-Type": "application/octet-stream",
163
+ },
164
+ responseType: "arraybuffer",
165
+ });
166
+ return Buffer.from(response.data);
167
+ }
168
+ /**
169
+ * 获取虎牙流的真实 URL(使用 WUP 协议)
170
+ * 对应 Rust 中的 get_stream_url_wup 方法
171
+ *
172
+ * @param streamInfo - 流信息对象
173
+ * @returns 真实流 URL
174
+ */
175
+ export async function getStreamUrlWup(streamInfo) {
176
+ // 1. 提取参数
177
+ const { extras } = streamInfo;
178
+ if (!extras) {
179
+ throw new Error("Stream extras not found for WUP request");
180
+ }
181
+ const cdn = extras.cdn || "AL";
182
+ const streamName = extras.stream_name;
183
+ const presenterUid = extras.presenter_uid || 0;
184
+ if (!streamName) {
185
+ throw new Error("Stream name not found in extras");
186
+ }
187
+ // 2. 构建请求
188
+ const requestBody = buildGetCdnTokenInfoRequest(streamName, cdn, presenterUid);
189
+ // 3. 发送请求
190
+ const responseBytes = await sendWupRequest(requestBody);
191
+ // 4. 解码响应
192
+ const tokenInfo = decodeGetCdnTokenInfoResponse(responseBytes);
193
+ // 5. 获取防盗链参数
194
+ const antiCode = streamInfo.streamFormat === "flv" ? tokenInfo.flvAntiCode : tokenInfo.hlsAntiCode;
195
+ // 6. 解析原始 URL
196
+ const url = new URL(streamInfo.url);
197
+ const host = url.host;
198
+ const pathParts = url.pathname.split("/");
199
+ const pathPrefix = pathParts[1] || "";
200
+ const baseUrl = `${url.protocol}//${host}/${pathPrefix}`;
201
+ // 7. 确定文件后缀
202
+ const suffix = streamInfo.streamFormat;
203
+ // 8. 构建新 URL
204
+ const newUrl = `${baseUrl}/${streamName}.${suffix}?${antiCode}`;
205
+ return newUrl;
206
+ }
207
+ /**
208
+ * 简化版本:直接获取防盗链参数
209
+ *
210
+ * @param streamName - 流名称
211
+ * @param cdnType - CDN 类型
212
+ * @param presenterUid - 主播 UID
213
+ * @returns 包含 flvAntiCode 和 hlsAntiCode
214
+ */
215
+ export async function getCdnTokenInfo(streamName, cdnType = "AL", presenterUid = 0) {
216
+ const requestBody = buildGetCdnTokenInfoRequest(streamName, cdnType, presenterUid);
217
+ const responseBytes = await sendWupRequest(requestBody);
218
+ const tokenInfo = decodeGetCdnTokenInfoResponse(responseBytes);
219
+ return tokenInfo;
220
+ }
221
+ export {
222
+ // 类
223
+ GetCdnTokenInfoReq, HuyaGetTokenResp,
224
+ // 核心函数
225
+ buildGetCdnTokenInfoRequest, decodeGetCdnTokenInfoResponse, sendWupRequest,
226
+ // 常量
227
+ WUP_URL, WUP_UA, HUYA_ORIGIN, };
package/lib/index.js CHANGED
@@ -95,14 +95,13 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
95
95
  const qualityRetryLeft = (await this.cache.get("qualityRetryLeft")) ?? this.qualityRetry;
96
96
  const strictQuality = utils.shouldUseStrictQuality(qualityRetryLeft, this.qualityRetry, isManualStart);
97
97
  let res;
98
- // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
99
98
  try {
100
99
  res = await getStream({
101
100
  channelId: this.channelId,
102
101
  quality: this.quality,
103
102
  streamPriorities: this.streamPriorities,
104
103
  sourcePriorities: this.sourcePriorities,
105
- api: this.api,
104
+ api: this.api, //"wup"
106
105
  strictQuality,
107
106
  formatPriorities: this.formatPriorities,
108
107
  });
@@ -120,12 +119,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
120
119
  this.usedStream = stream.name;
121
120
  this.usedSource = stream.source;
122
121
  let isEnded = false;
123
- let isCutting = false;
124
122
  const onEnd = (...args) => {
125
- if (isCutting) {
126
- isCutting = false;
127
- return;
128
- }
129
123
  if (isEnded)
130
124
  return;
131
125
  isEnded = true;
@@ -136,6 +130,10 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
136
130
  const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
137
131
  this.recordHandle?.stop(reason);
138
132
  };
133
+ let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";
134
+ if (res.api === "wup") {
135
+ ua = "HYSDK(Windows,30000002)_APP(pc_exe&7030003&official)_SDK(trans&2.29.0.5493)";
136
+ }
139
137
  const downloader = createDownloader(this.recorderType, {
140
138
  url: stream.url,
141
139
  outputOptions: ffmpegOutputOptions,
@@ -148,8 +146,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
148
146
  liveStartTime,
149
147
  recordStartTime,
150
148
  }),
149
+ disableDanma: this.disableProvideCommentsWhenRecording,
151
150
  videoFormat: this.videoFormat ?? "auto",
152
151
  debugLevel: this.debugLevel ?? "none",
152
+ headers: {
153
+ "User-Agent": ua,
154
+ },
153
155
  }, onEnd, async () => {
154
156
  const info = await getInfo(this.channelId);
155
157
  return info;
@@ -201,7 +203,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
201
203
  });
202
204
  let client = null;
203
205
  if (!this.disableProvideCommentsWhenRecording) {
204
- client = new HuYaDanMu(this.channelId);
206
+ client = new HuYaDanMu({
207
+ roomid: this.channelId,
208
+ uid: res.currentStream.uid,
209
+ subChannelId: res.currentStream.subChannelId,
210
+ channelId: res.currentStream.channelId,
211
+ });
205
212
  client.on("message", (msg) => {
206
213
  const extraDataController = downloader.getExtraDataController();
207
214
  if (!extraDataController)
@@ -260,12 +267,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
260
267
  const cut = utils.singleton(async () => {
261
268
  if (!this.recordHandle)
262
269
  return;
263
- if (isCutting)
264
- return;
265
- isCutting = true;
266
- await downloader.stop();
267
- downloader.createCommand();
268
- downloader.run();
270
+ downloader.cut();
269
271
  });
270
272
  const stop = utils.singleton(async (reason) => {
271
273
  if (!this.recordHandle)
package/lib/stream.d.ts CHANGED
@@ -13,14 +13,18 @@ export declare function getInfo(channelId: string): Promise<{
13
13
  }>;
14
14
  export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities" | "api" | "formatPriorities"> & {
15
15
  strictQuality?: boolean;
16
- api?: "web" | "mp" | "auto";
16
+ api?: "web" | "mp" | "auto" | "wup";
17
17
  }): Promise<{
18
18
  currentStream: {
19
19
  name: string;
20
20
  source: string;
21
+ uid: number;
22
+ subChannelId: number;
23
+ channelId: number;
21
24
  url: string;
22
25
  };
23
26
  living: boolean;
27
+ api: string;
24
28
  id: number;
25
29
  owner: string;
26
30
  title: string;
package/lib/stream.js CHANGED
@@ -2,6 +2,7 @@ import { sortBy } from "lodash-es";
2
2
  import { HuYaQualities } from "@bililive-tools/manager";
3
3
  import { getRoomInfo as getRoomInfoByWeb } from "./huya_api.js";
4
4
  import { getRoomInfo as getRoomInfoByMobile } from "./huya_mobile_api.js";
5
+ import { getStreamUrlWup } from "./huya_wup_api.js";
5
6
  import { assert } from "./utils.js";
6
7
  export async function getInfo(channelId) {
7
8
  const info = await getRoomInfoByWeb(channelId);
@@ -24,8 +25,13 @@ async function getRoomInfo(channelId, options) {
24
25
  formatPriorities: options.formatPriorities,
25
26
  quality: options.quality,
26
27
  });
27
- if (info.gid == 1663) {
28
- return getRoomInfoByMobile(channelId, options.formatPriorities);
28
+ if (info.gid == 1663 || info.gid == 1) {
29
+ // 1663=星秀区,1=英雄联盟区
30
+ return getRoomInfo(channelId, {
31
+ api: "wup",
32
+ formatPriorities: options.formatPriorities,
33
+ quality: options.quality,
34
+ });
29
35
  }
30
36
  return info;
31
37
  }
@@ -38,6 +44,14 @@ async function getRoomInfo(channelId, options) {
38
44
  quality: options.quality,
39
45
  });
40
46
  }
47
+ else if (options.api == "wup") {
48
+ // 参数与web一致,之后anticode有额外处理
49
+ const info = await getRoomInfoByWeb(channelId, {
50
+ formatPriorities: options.formatPriorities,
51
+ quality: options.quality,
52
+ });
53
+ return { ...info, api: "wup" };
54
+ }
41
55
  assert(false, "Invalid api");
42
56
  }
43
57
  export async function getStream(opts) {
@@ -80,6 +94,26 @@ export async function getStream(opts) {
80
94
  }
81
95
  }
82
96
  let url = expectSource.url;
97
+ if (info.api === "wup") {
98
+ try {
99
+ let newUrl = await getStreamUrlWup({
100
+ url,
101
+ streamFormat: expectSource.suffix,
102
+ extras: {
103
+ stream_name: expectSource.streamName,
104
+ cdn: expectSource.name,
105
+ presenter_uid: expectSource.presenterUid,
106
+ },
107
+ });
108
+ if (!newUrl.includes("codec=")) {
109
+ newUrl += "&codec=264";
110
+ }
111
+ url = newUrl;
112
+ }
113
+ catch (e) {
114
+ console.warn("Get stream url by WUP failed, fallback to original url", e);
115
+ }
116
+ }
83
117
  // MP协议下原画不需要添加ratio参数
84
118
  if (expectStream.bitRate && expectStream.bitRate !== -1 && !url.includes("ratio=")) {
85
119
  url = url + "&ratio=" + expectStream.bitRate;
@@ -89,6 +123,9 @@ export async function getStream(opts) {
89
123
  currentStream: {
90
124
  name: expectStream.desc,
91
125
  source: expectSource.name,
126
+ uid: expectSource.presenterUid,
127
+ subChannelId: expectSource.subChannelId,
128
+ channelId: expectSource.channelId,
92
129
  url,
93
130
  },
94
131
  };
package/lib/types.d.ts CHANGED
@@ -1,12 +1,6 @@
1
1
  export interface StreamResult {
2
- flv: {
3
- name: string;
4
- url: string;
5
- }[];
6
- hls: {
7
- name: string;
8
- url: string;
9
- }[];
2
+ flv: SourceProfile[];
3
+ hls: SourceProfile[];
10
4
  }
11
5
  export interface StreamProfile {
12
6
  desc: string;
@@ -15,4 +9,9 @@ export interface StreamProfile {
15
9
  export interface SourceProfile {
16
10
  name: string;
17
11
  url: string;
12
+ streamName: string;
13
+ presenterUid: number;
14
+ subChannelId: number;
15
+ channelId: number;
16
+ suffix: string;
18
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/huya-recorder",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "bililive-tools huya recorder implemention",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -34,13 +34,13 @@
34
34
  "author": "renmu123",
35
35
  "license": "LGPL",
36
36
  "dependencies": {
37
- "mitt": "^3.0.1",
38
- "lodash-es": "^4.17.21",
37
+ "@tars/stream": "^2.0.3",
39
38
  "axios": "^1.7.8",
40
- "@bililive-tools/manager": "^1.10.0",
41
- "huya-danma-listener": "0.1.3"
39
+ "lodash-es": "^4.17.21",
40
+ "mitt": "^3.0.1",
41
+ "@bililive-tools/manager": "^1.11.1",
42
+ "huya-danma-listener": "0.1.4"
42
43
  },
43
- "devDependencies": {},
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "watch": "tsc -w"