@bililive-tools/huya-recorder 1.12.0 → 1.15.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/lib/huya_api.d.ts CHANGED
@@ -17,4 +17,5 @@ export declare function getRoomInfo(roomIdOrShortId: string, opts?: {
17
17
  startTime: Date;
18
18
  liveId: string;
19
19
  gid: number;
20
+ area: string;
20
21
  }>;
package/lib/huya_api.js CHANGED
@@ -93,5 +93,6 @@ export async function getRoomInfo(roomIdOrShortId, opts = {}) {
93
93
  startTime,
94
94
  liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
95
95
  gid: data.gameLiveInfo.gid,
96
+ area: data.gameLiveInfo.gameFullName,
96
97
  };
97
98
  }
@@ -1,125 +1,86 @@
1
1
  /**
2
2
  * 虎牙 WUP 协议客户端
3
- * 参考 https://github.com/hua0512/rust-srec 实现
3
+ * 参考 DanmakuRender 项目实现
4
+ * @see https://github.com/SmallPeaches/DanmakuRender/pull/527/files
4
5
  */
5
6
  declare const WUP_URL = "https://wup.huya.com";
7
+ declare const WUP_YST_URL = "https://snmhuya.yst.aisee.tv";
6
8
  declare const WUP_UA = "HYSDK(Windows, 30000002)_APP(pc_exe&7030003&official)_SDK(trans&2.29.0.5493)";
7
9
  declare const HUYA_ORIGIN = "https://www.huya.com";
8
10
  /**
9
- * 流信息接口
11
+ * CDN Token 扩展响应接口
10
12
  */
11
- export interface StreamInfo {
12
- url: string;
13
- streamFormat: string;
14
- extras?: StreamExtras;
13
+ export interface CdnTokenExInfo {
14
+ sFlvToken: string;
15
+ iExpireTime: number;
16
+ ua: string;
15
17
  }
16
18
  /**
17
- * 流扩展信息接口
19
+ * UserId 结构体
18
20
  */
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;
21
+ declare class HuyaUserId {
22
+ lUid: number;
23
+ sGuid: string;
24
+ sToken: string;
25
+ sHuYaUA: string;
26
+ sCookie: string;
27
+ iTokenType: number;
28
+ sDeviceId: string;
29
+ sQIMEI: string;
30
+ constructor();
31
+ _writeTo(os: any): void;
32
+ _readFrom(is: any): void;
36
33
  }
37
34
  /**
38
- * GetCdnTokenInfoReq 请求结构体
39
- * 对应 Rust 代码中的 GetCdnTokenInfoReq
35
+ * GetCdnTokenExReq 请求结构体
40
36
  */
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
- */
37
+ declare class HuyaGetCdnTokenExReq {
38
+ sFlvUrl: string;
39
+ sStreamName: string;
40
+ iLoopTime: number;
41
+ tId: HuyaUserId;
42
+ iAppId: number;
43
+ constructor(streamName?: string);
51
44
  _writeTo(os: any): void;
52
- /**
53
- * TARS 解码方法 - 从输入流读取结构体
54
- * @param is - TARS 输入流
55
- */
56
45
  _readFrom(is: any): void;
57
46
  }
58
47
  /**
59
- * HuyaGetTokenResp 响应结构体
60
- * 对应 Rust 代码中的 HuyaGetTokenResp
48
+ * GetCdnTokenExRsp 响应结构体
61
49
  */
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;
50
+ declare class HuyaGetCdnTokenExRsp {
51
+ sFlvToken: string;
52
+ iExpireTime: number;
71
53
  constructor();
72
- /**
73
- * TARS 编码方法 - 将结构体写入输出流
74
- * @param os - TARS 输出流
75
- */
76
54
  _writeTo(os: any): void;
77
- /**
78
- * TARS 解码方法 - 从输入流读取结构体
79
- * @param is - TARS 输入流
80
- */
81
55
  _readFrom(is: any): void;
82
56
  }
83
57
  /**
84
- * 构建 getCdnTokenInfo 请求
85
- * 对应 Rust 中的 build_get_cdn_token_info_request 函数
86
- *
58
+ * 生成随机虎牙 UA
59
+ */
60
+ declare function generateRandomHuYaUA(): string;
61
+ /**
62
+ * 构建 getCdnTokenInfoEx 请求
87
63
  * @param streamName - 流名称
88
- * @param cdnType - CDN 类型 (例如: "AL")
89
- * @param presenterUid - 主播 UID
90
64
  * @returns TARS 编码的请求体
91
65
  */
92
- declare function buildGetCdnTokenInfoRequest(streamName: string, cdnType: string, presenterUid: number): Buffer;
66
+ declare function buildGetCdnTokenInfoExRequest(streamName: string, ua: string): Buffer;
93
67
  /**
94
- * 解码 getCdnTokenInfo 响应
95
- * 对应 Rust 中的 decode_get_cdn_token_info_response 函数
96
- *
68
+ * 解码 getCdnTokenInfoEx 响应
97
69
  * @param responseBytes - 响应二进制数据
98
70
  * @returns 解码后的响应对象
99
71
  */
100
- declare function decodeGetCdnTokenInfoResponse(responseBytes: Buffer): HuyaGetTokenResp;
72
+ declare function decodeGetCdnTokenInfoExResponse(responseBytes: Buffer): HuyaGetCdnTokenExRsp;
101
73
  /**
102
74
  * 发送 WUP 请求到虎牙服务器
103
- *
104
75
  * @param requestBody - 请求体
76
+ * @param funcName - 函数名
105
77
  * @returns 响应体
106
78
  */
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>;
79
+ declare function sendWupRequest(requestBody: Buffer, funcName?: string, ua?: string): Promise<Buffer>;
116
80
  /**
117
- * 简化版本:直接获取防盗链参数
118
- *
81
+ * 获取 CDN Token 信息(使用 getCdnTokenInfoEx API)
119
82
  * @param streamName - 流名称
120
- * @param cdnType - CDN 类型
121
- * @param presenterUid - 主播 UID
122
- * @returns 包含 flvAntiCode 和 hlsAntiCode
83
+ * @returns CDN Token 信息
123
84
  */
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, };
85
+ export declare function getCdnTokenInfoEx(streamName: string): Promise<CdnTokenExInfo>;
86
+ export { HuyaUserId, HuyaGetCdnTokenExReq, HuyaGetCdnTokenExRsp, buildGetCdnTokenInfoExRequest, decodeGetCdnTokenInfoExResponse, sendWupRequest, generateRandomHuYaUA, WUP_URL, WUP_YST_URL, WUP_UA, HUYA_ORIGIN, };
@@ -1,140 +1,170 @@
1
1
  /**
2
2
  * 虎牙 WUP 协议客户端
3
- * 参考 https://github.com/hua0512/rust-srec 实现
3
+ * 参考 DanmakuRender 项目实现
4
+ * @see https://github.com/SmallPeaches/DanmakuRender/pull/527/files
4
5
  */
5
6
  import { requester } from "./requester.js";
6
7
  import TarsStream from "@tars/stream";
7
8
  const Tup = TarsStream.Tup;
8
9
  const WUP_URL = "https://wup.huya.com";
10
+ const WUP_YST_URL = "https://snmhuya.yst.aisee.tv";
9
11
  const WUP_UA = "HYSDK(Windows, 30000002)_APP(pc_exe&7030003&official)_SDK(trans&2.29.0.5493)";
10
12
  const HUYA_ORIGIN = "https://www.huya.com";
11
13
  // ============================================================================
12
14
  // TARS 结构体定义
13
15
  // ============================================================================
14
16
  /**
15
- * GetCdnTokenInfoReq 请求结构体
16
- * 对应 Rust 代码中的 GetCdnTokenInfoReq
17
+ * UserId 结构体
17
18
  */
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
19
+ class HuyaUserId {
20
+ lUid;
21
+ sGuid;
22
+ sToken;
23
+ sHuYaUA;
24
+ sCookie;
25
+ iTokenType;
26
+ sDeviceId;
27
+ sQIMEI;
28
+ constructor() {
29
+ this.lUid = 0; // tag=0
30
+ this.sGuid = ""; // tag=1
31
+ this.sToken = ""; // tag=2
32
+ this.sHuYaUA = ""; // tag=3
33
+ this.sCookie = ""; // tag=4
34
+ this.iTokenType = 0; // tag=5
35
+ this.sDeviceId = ""; // tag=6
36
+ this.sQIMEI = ""; // tag=7
37
+ }
38
+ _writeTo(os) {
39
+ os.writeInt64(0, this.lUid);
40
+ os.writeString(1, this.sGuid);
41
+ os.writeString(2, this.sToken);
42
+ os.writeString(3, this.sHuYaUA);
43
+ os.writeString(4, this.sCookie);
44
+ os.writeInt32(5, this.iTokenType);
45
+ os.writeString(6, this.sDeviceId);
46
+ os.writeString(7, this.sQIMEI);
47
+ }
48
+ _readFrom(is) {
49
+ this.lUid = is.readInt64(0, false, 0);
50
+ this.sGuid = is.readString(1, false, "");
51
+ this.sToken = is.readString(2, false, "");
52
+ this.sHuYaUA = is.readString(3, false, "");
53
+ this.sCookie = is.readString(4, false, "");
54
+ this.iTokenType = is.readInt32(5, false, 0);
55
+ this.sDeviceId = is.readString(6, false, "");
56
+ this.sQIMEI = is.readString(7, false, "");
57
+ }
58
+ }
59
+ /**
60
+ * GetCdnTokenExReq 请求结构体
61
+ */
62
+ class HuyaGetCdnTokenExReq {
63
+ sFlvUrl;
64
+ sStreamName;
65
+ iLoopTime;
66
+ tId;
67
+ iAppId;
68
+ constructor(streamName = "") {
69
+ this.sFlvUrl = ""; // tag=0
70
+ this.sStreamName = streamName; // tag=1
71
+ this.iLoopTime = 0; // tag=2
72
+ this.tId = new HuyaUserId(); // tag=3
73
+ this.iAppId = 66; // tag=4
28
74
  }
29
- /**
30
- * TARS 编码方法 - 将结构体写入输出流
31
- * @param os - TARS 输出流
32
- */
33
75
  _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);
76
+ os.writeString(0, this.sFlvUrl);
77
+ os.writeString(1, this.sStreamName);
78
+ os.writeInt32(2, this.iLoopTime);
79
+ os.writeStruct(3, this.tId);
80
+ os.writeInt32(4, this.iAppId);
38
81
  }
39
- /**
40
- * TARS 解码方法 - 从输入流读取结构体
41
- * @param is - TARS 输入流
42
- */
43
82
  _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);
83
+ this.sFlvUrl = is.readString(0, false, "");
84
+ this.sStreamName = is.readString(1, false, "");
85
+ this.iLoopTime = is.readInt32(2, false, 0);
86
+ this.tId = is.readStruct(3, false, HuyaUserId);
87
+ this.iAppId = is.readInt32(4, false, 66);
48
88
  }
49
89
  }
50
90
  /**
51
- * HuyaGetTokenResp 响应结构体
52
- * 对应 Rust 代码中的 HuyaGetTokenResp
91
+ * GetCdnTokenExRsp 响应结构体
53
92
  */
54
- class HuyaGetTokenResp {
55
- url;
56
- cdnType;
57
- streamName;
58
- presenterUid;
59
- antiCode;
60
- sTime;
61
- flvAntiCode;
62
- hlsAntiCode;
93
+ class HuyaGetCdnTokenExRsp {
94
+ sFlvToken;
95
+ iExpireTime;
63
96
  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
97
+ this.sFlvToken = ""; // tag=0
98
+ this.iExpireTime = 0; // tag=1
72
99
  }
73
- /**
74
- * TARS 编码方法 - 将结构体写入输出流
75
- * @param os - TARS 输出流
76
- */
77
100
  _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);
101
+ os.writeString(0, this.sFlvToken);
102
+ os.writeInt64(1, this.iExpireTime);
86
103
  }
87
- /**
88
- * TARS 解码方法 - 从输入流读取结构体
89
- * @param is - TARS 输入流
90
- */
91
104
  _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, "");
105
+ this.sFlvToken = is.readString(0, false, "");
106
+ this.iExpireTime = is.readInt64(1, false, 0);
100
107
  }
101
108
  }
102
109
  // ============================================================================
103
110
  // WUP 协议处理
104
111
  // ============================================================================
105
112
  /**
106
- * 构建 getCdnTokenInfo 请求
107
- * 对应 Rust 中的 build_get_cdn_token_info_request 函数
108
- *
113
+ * 生成随机虎牙 UA
114
+ */
115
+ function generateRandomHuYaUA() {
116
+ const platforms = [
117
+ { name: "adr", version: "13.1.0", hasApiLevel: true },
118
+ { name: "ios", version: "13.1.0", hasApiLevel: false },
119
+ { name: "huya_nftv", version: "2.6.10", hasApiLevel: true },
120
+ { name: "pc_exe", version: "7000000", hasApiLevel: false },
121
+ ];
122
+ const platform = platforms[Math.floor(Math.random() * platforms.length)];
123
+ let version = platform.version;
124
+ // 对于支持多版本号的平台,添加随机版本号
125
+ if (platform.name === "adr" || platform.name === "huya_nftv") {
126
+ const subVersion = Math.floor(Math.random() * 2000) + 3000;
127
+ version = `${version}.${subVersion}`;
128
+ }
129
+ let ua = `${platform.name}&${version}&official`;
130
+ // 添加 Android API Level
131
+ if (platform.hasApiLevel) {
132
+ const apiLevel = Math.floor(Math.random() * 9) + 28; // 28-36
133
+ ua = `${ua}&${apiLevel}`;
134
+ }
135
+ return ua;
136
+ }
137
+ /**
138
+ * 构建 getCdnTokenInfoEx 请求
109
139
  * @param streamName - 流名称
110
- * @param cdnType - CDN 类型 (例如: "AL")
111
- * @param presenterUid - 主播 UID
112
140
  * @returns TARS 编码的请求体
113
141
  */
114
- function buildGetCdnTokenInfoRequest(streamName, cdnType, presenterUid) {
115
- // 1. 创建请求对象
116
- const req = new GetCdnTokenInfoReq("", streamName, cdnType, presenterUid);
117
- // 2. 创建 TUP 实例
142
+ function buildGetCdnTokenInfoExRequest(streamName, ua) {
143
+ // 1. 创建 UserId 对象
144
+ const userId = new HuyaUserId();
145
+ userId.sHuYaUA = ua;
146
+ // 2. 创建请求对象
147
+ const req = new HuyaGetCdnTokenExReq(streamName);
148
+ req.tId = userId;
149
+ // 3. 创建 TUP 实例
118
150
  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"]
151
+ // 4. 设置请求头信息
152
+ tup.tupVersion = 3;
153
+ tup.requestId = Math.abs(Math.floor(Math.random() * 1000000));
154
+ tup.servantName = "liveui";
155
+ tup.funcName = "getCdnTokenInfoEx";
156
+ // 5. 写入请求结构体到 body["tReq"]
125
157
  tup.writeStruct("tReq", req);
126
- // 5. 编码为二进制
158
+ // 6. 编码为二进制
127
159
  const binBuffer = tup.encode();
128
160
  return binBuffer.toNodeBuffer();
129
161
  }
130
162
  /**
131
- * 解码 getCdnTokenInfo 响应
132
- * 对应 Rust 中的 decode_get_cdn_token_info_response 函数
133
- *
163
+ * 解码 getCdnTokenInfoEx 响应
134
164
  * @param responseBytes - 响应二进制数据
135
165
  * @returns 解码后的响应对象
136
166
  */
137
- function decodeGetCdnTokenInfoResponse(responseBytes) {
167
+ function decodeGetCdnTokenInfoExResponse(responseBytes) {
138
168
  // 1. 将 Node.js Buffer 转换为 BinBuffer
139
169
  const binBuffer = new TarsStream.BinBuffer();
140
170
  binBuffer.writeNodeBuffer(responseBytes);
@@ -142,21 +172,25 @@ function decodeGetCdnTokenInfoResponse(responseBytes) {
142
172
  const tup = new Tup();
143
173
  tup.decode(binBuffer);
144
174
  // 3. 读取响应结构体 body["tRsp"]
145
- const resp = new HuyaGetTokenResp();
175
+ const resp = new HuyaGetCdnTokenExRsp();
146
176
  tup.readStruct("tRsp", resp);
147
- // 4. 返回响应对象
148
177
  return resp;
149
178
  }
150
179
  /**
151
180
  * 发送 WUP 请求到虎牙服务器
152
- *
153
181
  * @param requestBody - 请求体
182
+ * @param funcName - 函数名
154
183
  * @returns 响应体
155
184
  */
156
- async function sendWupRequest(requestBody) {
157
- const response = await requester.post(WUP_URL, requestBody, {
185
+ async function sendWupRequest(requestBody, funcName, ua) {
186
+ // 随机选择服务器地址
187
+ let url = WUP_URL;
188
+ if (Math.random() > 0.5 && funcName) {
189
+ url = `${WUP_YST_URL}/liveui/${funcName}`;
190
+ }
191
+ const response = await requester.post(url, requestBody, {
158
192
  headers: {
159
- "User-Agent": WUP_UA,
193
+ "User-Agent": ua ?? WUP_UA,
160
194
  Origin: HUYA_ORIGIN,
161
195
  Referer: HUYA_ORIGIN,
162
196
  "Content-Type": "application/octet-stream",
@@ -166,62 +200,25 @@ async function sendWupRequest(requestBody) {
166
200
  return Buffer.from(response.data);
167
201
  }
168
202
  /**
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
- *
203
+ * 获取 CDN Token 信息(使用 getCdnTokenInfoEx API)
210
204
  * @param streamName - 流名称
211
- * @param cdnType - CDN 类型
212
- * @param presenterUid - 主播 UID
213
- * @returns 包含 flvAntiCode 和 hlsAntiCode
205
+ * @returns CDN Token 信息
214
206
  */
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;
207
+ export async function getCdnTokenInfoEx(streamName) {
208
+ const ua = generateRandomHuYaUA();
209
+ const requestBody = buildGetCdnTokenInfoExRequest(streamName, ua);
210
+ const responseBytes = await sendWupRequest(requestBody, "getCdnTokenInfoEx", ua);
211
+ const tokenInfo = decodeGetCdnTokenInfoExResponse(responseBytes);
212
+ return {
213
+ sFlvToken: tokenInfo.sFlvToken,
214
+ iExpireTime: tokenInfo.iExpireTime,
215
+ ua,
216
+ };
220
217
  }
221
218
  export {
222
219
  // 类
223
- GetCdnTokenInfoReq, HuyaGetTokenResp,
220
+ HuyaUserId, HuyaGetCdnTokenExReq, HuyaGetCdnTokenExRsp,
224
221
  // 核心函数
225
- buildGetCdnTokenInfoRequest, decodeGetCdnTokenInfoResponse, sendWupRequest,
222
+ buildGetCdnTokenInfoExRequest, decodeGetCdnTokenInfoExResponse, sendWupRequest, generateRandomHuYaUA,
226
223
  // 常量
227
- WUP_URL, WUP_UA, HUYA_ORIGIN, };
224
+ WUP_URL, WUP_YST_URL, WUP_UA, HUYA_ORIGIN, };
package/lib/index.js CHANGED
@@ -130,10 +130,11 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
130
130
  const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
131
131
  this.recordHandle?.stop(reason);
132
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
- }
133
+ // let ua =
134
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";
135
+ // if (res.api === "wup") {
136
+ // ua = "HYSDK(Windows,30000002)_APP(pc_exe&7030003&official)_SDK(trans&2.29.0.5493)";
137
+ // }
137
138
  const downloader = createDownloader(this.recorderType, {
138
139
  url: stream.url,
139
140
  outputOptions: ffmpegOutputOptions,
@@ -150,7 +151,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
150
151
  videoFormat: this.videoFormat ?? "auto",
151
152
  debugLevel: this.debugLevel ?? "none",
152
153
  headers: {
153
- "User-Agent": ua,
154
+ "User-Agent": stream.ua,
154
155
  },
155
156
  }, onEnd, async () => {
156
157
  const info = await getInfo(this.channelId);
@@ -1,2 +1 @@
1
- import axios from "axios";
2
- export declare const requester: axios.AxiosInstance;
1
+ export declare const requester: import("axios").AxiosInstance;
package/lib/stream.d.ts CHANGED
@@ -10,12 +10,14 @@ export declare function getInfo(channelId: string): Promise<{
10
10
  liveStartTime: Date;
11
11
  liveId: string;
12
12
  recordStartTime: Date;
13
+ area: string;
13
14
  }>;
14
15
  export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities" | "api" | "formatPriorities"> & {
15
16
  strictQuality?: boolean;
16
17
  api?: "web" | "mp" | "auto" | "wup";
17
18
  }): Promise<{
18
19
  currentStream: {
20
+ ua: string;
19
21
  name: string;
20
22
  source: string;
21
23
  uid: number;
package/lib/stream.js CHANGED
@@ -2,7 +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
+ import { getCdnTokenInfoEx } from "./huya_wup_api.js";
6
6
  import { assert } from "./utils.js";
7
7
  export async function getInfo(channelId) {
8
8
  const info = await getRoomInfoByWeb(channelId);
@@ -17,6 +17,7 @@ export async function getInfo(channelId) {
17
17
  liveStartTime: info.startTime,
18
18
  liveId: info.liveId,
19
19
  recordStartTime: recordStartTime,
20
+ area: info.area,
20
21
  };
21
22
  }
22
23
  async function getRoomInfo(channelId, options) {
@@ -27,8 +28,7 @@ async function getRoomInfo(channelId, options) {
27
28
  });
28
29
  if (info.gid == 1663 || info.gid == 1) {
29
30
  // 1663=星秀区,1=英雄联盟区
30
- return getRoomInfoByMobile(channelId, options.formatPriorities);
31
- // TODO: wup 接口出问题了,先切回mp吧
31
+ // return getRoomInfoByMobile(channelId, options.formatPriorities);
32
32
  return getRoomInfo(channelId, {
33
33
  api: "wup",
34
34
  formatPriorities: options.formatPriorities,
@@ -96,17 +96,14 @@ export async function getStream(opts) {
96
96
  }
97
97
  }
98
98
  let url = expectSource.url;
99
+ 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";
99
100
  if (info.api === "wup") {
100
101
  try {
101
- let newUrl = await getStreamUrlWup({
102
- url,
103
- streamFormat: expectSource.suffix,
104
- extras: {
105
- stream_name: expectSource.streamName,
106
- cdn: expectSource.name,
107
- presenter_uid: expectSource.presenterUid,
108
- },
109
- });
102
+ const res = await getCdnTokenInfoEx(expectSource.streamName);
103
+ let urlObj = new URL(url);
104
+ urlObj.search = `?${res.sFlvToken}`;
105
+ let newUrl = urlObj.toString();
106
+ ua = res.ua;
110
107
  if (!newUrl.includes("codec=")) {
111
108
  newUrl += "&codec=264";
112
109
  }
@@ -123,6 +120,7 @@ export async function getStream(opts) {
123
120
  return {
124
121
  ...info,
125
122
  currentStream: {
123
+ ua: ua,
126
124
  name: expectStream.desc,
127
125
  source: expectSource.name,
128
126
  uid: expectSource.presenterUid,
package/lib/utils.d.ts CHANGED
@@ -21,7 +21,6 @@ export declare function assert(assertion: unknown, msg?: string): asserts assert
21
21
  export declare function assertStringType(data: unknown, msg?: string): asserts data is string;
22
22
  export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
23
23
  export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
24
- export declare function createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean;
25
24
  export declare function getFormatSources(sources: StreamResult, formatPriorities?: Array<"flv" | "hls">): {
26
25
  sources: SourceProfile[];
27
26
  formatName: "flv" | "hls";
package/lib/utils.js CHANGED
@@ -59,31 +59,6 @@ export function assertNumberType(data, msg) {
59
59
  export function assertObjectType(data, msg) {
60
60
  assert(typeof data === "object", msg);
61
61
  }
62
- export function createInvalidStreamChecker() {
63
- let prevFrame = 0;
64
- let frameUnchangedCount = 0;
65
- return (ffmpegLogLine) => {
66
- const streamInfo = ffmpegLogLine.match(/frame=\s*(\d+) fps=.*? q=.*? size=.*? time=.*? bitrate=.*? speed=.*?/);
67
- if (streamInfo != null) {
68
- const [, frameText] = streamInfo;
69
- const frame = Number(frameText);
70
- if (frame === prevFrame) {
71
- if (++frameUnchangedCount >= 15) {
72
- return true;
73
- }
74
- }
75
- else {
76
- prevFrame = frame;
77
- frameUnchangedCount = 0;
78
- }
79
- return false;
80
- }
81
- if (ffmpegLogLine.includes("HTTP error 404 Not Found")) {
82
- return true;
83
- }
84
- return false;
85
- };
86
- }
87
62
  // 根据formatPriorities获取最终的sources
88
63
  // 如果formatPriorities为空或者undefined,则使用['flv','hls']
89
64
  // 如果有参数,按照顺序进行匹配,如果匹配的值不存在或者为空,则使用下一个参数,最后返回的是流数组
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/huya-recorder",
3
- "version": "1.12.0",
3
+ "version": "1.15.0",
4
4
  "description": "bililive-tools huya recorder implemention",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -35,10 +35,10 @@
35
35
  "license": "LGPL",
36
36
  "dependencies": {
37
37
  "@tars/stream": "^2.0.3",
38
- "axios": "^1.7.8",
38
+ "axios": "^1.15.0",
39
39
  "lodash-es": "^4.17.21",
40
40
  "mitt": "^3.0.1",
41
- "@bililive-tools/manager": "^1.12.0",
41
+ "@bililive-tools/manager": "^1.14.1",
42
42
  "huya-danma-listener": "0.1.4"
43
43
  },
44
44
  "scripts": {