@bililive-tools/huya-recorder 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- 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 +45 -86
- package/lib/stream.d.ts +7 -2
- package/lib/stream.js +40 -2
- package/lib/types.d.ts +7 -8
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -41,11 +41,11 @@ interface Options {
|
|
|
41
41
|
sourcePriorities: []; // 按提供的源优先级去给CDN列表排序,并过滤掉不在优先级配置中的源,在未匹配到的情况下会优先使用TX的CDN,具体参数见 CDN 参数
|
|
42
42
|
formatPriorities?: string[]; // 支持,`flv`和`hls` 参数,默认为['flv','hls']
|
|
43
43
|
disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
|
|
44
|
-
segment?: number; //
|
|
44
|
+
segment?: number | string; // 分段参数,单位分钟,如果以"B","KB","MB","GB"结尾,会尝试使用文件大小分段,仅推荐在使用mesio录制引擎时使用
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import mitt from "mitt";
|
|
3
|
-
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils,
|
|
3
|
+
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, createDownloader, } from "@bililive-tools/manager";
|
|
4
4
|
import { getInfo, getStream } from "./stream.js";
|
|
5
5
|
import { ensureFolderExist } from "./utils.js";
|
|
6
6
|
import HuYaDanMu from "huya-danma-listener";
|
|
@@ -13,9 +13,9 @@ function createRecorder(opts) {
|
|
|
13
13
|
// @ts-ignore
|
|
14
14
|
...mitt(),
|
|
15
15
|
...opts,
|
|
16
|
+
cache: null,
|
|
16
17
|
availableStreams: [],
|
|
17
18
|
availableSources: [],
|
|
18
|
-
qualityMaxRetry: opts.qualityRetry ?? 0,
|
|
19
19
|
qualityRetry: opts.qualityRetry ?? 0,
|
|
20
20
|
state: "idle",
|
|
21
21
|
api: opts.api ?? "auto",
|
|
@@ -61,34 +61,9 @@ const ffmpegInputOptions = ["-rw_timeout", "10000000", "-timeout", "10000000"];
|
|
|
61
61
|
const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
|
|
62
62
|
// 如果已经在录制中,只在需要检查标题关键词时才获取最新信息
|
|
63
63
|
if (this.recordHandle != null) {
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
// 每5分钟检查一次标题变化
|
|
68
|
-
const titleCheckInterval = 5 * 60 * 1000; // 5分钟
|
|
69
|
-
// 获取上次检查时间
|
|
70
|
-
const lastCheckTime = typeof this.extra.lastTitleCheckTime === "number" ? this.extra.lastTitleCheckTime : 0;
|
|
71
|
-
// 如果距离上次检查时间不足指定间隔,则跳过检查
|
|
72
|
-
if (now - lastCheckTime < titleCheckInterval) {
|
|
73
|
-
return this.recordHandle;
|
|
74
|
-
}
|
|
75
|
-
// 更新检查时间
|
|
76
|
-
this.extra.lastTitleCheckTime = now;
|
|
77
|
-
// 获取直播间信息
|
|
78
|
-
const liveInfo = await getInfo(this.channelId);
|
|
79
|
-
const { title } = liveInfo;
|
|
80
|
-
// 检查标题是否包含关键词
|
|
81
|
-
if (utils.hasBlockedTitleKeywords(title, this.titleKeywords)) {
|
|
82
|
-
this.state = "title-blocked";
|
|
83
|
-
this.emit("DebugLog", {
|
|
84
|
-
type: "common",
|
|
85
|
-
text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
|
|
86
|
-
});
|
|
87
|
-
// 停止录制
|
|
88
|
-
await this.recordHandle.stop("直播间标题包含关键词");
|
|
89
|
-
// 返回 null,停止录制
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
64
|
+
const shouldStop = await utils.checkTitleKeywordsWhileRecording(this, isManualStart, getInfo);
|
|
65
|
+
if (shouldStop) {
|
|
66
|
+
return null;
|
|
92
67
|
}
|
|
93
68
|
// 已经在录制中,直接返回
|
|
94
69
|
return this.recordHandle;
|
|
@@ -103,7 +78,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
103
78
|
this.state = "check-error";
|
|
104
79
|
throw error;
|
|
105
80
|
}
|
|
106
|
-
const { living, owner, title,
|
|
81
|
+
const { living, owner, title, liveStartTime, recordStartTime } = this.liveInfo;
|
|
107
82
|
if (this.liveInfo.liveId === banLiveId) {
|
|
108
83
|
this.tempStopIntervalCheck = true;
|
|
109
84
|
}
|
|
@@ -114,44 +89,26 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
114
89
|
return null;
|
|
115
90
|
if (!living)
|
|
116
91
|
return null;
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.emit("DebugLog", {
|
|
123
|
-
type: "common",
|
|
124
|
-
text: `跳过录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
|
|
125
|
-
});
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
92
|
+
// 检查标题是否包含关键词
|
|
93
|
+
if (utils.checkTitleKeywordsBeforeRecord(title, this, isManualStart))
|
|
94
|
+
return null;
|
|
95
|
+
const qualityRetryLeft = (await this.cache.get("qualityRetryLeft")) ?? this.qualityRetry;
|
|
96
|
+
const strictQuality = utils.shouldUseStrictQuality(qualityRetryLeft, this.qualityRetry, isManualStart);
|
|
129
97
|
let res;
|
|
130
|
-
// TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
|
|
131
98
|
try {
|
|
132
|
-
let strictQuality = false;
|
|
133
|
-
if (this.qualityRetry > 0) {
|
|
134
|
-
strictQuality = true;
|
|
135
|
-
}
|
|
136
|
-
if (this.qualityMaxRetry < 0) {
|
|
137
|
-
strictQuality = true;
|
|
138
|
-
}
|
|
139
|
-
if (isManualStart) {
|
|
140
|
-
strictQuality = false;
|
|
141
|
-
}
|
|
142
99
|
res = await getStream({
|
|
143
100
|
channelId: this.channelId,
|
|
144
101
|
quality: this.quality,
|
|
145
102
|
streamPriorities: this.streamPriorities,
|
|
146
103
|
sourcePriorities: this.sourcePriorities,
|
|
147
|
-
api: this.api,
|
|
104
|
+
api: this.api, //"wup"
|
|
148
105
|
strictQuality,
|
|
149
106
|
formatPriorities: this.formatPriorities,
|
|
150
107
|
});
|
|
151
108
|
}
|
|
152
109
|
catch (err) {
|
|
153
|
-
if (
|
|
154
|
-
this.
|
|
110
|
+
if (qualityRetryLeft > 0)
|
|
111
|
+
await this.cache.set("qualityRetryLeft", qualityRetryLeft - 1);
|
|
155
112
|
this.state = "check-error";
|
|
156
113
|
throw err;
|
|
157
114
|
}
|
|
@@ -162,12 +119,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
162
119
|
this.usedStream = stream.name;
|
|
163
120
|
this.usedSource = stream.source;
|
|
164
121
|
let isEnded = false;
|
|
165
|
-
let isCutting = false;
|
|
166
122
|
const onEnd = (...args) => {
|
|
167
|
-
if (isCutting) {
|
|
168
|
-
isCutting = false;
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
123
|
if (isEnded)
|
|
172
124
|
return;
|
|
173
125
|
isEnded = true;
|
|
@@ -178,8 +130,11 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
178
130
|
const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
179
131
|
this.recordHandle?.stop(reason);
|
|
180
132
|
};
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
137
|
+
const downloader = createDownloader(this.recorderType, {
|
|
183
138
|
url: stream.url,
|
|
184
139
|
outputOptions: ffmpegOutputOptions,
|
|
185
140
|
inputOptions: ffmpegInputOptions,
|
|
@@ -188,12 +143,15 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
188
143
|
owner,
|
|
189
144
|
title: opts.title ?? title,
|
|
190
145
|
startTime: opts.startTime,
|
|
191
|
-
liveStartTime
|
|
146
|
+
liveStartTime,
|
|
192
147
|
recordStartTime,
|
|
193
148
|
}),
|
|
194
149
|
disableDanma: this.disableProvideCommentsWhenRecording,
|
|
195
150
|
videoFormat: this.videoFormat ?? "auto",
|
|
196
151
|
debugLevel: this.debugLevel ?? "none",
|
|
152
|
+
headers: {
|
|
153
|
+
"User-Agent": ua,
|
|
154
|
+
},
|
|
197
155
|
}, onEnd, async () => {
|
|
198
156
|
const info = await getInfo(this.channelId);
|
|
199
157
|
return info;
|
|
@@ -202,7 +160,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
202
160
|
owner,
|
|
203
161
|
title,
|
|
204
162
|
startTime: Date.now(),
|
|
205
|
-
liveStartTime
|
|
163
|
+
liveStartTime,
|
|
206
164
|
recordStartTime,
|
|
207
165
|
});
|
|
208
166
|
try {
|
|
@@ -220,24 +178,24 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
220
178
|
if (cover && this?.liveInfo) {
|
|
221
179
|
this.liveInfo.cover = cover;
|
|
222
180
|
}
|
|
223
|
-
const extraDataController =
|
|
181
|
+
const extraDataController = downloader.getExtraDataController();
|
|
224
182
|
extraDataController?.setMeta({
|
|
225
183
|
room_id: this.channelId,
|
|
226
184
|
platform: provider?.id,
|
|
227
|
-
liveStartTimestamp: this?.liveInfo?.
|
|
185
|
+
liveStartTimestamp: this?.liveInfo?.liveStartTime?.getTime(),
|
|
228
186
|
// recordStopTimestamp: Date.now(),
|
|
229
187
|
title: title,
|
|
230
188
|
user_name: owner,
|
|
231
189
|
});
|
|
232
190
|
};
|
|
233
|
-
|
|
234
|
-
|
|
191
|
+
downloader.on("videoFileCreated", handleVideoCreated);
|
|
192
|
+
downloader.on("videoFileCompleted", ({ filename }) => {
|
|
235
193
|
this.emit("videoFileCompleted", { filename });
|
|
236
194
|
});
|
|
237
|
-
|
|
195
|
+
downloader.on("DebugLog", (data) => {
|
|
238
196
|
this.emit("DebugLog", data);
|
|
239
197
|
});
|
|
240
|
-
|
|
198
|
+
downloader.on("progress", (progress) => {
|
|
241
199
|
if (this.recordHandle) {
|
|
242
200
|
this.recordHandle.progress = progress;
|
|
243
201
|
}
|
|
@@ -245,9 +203,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
245
203
|
});
|
|
246
204
|
let client = null;
|
|
247
205
|
if (!this.disableProvideCommentsWhenRecording) {
|
|
248
|
-
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
|
+
});
|
|
249
212
|
client.on("message", (msg) => {
|
|
250
|
-
const extraDataController =
|
|
213
|
+
const extraDataController = downloader.getExtraDataController();
|
|
251
214
|
if (!extraDataController)
|
|
252
215
|
return;
|
|
253
216
|
switch (msg.type) {
|
|
@@ -299,17 +262,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
299
262
|
});
|
|
300
263
|
client.start();
|
|
301
264
|
}
|
|
302
|
-
const
|
|
303
|
-
|
|
265
|
+
const downloaderArgs = downloader.getArguments();
|
|
266
|
+
downloader.run();
|
|
304
267
|
const cut = utils.singleton(async () => {
|
|
305
268
|
if (!this.recordHandle)
|
|
306
269
|
return;
|
|
307
|
-
|
|
308
|
-
return;
|
|
309
|
-
isCutting = true;
|
|
310
|
-
await recorder.stop();
|
|
311
|
-
recorder.createCommand();
|
|
312
|
-
recorder.run();
|
|
270
|
+
downloader.cut();
|
|
313
271
|
});
|
|
314
272
|
const stop = utils.singleton(async (reason) => {
|
|
315
273
|
if (!this.recordHandle)
|
|
@@ -317,12 +275,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
317
275
|
this.state = "stopping-record";
|
|
318
276
|
try {
|
|
319
277
|
client?.stop();
|
|
320
|
-
await
|
|
278
|
+
await downloader.stop();
|
|
321
279
|
}
|
|
322
280
|
catch (err) {
|
|
323
281
|
this.emit("DebugLog", {
|
|
324
282
|
type: "error",
|
|
325
|
-
text: `stop
|
|
283
|
+
text: `stop record error: ${String(err)}`,
|
|
326
284
|
});
|
|
327
285
|
}
|
|
328
286
|
this.usedStream = undefined;
|
|
@@ -331,14 +289,15 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
331
289
|
this.recordHandle = undefined;
|
|
332
290
|
this.liveInfo = undefined;
|
|
333
291
|
this.state = "idle";
|
|
292
|
+
this.cache.set("qualityRetryLeft", this.qualityRetry);
|
|
334
293
|
});
|
|
335
294
|
this.recordHandle = {
|
|
336
295
|
id: genRecordUUID(),
|
|
337
296
|
stream: stream.name,
|
|
338
297
|
source: stream.source,
|
|
339
|
-
recorderType:
|
|
298
|
+
recorderType: downloader.type,
|
|
340
299
|
url: stream.url,
|
|
341
|
-
|
|
300
|
+
downloaderArgs,
|
|
342
301
|
savePath: savePath,
|
|
343
302
|
stop,
|
|
344
303
|
cut,
|
package/lib/stream.d.ts
CHANGED
|
@@ -7,19 +7,24 @@ export declare function getInfo(channelId: string): Promise<{
|
|
|
7
7
|
roomId: number;
|
|
8
8
|
avatar: string;
|
|
9
9
|
cover: string;
|
|
10
|
-
|
|
10
|
+
liveStartTime: Date;
|
|
11
11
|
liveId: string;
|
|
12
|
+
recordStartTime: Date;
|
|
12
13
|
}>;
|
|
13
14
|
export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities" | "api" | "formatPriorities"> & {
|
|
14
15
|
strictQuality?: boolean;
|
|
15
|
-
api?: "web" | "mp" | "auto";
|
|
16
|
+
api?: "web" | "mp" | "auto" | "wup";
|
|
16
17
|
}): Promise<{
|
|
17
18
|
currentStream: {
|
|
18
19
|
name: string;
|
|
19
20
|
source: string;
|
|
21
|
+
uid: number;
|
|
22
|
+
subChannelId: number;
|
|
23
|
+
channelId: number;
|
|
20
24
|
url: string;
|
|
21
25
|
};
|
|
22
26
|
living: boolean;
|
|
27
|
+
api: string;
|
|
23
28
|
id: number;
|
|
24
29
|
owner: string;
|
|
25
30
|
title: string;
|
package/lib/stream.js
CHANGED
|
@@ -2,9 +2,11 @@ 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);
|
|
9
|
+
const recordStartTime = new Date();
|
|
8
10
|
return {
|
|
9
11
|
living: info.living,
|
|
10
12
|
owner: info.owner,
|
|
@@ -12,8 +14,9 @@ export async function getInfo(channelId) {
|
|
|
12
14
|
avatar: info.avatar,
|
|
13
15
|
cover: info.cover,
|
|
14
16
|
roomId: info.roomId,
|
|
15
|
-
|
|
17
|
+
liveStartTime: info.startTime,
|
|
16
18
|
liveId: info.liveId,
|
|
19
|
+
recordStartTime: recordStartTime,
|
|
17
20
|
};
|
|
18
21
|
}
|
|
19
22
|
async function getRoomInfo(channelId, options) {
|
|
@@ -23,7 +26,11 @@ async function getRoomInfo(channelId, options) {
|
|
|
23
26
|
quality: options.quality,
|
|
24
27
|
});
|
|
25
28
|
if (info.gid == 1663) {
|
|
26
|
-
return
|
|
29
|
+
return getRoomInfo(channelId, {
|
|
30
|
+
api: "wup",
|
|
31
|
+
formatPriorities: options.formatPriorities,
|
|
32
|
+
quality: options.quality,
|
|
33
|
+
});
|
|
27
34
|
}
|
|
28
35
|
return info;
|
|
29
36
|
}
|
|
@@ -36,6 +43,14 @@ async function getRoomInfo(channelId, options) {
|
|
|
36
43
|
quality: options.quality,
|
|
37
44
|
});
|
|
38
45
|
}
|
|
46
|
+
else if (options.api == "wup") {
|
|
47
|
+
// 参数与web一致,之后anticode有额外处理
|
|
48
|
+
const info = await getRoomInfoByWeb(channelId, {
|
|
49
|
+
formatPriorities: options.formatPriorities,
|
|
50
|
+
quality: options.quality,
|
|
51
|
+
});
|
|
52
|
+
return { ...info, api: "wup" };
|
|
53
|
+
}
|
|
39
54
|
assert(false, "Invalid api");
|
|
40
55
|
}
|
|
41
56
|
export async function getStream(opts) {
|
|
@@ -78,6 +93,26 @@ export async function getStream(opts) {
|
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
let url = expectSource.url;
|
|
96
|
+
if (info.api === "wup") {
|
|
97
|
+
try {
|
|
98
|
+
let newUrl = await getStreamUrlWup({
|
|
99
|
+
url,
|
|
100
|
+
streamFormat: expectSource.suffix,
|
|
101
|
+
extras: {
|
|
102
|
+
stream_name: expectSource.streamName,
|
|
103
|
+
cdn: expectSource.name,
|
|
104
|
+
presenter_uid: expectSource.presenterUid,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
if (!newUrl.includes("codec=")) {
|
|
108
|
+
newUrl += "&codec=264";
|
|
109
|
+
}
|
|
110
|
+
url = newUrl;
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
console.warn("Get stream url by WUP failed, fallback to original url", e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
81
116
|
// MP协议下原画不需要添加ratio参数
|
|
82
117
|
if (expectStream.bitRate && expectStream.bitRate !== -1 && !url.includes("ratio=")) {
|
|
83
118
|
url = url + "&ratio=" + expectStream.bitRate;
|
|
@@ -87,6 +122,9 @@ export async function getStream(opts) {
|
|
|
87
122
|
currentStream: {
|
|
88
123
|
name: expectStream.desc,
|
|
89
124
|
source: expectSource.name,
|
|
125
|
+
uid: expectSource.presenterUid,
|
|
126
|
+
subChannelId: expectSource.subChannelId,
|
|
127
|
+
channelId: expectSource.channelId,
|
|
90
128
|
url,
|
|
91
129
|
},
|
|
92
130
|
};
|
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.0",
|
|
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.10.0",
|
|
42
|
+
"huya-danma-listener": "0.1.4"
|
|
42
43
|
},
|
|
43
|
-
"devDependencies": {},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc",
|
|
46
46
|
"watch": "tsc -w"
|