@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 +12 -1
- package/lib/huya_api.d.ts +1 -0
- package/lib/huya_api.js +11 -0
- package/lib/huya_mobile_api.d.ts +1 -0
- package/lib/huya_mobile_api.js +12 -0
- package/lib/huya_wup_api.d.ts +125 -0
- package/lib/huya_wup_api.js +227 -0
- package/lib/index.js +16 -14
- package/lib/stream.d.ts +5 -1
- package/lib/stream.js +39 -2
- package/lib/types.d.ts +7 -8
- package/package.json +6 -6
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
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,
|
package/lib/huya_mobile_api.d.ts
CHANGED
package/lib/huya_mobile_api.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
38
|
-
"lodash-es": "^4.17.21",
|
|
37
|
+
"@tars/stream": "^2.0.3",
|
|
39
38
|
"axios": "^1.7.8",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
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"
|