@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 +1 -0
- package/lib/huya_api.js +1 -0
- package/lib/huya_wup_api.d.ts +47 -86
- package/lib/huya_wup_api.js +147 -150
- package/lib/index.js +6 -5
- package/lib/requester.d.ts +1 -2
- package/lib/stream.d.ts +2 -0
- package/lib/stream.js +10 -12
- package/lib/utils.d.ts +0 -1
- package/lib/utils.js +0 -25
- package/package.json +3 -3
package/lib/huya_api.d.ts
CHANGED
package/lib/huya_api.js
CHANGED
package/lib/huya_wup_api.d.ts
CHANGED
|
@@ -1,125 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 虎牙 WUP 协议客户端
|
|
3
|
-
* 参考
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
export interface CdnTokenExInfo {
|
|
14
|
+
sFlvToken: string;
|
|
15
|
+
iExpireTime: number;
|
|
16
|
+
ua: string;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
19
|
+
* UserId 结构体
|
|
18
20
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
39
|
-
* 对应 Rust 代码中的 GetCdnTokenInfoReq
|
|
35
|
+
* GetCdnTokenExReq 请求结构体
|
|
40
36
|
*/
|
|
41
|
-
declare class
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
*
|
|
60
|
-
* 对应 Rust 代码中的 HuyaGetTokenResp
|
|
48
|
+
* GetCdnTokenExRsp 响应结构体
|
|
61
49
|
*/
|
|
62
|
-
declare class
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
85
|
-
|
|
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
|
|
66
|
+
declare function buildGetCdnTokenInfoExRequest(streamName: string, ua: string): Buffer;
|
|
93
67
|
/**
|
|
94
|
-
* 解码
|
|
95
|
-
* 对应 Rust 中的 decode_get_cdn_token_info_response 函数
|
|
96
|
-
*
|
|
68
|
+
* 解码 getCdnTokenInfoEx 响应
|
|
97
69
|
* @param responseBytes - 响应二进制数据
|
|
98
70
|
* @returns 解码后的响应对象
|
|
99
71
|
*/
|
|
100
|
-
declare function
|
|
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
|
-
* @
|
|
121
|
-
* @param presenterUid - 主播 UID
|
|
122
|
-
* @returns 包含 flvAntiCode 和 hlsAntiCode
|
|
83
|
+
* @returns CDN Token 信息
|
|
123
84
|
*/
|
|
124
|
-
export declare function
|
|
125
|
-
export {
|
|
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, };
|
package/lib/huya_wup_api.js
CHANGED
|
@@ -1,140 +1,170 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 虎牙 WUP 协议客户端
|
|
3
|
-
* 参考
|
|
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
|
-
*
|
|
16
|
-
* 对应 Rust 代码中的 GetCdnTokenInfoReq
|
|
17
|
+
* UserId 结构体
|
|
17
18
|
*/
|
|
18
|
-
class
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
35
|
-
os.writeString(1, this.
|
|
36
|
-
os.
|
|
37
|
-
os.
|
|
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.
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
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
|
-
*
|
|
52
|
-
* 对应 Rust 代码中的 HuyaGetTokenResp
|
|
91
|
+
* GetCdnTokenExRsp 响应结构体
|
|
53
92
|
*/
|
|
54
|
-
class
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
streamName;
|
|
58
|
-
presenterUid;
|
|
59
|
-
antiCode;
|
|
60
|
-
sTime;
|
|
61
|
-
flvAntiCode;
|
|
62
|
-
hlsAntiCode;
|
|
93
|
+
class HuyaGetCdnTokenExRsp {
|
|
94
|
+
sFlvToken;
|
|
95
|
+
iExpireTime;
|
|
63
96
|
constructor() {
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
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.
|
|
79
|
-
os.
|
|
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.
|
|
93
|
-
this.
|
|
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
|
-
*
|
|
107
|
-
|
|
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
|
|
115
|
-
// 1.
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
-
//
|
|
120
|
-
tup.tupVersion = 3;
|
|
121
|
-
tup.requestId =
|
|
122
|
-
tup.servantName = "liveui";
|
|
123
|
-
tup.funcName = "
|
|
124
|
-
//
|
|
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
|
-
//
|
|
158
|
+
// 6. 编码为二进制
|
|
127
159
|
const binBuffer = tup.encode();
|
|
128
160
|
return binBuffer.toNodeBuffer();
|
|
129
161
|
}
|
|
130
162
|
/**
|
|
131
|
-
* 解码
|
|
132
|
-
* 对应 Rust 中的 decode_get_cdn_token_info_response 函数
|
|
133
|
-
*
|
|
163
|
+
* 解码 getCdnTokenInfoEx 响应
|
|
134
164
|
* @param responseBytes - 响应二进制数据
|
|
135
165
|
* @returns 解码后的响应对象
|
|
136
166
|
*/
|
|
137
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
212
|
-
* @param presenterUid - 主播 UID
|
|
213
|
-
* @returns 包含 flvAntiCode 和 hlsAntiCode
|
|
205
|
+
* @returns CDN Token 信息
|
|
214
206
|
*/
|
|
215
|
-
export async function
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
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
|
-
|
|
220
|
+
HuyaUserId, HuyaGetCdnTokenExReq, HuyaGetCdnTokenExRsp,
|
|
224
221
|
// 核心函数
|
|
225
|
-
|
|
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 =
|
|
134
|
-
|
|
135
|
-
|
|
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);
|
package/lib/requester.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
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 {
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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.
|
|
38
|
+
"axios": "^1.15.0",
|
|
39
39
|
"lodash-es": "^4.17.21",
|
|
40
40
|
"mitt": "^3.0.1",
|
|
41
|
-
"@bililive-tools/manager": "^1.
|
|
41
|
+
"@bililive-tools/manager": "^1.14.1",
|
|
42
42
|
"huya-danma-listener": "0.1.4"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|