@bililive-tools/huya-recorder 1.0.1 → 1.1.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 +43 -14
- package/lib/anticode.d.ts +3 -2
- package/lib/anticode.js +62 -53
- package/lib/huya_api.d.ts +6 -2
- package/lib/huya_api.js +44 -11
- package/lib/huya_mobile_api.d.ts +24 -0
- package/lib/huya_mobile_api.js +83 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +89 -73
- package/lib/stream.d.ts +8 -6
- package/lib/stream.js +67 -33
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +25 -0
- package/package.json +3 -6
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
# 安装
|
|
8
8
|
|
|
9
|
+
**建议所有录制器和manager包都升级到最新版,我不会对兼容性做过多考虑**
|
|
10
|
+
|
|
9
11
|
`npm i @bililive-tools/huya-recorder @bililive-tools/manager`
|
|
10
12
|
|
|
11
13
|
# 使用
|
|
@@ -18,7 +20,7 @@ const manager = createRecorderManager({ providers: [provider] });
|
|
|
18
20
|
manager.addRecorder({
|
|
19
21
|
providerId: provider.id,
|
|
20
22
|
channelId: "7734200",
|
|
21
|
-
quality:
|
|
23
|
+
quality: 0,
|
|
22
24
|
streamPriorities: [],
|
|
23
25
|
sourcePriorities: [],
|
|
24
26
|
});
|
|
@@ -33,28 +35,55 @@ manager.startCheckLoop();
|
|
|
33
35
|
interface Options {
|
|
34
36
|
channelId: string; // 直播间ID,具体解析见文档,也可自行解析
|
|
35
37
|
quality: number; // 见画质参数
|
|
36
|
-
qualityRetry?: number; //
|
|
38
|
+
qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
|
|
37
39
|
streamPriorities: []; // 废弃
|
|
38
|
-
sourcePriorities: []; //
|
|
40
|
+
sourcePriorities: []; // 按提供的源优先级去给CDN列表排序,并过滤掉不在优先级配置中的源,在未匹配到的情况下会优先使用TX的CDN,具体参数见 CDN 参数
|
|
41
|
+
formatName?: "auto" | "flv" | "hls"; // 支持 flv,hls参数,默认使用flv,具体见文档
|
|
39
42
|
disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
|
|
40
|
-
segment?: number; //
|
|
43
|
+
segment?: number; // 分段参数,单位分钟
|
|
41
44
|
disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
|
|
42
45
|
saveGiftDanma?: boolean; // 保存礼物弹幕
|
|
43
46
|
saveCover?: boolean; // 保存封面
|
|
47
|
+
api?: "auto" | "mp" | "web"; // 默认为auto,在星秀区使用mp接口,其他使用web接口,你也可以强制指定
|
|
44
48
|
}
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
### 画质
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
| 蓝光
|
|
56
|
-
|
|
|
57
|
-
|
|
|
53
|
+
| 画质 | 值 |
|
|
54
|
+
| -------- | ----- |
|
|
55
|
+
| 2K HDR | 14100 |
|
|
56
|
+
| 2K | 14000 |
|
|
57
|
+
| HDR(10M) | 4200 |
|
|
58
|
+
| 原画 | 0 |
|
|
59
|
+
| 蓝光20M | 20000 |
|
|
60
|
+
| 蓝光10M | 10000 |
|
|
61
|
+
| 蓝光8M | 8000 |
|
|
62
|
+
| 蓝光4M | 4000 |
|
|
63
|
+
| 超清 | 2000 |
|
|
64
|
+
| 流畅 | 500 |
|
|
65
|
+
|
|
66
|
+
### CDN
|
|
67
|
+
|
|
68
|
+
不同直播间可能支持的cdn并不一致
|
|
69
|
+
|
|
70
|
+
| 画质 | 值 |
|
|
71
|
+
| -------------------------- | ---- |
|
|
72
|
+
| 阿里 | AL |
|
|
73
|
+
| 腾讯 | TX |
|
|
74
|
+
| 华为 | HW |
|
|
75
|
+
| 火山 | HS |
|
|
76
|
+
| 网宿 | WS |
|
|
77
|
+
| 阿里13 | AL13 |
|
|
78
|
+
| 腾讯15 | TX15 |
|
|
79
|
+
| 华为16 | HW16 |
|
|
80
|
+
| 不知道是啥(可能是虎牙自建) | HYZJ |
|
|
81
|
+
|
|
82
|
+
### 流格式
|
|
83
|
+
|
|
84
|
+
hls 可能并不适合 mp 的 api,也许你能找到可以使用的cdn
|
|
85
|
+
|
|
86
|
+
支持 hls 和 flv
|
|
58
87
|
|
|
59
88
|
## 直播间ID解析
|
|
60
89
|
|
|
@@ -63,7 +92,7 @@ interface Options {
|
|
|
63
92
|
```ts
|
|
64
93
|
import { provider } from "@bililive-tools/huya-recorder";
|
|
65
94
|
|
|
66
|
-
const url = "https://
|
|
95
|
+
const url = "https://www.huya.com/910323";
|
|
67
96
|
const { id } = await provider.resolveChannelInfoFromURL(url);
|
|
68
97
|
```
|
|
69
98
|
|
package/lib/anticode.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* 该文件是从 vplayer.js 中提取出来的,保留的部分注释掉的代码是为了标明改动前的代码逻辑
|
|
3
3
|
*/
|
|
4
4
|
export declare function initInfo(info: {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
antiCode: string;
|
|
7
7
|
sStreamName: string;
|
|
8
8
|
_sessionId: number;
|
|
9
|
+
suffix: string;
|
|
9
10
|
}, t?: unknown): any;
|
package/lib/anticode.js
CHANGED
|
@@ -12,8 +12,8 @@ const PLATFORM_TYPE = {
|
|
|
12
12
|
web: 100,
|
|
13
13
|
};
|
|
14
14
|
const PLATFORM_TYPE_NAME = {
|
|
15
|
-
web:
|
|
16
|
-
wap:
|
|
15
|
+
web: "web",
|
|
16
|
+
wap: "wap",
|
|
17
17
|
};
|
|
18
18
|
function pe(e, t) {
|
|
19
19
|
var i = (65535 & e) + (65535 & t);
|
|
@@ -112,7 +112,7 @@ function ge(e, t) {
|
|
|
112
112
|
return [o, h, u, l];
|
|
113
113
|
}
|
|
114
114
|
function Se(e) {
|
|
115
|
-
var t, i =
|
|
115
|
+
var t, i = "", s = 32 * e.length;
|
|
116
116
|
for (t = 0; t < s; t += 8)
|
|
117
117
|
i += String.fromCharCode((e[t >> 5] >>> t % 32) & 255);
|
|
118
118
|
return i;
|
|
@@ -127,9 +127,10 @@ function Te(e) {
|
|
|
127
127
|
return i;
|
|
128
128
|
}
|
|
129
129
|
function Pe(e) {
|
|
130
|
-
var t, i, s =
|
|
130
|
+
var t, i, s = "";
|
|
131
131
|
for (i = 0; i < e.length; i += 1)
|
|
132
|
-
(t = e.charCodeAt(i)),
|
|
132
|
+
(t = e.charCodeAt(i)),
|
|
133
|
+
(s += "0123456789abcdef".charAt((t >>> 4) & 15) + "0123456789abcdef".charAt(15 & t));
|
|
133
134
|
return s;
|
|
134
135
|
}
|
|
135
136
|
function Ee(e) {
|
|
@@ -166,74 +167,78 @@ class Anticode {
|
|
|
166
167
|
}
|
|
167
168
|
parseAnticode(e) {
|
|
168
169
|
var self = this;
|
|
169
|
-
this._fm =
|
|
170
|
-
this[
|
|
171
|
-
this[
|
|
172
|
-
this[
|
|
170
|
+
this._fm = "";
|
|
171
|
+
this["_wsTime"] = "";
|
|
172
|
+
this["_ctype"] = "";
|
|
173
|
+
this["_params"] = [];
|
|
173
174
|
this._sFlvAnticode = e;
|
|
174
|
-
e.split(
|
|
175
|
-
let [key, val] = e.split(
|
|
176
|
-
if (key ===
|
|
175
|
+
e.split("&").forEach(function (e) {
|
|
176
|
+
let [key, val] = e.split("=");
|
|
177
|
+
if (key === "fm") {
|
|
177
178
|
val = decodeURI(val);
|
|
178
179
|
val = unescape(val);
|
|
179
180
|
val = atob(val);
|
|
180
181
|
self._fm = val;
|
|
181
182
|
}
|
|
182
|
-
else if (key ===
|
|
183
|
-
self[
|
|
183
|
+
else if (key === "wsTime") {
|
|
184
|
+
self["_wsTime"] = val;
|
|
184
185
|
var r = 1e3 * parseInt(val, 16) + 3e5;
|
|
185
|
-
self[
|
|
186
|
-
self[
|
|
186
|
+
self["_invalidTime"] = performance.now() + (r - Date.now());
|
|
187
|
+
self["_nextRefreshTime"] = self["_invalidTime"] - 3e4;
|
|
187
188
|
}
|
|
188
189
|
else
|
|
189
|
-
key ==
|
|
190
|
+
key == "ctype" ? (self["_ctype"] = val) : key !== "wsSecret" && self["_params"].push(e);
|
|
190
191
|
});
|
|
191
192
|
}
|
|
192
193
|
hasAnticode() {
|
|
193
|
-
return
|
|
194
|
+
return "" !== this._sFlvAnticode;
|
|
194
195
|
}
|
|
195
196
|
getAnticode(e) {
|
|
196
|
-
if (
|
|
197
|
+
if ("" === this._fm)
|
|
197
198
|
return this._sFlvAnticode;
|
|
198
|
-
const platform =
|
|
199
|
+
const platform = "web";
|
|
199
200
|
const i = PLATFORM_TYPE[platform] || PLATFORM_TYPE.web;
|
|
200
201
|
this._seqid = Number(this.uid) + Date.now();
|
|
201
|
-
var s = keHash(
|
|
202
|
+
var s = keHash("".concat(this._seqid, "|").concat(this._ctype, "|").concat(i));
|
|
202
203
|
var uid = platform === PLATFORM_TYPE_NAME.wap ? this.uid : this.convertUid;
|
|
203
|
-
var r = this._fm
|
|
204
|
+
var r = this._fm
|
|
205
|
+
.replace("$0", uid)
|
|
206
|
+
.replace("$1", this._sStreamName)
|
|
207
|
+
.replace("$2", s)
|
|
208
|
+
.replace("$3", this._wsTime);
|
|
204
209
|
if (e)
|
|
205
210
|
r += Ye;
|
|
206
|
-
var n =
|
|
207
|
-
.concat(
|
|
208
|
-
.concat(
|
|
211
|
+
var n = ""
|
|
212
|
+
.concat("wsSecret")
|
|
213
|
+
.concat("=")
|
|
209
214
|
.concat(keHash(r))
|
|
210
|
-
.concat(
|
|
211
|
-
.concat(
|
|
212
|
-
.concat(
|
|
215
|
+
.concat("&")
|
|
216
|
+
.concat("wsTime")
|
|
217
|
+
.concat("=")
|
|
213
218
|
.concat(this._wsTime)
|
|
214
|
-
.concat(
|
|
215
|
-
.concat(
|
|
216
|
-
.concat(
|
|
219
|
+
.concat("&")
|
|
220
|
+
.concat("seqid")
|
|
221
|
+
.concat("=")
|
|
217
222
|
.concat(this._seqid)
|
|
218
|
-
.concat(
|
|
219
|
-
.concat(
|
|
220
|
-
.concat(
|
|
223
|
+
.concat("&")
|
|
224
|
+
.concat("ctype")
|
|
225
|
+
.concat("=")
|
|
221
226
|
.concat(this._ctype)
|
|
222
|
-
.concat(
|
|
223
|
-
.concat(
|
|
227
|
+
.concat("&")
|
|
228
|
+
.concat("ver=1");
|
|
224
229
|
if (this._params.length > 0) {
|
|
225
|
-
n +=
|
|
230
|
+
n += "&" + this._params.join("&");
|
|
226
231
|
}
|
|
227
232
|
return n;
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
235
|
export function initInfo(info, t) {
|
|
231
|
-
if (info.
|
|
232
|
-
info.url =
|
|
236
|
+
if (info.baseUrl && info.sStreamName) {
|
|
237
|
+
info.url = "".concat(info.baseUrl, "/").concat(info.sStreamName, `.${info.suffix}`);
|
|
233
238
|
}
|
|
234
239
|
var i = [];
|
|
235
240
|
const anticode = new Anticode();
|
|
236
|
-
anticode.init(info.
|
|
241
|
+
anticode.init(info.baseUrl, info.sStreamName, info.antiCode);
|
|
237
242
|
if (anticode.hasAnticode()) {
|
|
238
243
|
i.push(anticode.getAnticode(t));
|
|
239
244
|
// c.a.log('FlvPlayer.initInfo, anticode='.concat(this.anticode.getAnticode(t)))
|
|
@@ -245,7 +250,7 @@ export function initInfo(info, t) {
|
|
|
245
250
|
// } else {
|
|
246
251
|
// localStorage._ratio && i.push('&ratio='.concat(localStorage._ratio))
|
|
247
252
|
// }
|
|
248
|
-
i.push('ratio='.concat(info.curBitrate))
|
|
253
|
+
// i.push('ratio='.concat(info.curBitrate))
|
|
249
254
|
// if (
|
|
250
255
|
// !info.httpDomainOnly &&
|
|
251
256
|
// !(
|
|
@@ -257,34 +262,38 @@ export function initInfo(info, t) {
|
|
|
257
262
|
// i.push('&https=1')
|
|
258
263
|
// }
|
|
259
264
|
if (i.length > 0) {
|
|
260
|
-
info.url += (-1 !== info.url.indexOf(
|
|
265
|
+
info.url += (-1 !== info.url.indexOf("?") ? "&" : "?") + "".concat(i.join("&"));
|
|
261
266
|
}
|
|
262
267
|
// if (this.isUseAV1(e)) e.url += '&codec=av1'
|
|
263
268
|
// else if (this.h265Proxy.isReady || this.h265Proxy.isH265MseCodec) this.h265Proxy.checkUrl(e)
|
|
264
|
-
info.url +=
|
|
269
|
+
info.url += "&codec=264";
|
|
265
270
|
// info.url += '&dMod='.concat(getDMod(info._dMod, info._sMod))
|
|
266
271
|
// this.url = info.url
|
|
267
272
|
// this.lineType = info.lineType
|
|
268
|
-
info.url = _getUrlParams(info.url, !1,
|
|
273
|
+
info.url = _getUrlParams(info.url, !1, "firstCdn");
|
|
269
274
|
function _getUrlParams(url, t, i) {
|
|
270
|
-
var s =
|
|
275
|
+
var s = "";
|
|
271
276
|
// s += t ? "pulltype=pcdn&" : "sdkPcdn=".concat(this._cdnCnt, "_").concat(this._getSdkPcdnReason(i), "&");
|
|
272
|
-
s += t ?
|
|
273
|
-
var platform =
|
|
277
|
+
s += t ? "pulltype=pcdn&" : "sdkPcdn=".concat(1, "_").concat(1, "&");
|
|
278
|
+
var platform = "web";
|
|
274
279
|
var r = PLATFORM_TYPE[platform] || PLATFORM_TYPE.web;
|
|
275
280
|
var n = platform === PLATFORM_TYPE_NAME.wap
|
|
276
|
-
?
|
|
277
|
-
:
|
|
281
|
+
? "uid=".concat(anticode.uid, "&uuid=").concat(anticode.uuid)
|
|
282
|
+
: "u=".concat(anticode.convertUid);
|
|
278
283
|
// s += ''.concat(n, '&t=').concat(r, '&sv=').concat(2401040319, '&sdk_sid=').concat(this._flvPlayer.info._sessionId)
|
|
279
|
-
s +=
|
|
280
|
-
|
|
284
|
+
s += ""
|
|
285
|
+
.concat(n, "&t=")
|
|
286
|
+
.concat(r, "&sv=")
|
|
287
|
+
.concat(2401040319, "&sdk_sid=")
|
|
288
|
+
.concat(info._sessionId);
|
|
289
|
+
-1 === url.indexOf("?") ? (url += "?") : (url += "&");
|
|
281
290
|
return url + s;
|
|
282
291
|
}
|
|
283
292
|
return info.url;
|
|
284
293
|
}
|
|
285
294
|
function getDMod(e, t) {
|
|
286
|
-
return (
|
|
295
|
+
return ("mseh" !== e && "wcs" !== e && "wasm" !== e && "mses" !== e && (e = "unknow"),
|
|
287
296
|
(t = Number(t)),
|
|
288
297
|
isNaN(t) && (t = 0),
|
|
289
|
-
|
|
298
|
+
"".concat(e, "-").concat(t));
|
|
290
299
|
}
|
package/lib/huya_api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function getRoomInfo(roomIdOrShortId: string): Promise<{
|
|
1
|
+
export declare function getRoomInfo(roomIdOrShortId: string, formatName?: "auto" | "flv" | "hls"): Promise<{
|
|
2
2
|
living: boolean;
|
|
3
3
|
id: number;
|
|
4
4
|
owner: string;
|
|
@@ -7,9 +7,13 @@ export declare function getRoomInfo(roomIdOrShortId: string): Promise<{
|
|
|
7
7
|
avatar: string;
|
|
8
8
|
cover: string;
|
|
9
9
|
streams: StreamProfile[];
|
|
10
|
-
sources:
|
|
10
|
+
sources: {
|
|
11
|
+
name: string;
|
|
12
|
+
url: string;
|
|
13
|
+
}[];
|
|
11
14
|
startTime: Date;
|
|
12
15
|
liveId: string;
|
|
16
|
+
gid: number;
|
|
13
17
|
}>;
|
|
14
18
|
export interface StreamProfile {
|
|
15
19
|
desc: string;
|
package/lib/huya_api.js
CHANGED
|
@@ -5,7 +5,7 @@ import { initInfo } from "./anticode.js";
|
|
|
5
5
|
const requester = axios.create({
|
|
6
6
|
timeout: 10e3,
|
|
7
7
|
});
|
|
8
|
-
export async function getRoomInfo(roomIdOrShortId) {
|
|
8
|
+
export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
9
9
|
const res = await requester.get(`https://www.huya.com/${roomIdOrShortId}`);
|
|
10
10
|
const html = res.data;
|
|
11
11
|
const match = html.match(/var hyPlayerConfig = ({[^]+?};)/);
|
|
@@ -20,15 +20,47 @@ export async function getRoomInfo(roomIdOrShortId) {
|
|
|
20
20
|
}));
|
|
21
21
|
const data = hyPlayerConfig.stream.data[0];
|
|
22
22
|
assert(data, `Unexpected resp, data is null`);
|
|
23
|
-
const sources =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
const sources = {
|
|
24
|
+
flv: [],
|
|
25
|
+
hls: [],
|
|
26
|
+
};
|
|
27
|
+
// const sources: SourceProfile[] = data.gameStreamInfoList.map((info) => ({
|
|
28
|
+
// name: info.sCdnType,
|
|
29
|
+
// url: initInfo({
|
|
30
|
+
// sFlvUrl: info.sFlvUrl,
|
|
31
|
+
// sStreamName: info.sStreamName,
|
|
32
|
+
// sFlvAntiCode: info.sFlvAntiCode,
|
|
33
|
+
// _sessionId: Date.now(),
|
|
34
|
+
// }),
|
|
35
|
+
// }));
|
|
36
|
+
for (const item of data?.gameStreamInfoList ?? []) {
|
|
37
|
+
if (item.sFlvAntiCode && item.sFlvAntiCode.length > 0) {
|
|
38
|
+
const url = initInfo({
|
|
39
|
+
baseUrl: item.sFlvUrl,
|
|
40
|
+
sStreamName: item.sStreamName,
|
|
41
|
+
antiCode: item.sFlvAntiCode,
|
|
42
|
+
suffix: item.sFlvUrlSuffix,
|
|
43
|
+
_sessionId: Date.now(),
|
|
44
|
+
});
|
|
45
|
+
sources.flv.push({
|
|
46
|
+
name: item.sCdnType,
|
|
47
|
+
url,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (item.sHlsAntiCode && item.sHlsAntiCode.length > 0) {
|
|
51
|
+
const url = initInfo({
|
|
52
|
+
baseUrl: item.sHlsUrl,
|
|
53
|
+
sStreamName: item.sStreamName,
|
|
54
|
+
antiCode: item.sHlsAntiCode,
|
|
55
|
+
suffix: item.sHlsUrlSuffix,
|
|
56
|
+
_sessionId: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
sources.hls.push({
|
|
59
|
+
name: item.sCdnType,
|
|
60
|
+
url,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
32
64
|
const startTime = new Date(data.gameLiveInfo?.startTime * 1000);
|
|
33
65
|
return {
|
|
34
66
|
living: vMultiStreamInfo.length > 0 && data.gameStreamInfoList.length > 0,
|
|
@@ -39,8 +71,9 @@ export async function getRoomInfo(roomIdOrShortId) {
|
|
|
39
71
|
avatar: data.gameLiveInfo.avatar180,
|
|
40
72
|
cover: data.gameLiveInfo.screenshot,
|
|
41
73
|
streams,
|
|
42
|
-
sources,
|
|
74
|
+
sources: formatName === "hls" ? sources.hls : sources.flv,
|
|
43
75
|
startTime,
|
|
44
76
|
liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
|
|
77
|
+
gid: data.gameLiveInfo.gid,
|
|
45
78
|
};
|
|
46
79
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare function getRoomInfo(roomIdOrShortId: string, formatName?: "auto" | "flv" | "hls"): Promise<{
|
|
2
|
+
living: boolean;
|
|
3
|
+
id: number;
|
|
4
|
+
owner: string;
|
|
5
|
+
title: string;
|
|
6
|
+
roomId: number;
|
|
7
|
+
avatar: string;
|
|
8
|
+
cover: string;
|
|
9
|
+
streams: StreamProfile[];
|
|
10
|
+
sources: {
|
|
11
|
+
name: string;
|
|
12
|
+
url: string;
|
|
13
|
+
}[];
|
|
14
|
+
startTime: Date;
|
|
15
|
+
liveId: string;
|
|
16
|
+
}>;
|
|
17
|
+
export interface StreamProfile {
|
|
18
|
+
desc: string;
|
|
19
|
+
bitRate: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SourceProfile {
|
|
22
|
+
name: string;
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// import { createHash, randomInt } from "node:crypto";
|
|
2
|
+
// import { URLSearchParams } from "node:url";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { utils } from "@bililive-tools/manager";
|
|
5
|
+
import { assert } from "./utils.js";
|
|
6
|
+
const requester = axios.create({
|
|
7
|
+
timeout: 10e3,
|
|
8
|
+
headers: {
|
|
9
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko)",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
13
|
+
const res = await requester.get(`https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=${roomIdOrShortId}`);
|
|
14
|
+
const html = res.data;
|
|
15
|
+
assert(html, `Unexpected resp, hyPlayerConfig is null`);
|
|
16
|
+
if (res.status !== 200) {
|
|
17
|
+
throw new Error(`Unexpected resp, status is ${res.status}`);
|
|
18
|
+
}
|
|
19
|
+
const profile = html.data;
|
|
20
|
+
const sources = {
|
|
21
|
+
flv: [],
|
|
22
|
+
hls: [],
|
|
23
|
+
};
|
|
24
|
+
// const uid = await getAnonymousUid();
|
|
25
|
+
for (const item of profile?.stream?.baseSteamInfoList ?? []) {
|
|
26
|
+
if (item.sFlvAntiCode && item.sFlvAntiCode.length > 0) {
|
|
27
|
+
// const { uid, urlQuery, seq_id, ws_time, ws_secret } = generateStreamParams({
|
|
28
|
+
// sFlvAntiCode: item.sFlvAntiCode,
|
|
29
|
+
// sStreamName: item.sStreamName,
|
|
30
|
+
// });
|
|
31
|
+
// console.log("uid", uid, urlQuery);
|
|
32
|
+
// const url = `${item.sFlvUrl}/${item.sStreamName}.${item.sFlvUrlSuffix}?wsSecret=${ws_secret}&wsTime=${ws_time}&seqid=${seq_id}&ctype=${urlQuery.get("ctype")}&ver=1&fs=${urlQuery.get("fs")}&t=${urlQuery.get("t")}&uid=${uid}`;
|
|
33
|
+
const url = `${item.sFlvUrl}/${item.sStreamName}.${item.sFlvUrlSuffix}?${item.sFlvAntiCode}`;
|
|
34
|
+
sources.flv.push({
|
|
35
|
+
name: item.sCdnType,
|
|
36
|
+
url,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (item.sHlsAntiCode && item.sHlsAntiCode.length > 0) {
|
|
40
|
+
const url = `${item.sHlsUrl}/${item.sStreamName}.${item.sHlsUrlSuffix}?${item.sHlsAntiCode}`;
|
|
41
|
+
sources.hls.push({
|
|
42
|
+
name: item.sCdnType,
|
|
43
|
+
url,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const streams = {
|
|
48
|
+
hls: profile.stream.hls.rateArray.map((info) => ({
|
|
49
|
+
desc: info.sDisplayName,
|
|
50
|
+
bitRate: info.iBitRate,
|
|
51
|
+
})),
|
|
52
|
+
flv: profile.stream.flv.rateArray.map((info) => ({
|
|
53
|
+
desc: info.sDisplayName,
|
|
54
|
+
bitRate: info.iBitRate,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
const startTime = new Date(profile.liveData?.startTime * 1000);
|
|
58
|
+
return {
|
|
59
|
+
living: profile.liveStatus === "ON",
|
|
60
|
+
id: profile.liveData.profileRoom,
|
|
61
|
+
owner: profile.liveData.nick,
|
|
62
|
+
title: profile.liveData.introduction,
|
|
63
|
+
roomId: profile.liveData.profileRoom,
|
|
64
|
+
avatar: profile.liveData.avatar180,
|
|
65
|
+
cover: profile.liveData.screenshot,
|
|
66
|
+
streams: formatName === "hls" ? streams.hls : streams.flv,
|
|
67
|
+
sources: formatName === "hls" ? sources.hls : sources.flv,
|
|
68
|
+
startTime,
|
|
69
|
+
liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// type CacheProfileData = CacheProfileOffData | CacheProfileReplayData | CacheProfileOnData;
|
|
73
|
+
const cdn = {
|
|
74
|
+
AL: "阿里",
|
|
75
|
+
AL13: "阿里13",
|
|
76
|
+
TX15: "腾讯15",
|
|
77
|
+
HW16: "华为16",
|
|
78
|
+
HYZJ: "不知道是啥",
|
|
79
|
+
TX: "腾讯",
|
|
80
|
+
HW: "华为",
|
|
81
|
+
HS: "火山",
|
|
82
|
+
WS: "网宿",
|
|
83
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { RecorderProvider } from "@bililive-tools/manager";
|
|
1
|
+
import type { RecorderProvider } from "@bililive-tools/manager";
|
|
2
2
|
export declare const provider: RecorderProvider<Record<string, unknown>>;
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import path from "path";
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
import mitt from "mitt";
|
|
3
|
-
import {
|
|
3
|
+
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, FFMPEGRecorder, } from "@bililive-tools/manager";
|
|
4
4
|
import { getInfo, getStream } from "./stream.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ensureFolderExist } from "./utils.js";
|
|
6
6
|
import HuYaDanMu from "huya-danma-listener";
|
|
7
7
|
function createRecorder(opts) {
|
|
8
8
|
// 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
|
|
@@ -18,6 +18,8 @@ function createRecorder(opts) {
|
|
|
18
18
|
qualityMaxRetry: opts.qualityRetry ?? 0,
|
|
19
19
|
qualityRetry: opts.qualityRetry ?? 0,
|
|
20
20
|
state: "idle",
|
|
21
|
+
api: opts.api ?? "auto",
|
|
22
|
+
formatName: opts.formatName ?? "auto",
|
|
21
23
|
getChannelURL() {
|
|
22
24
|
return `https://www.huya.com/${this.channelId}`;
|
|
23
25
|
},
|
|
@@ -62,12 +64,25 @@ const ffmpegOutputOptions = [
|
|
|
62
64
|
"-min_frag_duration",
|
|
63
65
|
"60000000",
|
|
64
66
|
];
|
|
65
|
-
const
|
|
67
|
+
const ffmpegInputOptions = [
|
|
68
|
+
"-reconnect",
|
|
69
|
+
"1",
|
|
70
|
+
"-reconnect_streamed",
|
|
71
|
+
"1",
|
|
72
|
+
"-reconnect_delay_max",
|
|
73
|
+
"10",
|
|
74
|
+
"-rw_timeout",
|
|
75
|
+
"15000000",
|
|
76
|
+
"-user_agent",
|
|
77
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0",
|
|
78
|
+
];
|
|
79
|
+
const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
|
|
66
80
|
if (this.recordHandle != null)
|
|
67
81
|
return this.recordHandle;
|
|
68
82
|
const liveInfo = await getInfo(this.channelId);
|
|
69
|
-
const { living, owner, title,
|
|
83
|
+
const { living, owner, title, liveId } = liveInfo;
|
|
70
84
|
this.liveInfo = liveInfo;
|
|
85
|
+
this.emit("LiveStart", { liveId });
|
|
71
86
|
if (liveInfo.liveId === banLiveId) {
|
|
72
87
|
this.tempStopIntervalCheck = true;
|
|
73
88
|
}
|
|
@@ -78,29 +93,63 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, }) {
|
|
|
78
93
|
return null;
|
|
79
94
|
if (!living)
|
|
80
95
|
return null;
|
|
81
|
-
this.state = "recording";
|
|
82
96
|
let res;
|
|
83
97
|
// TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
|
|
84
98
|
try {
|
|
99
|
+
let strictQuality = false;
|
|
100
|
+
if (this.qualityRetry > 0) {
|
|
101
|
+
strictQuality = true;
|
|
102
|
+
}
|
|
103
|
+
if (this.qualityMaxRetry < 0) {
|
|
104
|
+
strictQuality = true;
|
|
105
|
+
}
|
|
106
|
+
if (isManualStart) {
|
|
107
|
+
strictQuality = false;
|
|
108
|
+
}
|
|
85
109
|
res = await getStream({
|
|
86
110
|
channelId: this.channelId,
|
|
87
111
|
quality: this.quality,
|
|
88
112
|
streamPriorities: this.streamPriorities,
|
|
89
113
|
sourcePriorities: this.sourcePriorities,
|
|
114
|
+
api: this.api,
|
|
115
|
+
strictQuality,
|
|
116
|
+
formatName: this.formatName,
|
|
90
117
|
});
|
|
91
|
-
// console.log('live info', res)
|
|
92
118
|
}
|
|
93
119
|
catch (err) {
|
|
94
120
|
this.state = "idle";
|
|
95
121
|
throw err;
|
|
96
122
|
}
|
|
123
|
+
this.state = "recording";
|
|
97
124
|
const { currentStream: stream, sources: availableSources, streams: availableStreams } = res;
|
|
98
125
|
this.availableStreams = availableStreams.map((s) => s.desc);
|
|
99
126
|
this.availableSources = availableSources.map((s) => s.name);
|
|
100
127
|
this.usedStream = stream.name;
|
|
101
128
|
this.usedSource = stream.source;
|
|
102
|
-
|
|
103
|
-
const
|
|
129
|
+
let isEnded = false;
|
|
130
|
+
const onEnd = (...args) => {
|
|
131
|
+
if (isEnded)
|
|
132
|
+
return;
|
|
133
|
+
isEnded = true;
|
|
134
|
+
this.emit("DebugLog", {
|
|
135
|
+
type: "common",
|
|
136
|
+
text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
|
|
137
|
+
});
|
|
138
|
+
const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
139
|
+
this.recordHandle?.stop(reason);
|
|
140
|
+
};
|
|
141
|
+
const recorder = new FFMPEGRecorder({
|
|
142
|
+
url: stream.url,
|
|
143
|
+
outputOptions: ffmpegOutputOptions,
|
|
144
|
+
inputOptions: ffmpegInputOptions,
|
|
145
|
+
segment: this.segment ?? 0,
|
|
146
|
+
getSavePath: (opts) => getSavePath({ owner, title, startTime: opts.startTime }),
|
|
147
|
+
disableDanma: this.disableProvideCommentsWhenRecording,
|
|
148
|
+
}, onEnd);
|
|
149
|
+
const savePath = getSavePath({
|
|
150
|
+
owner,
|
|
151
|
+
title,
|
|
152
|
+
});
|
|
104
153
|
try {
|
|
105
154
|
ensureFolderExist(savePath);
|
|
106
155
|
}
|
|
@@ -108,25 +157,36 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, }) {
|
|
|
108
157
|
this.state = "idle";
|
|
109
158
|
throw err;
|
|
110
159
|
}
|
|
111
|
-
const streamManager = new StreamManager(this, getSavePath, owner, title, savePath, hasSegment);
|
|
112
160
|
const handleVideoCreated = async ({ filename }) => {
|
|
113
|
-
|
|
161
|
+
this.emit("videoFileCreated", { filename });
|
|
162
|
+
const extraDataController = recorder.getExtraDataController();
|
|
114
163
|
extraDataController?.setMeta({
|
|
115
164
|
room_id: this.channelId,
|
|
116
165
|
platform: provider?.id,
|
|
117
166
|
liveStartTimestamp: liveInfo.startTime?.getTime(),
|
|
167
|
+
recordStopTimestamp: Date.now(),
|
|
168
|
+
title: title,
|
|
169
|
+
user_name: owner,
|
|
118
170
|
});
|
|
119
|
-
if (this.saveCover) {
|
|
120
|
-
const coverPath = utils.replaceExtName(filename, ".jpg");
|
|
121
|
-
utils.downloadImage(cover, coverPath);
|
|
122
|
-
}
|
|
123
171
|
};
|
|
124
|
-
|
|
172
|
+
recorder.on("videoFileCreated", handleVideoCreated);
|
|
173
|
+
recorder.on("videoFileCompleted", ({ filename }) => {
|
|
174
|
+
this.emit("videoFileCompleted", { filename });
|
|
175
|
+
});
|
|
176
|
+
recorder.on("DebugLog", (data) => {
|
|
177
|
+
this.emit("DebugLog", data);
|
|
178
|
+
});
|
|
179
|
+
recorder.on("progress", (progress) => {
|
|
180
|
+
if (this.recordHandle) {
|
|
181
|
+
this.recordHandle.progress = progress;
|
|
182
|
+
}
|
|
183
|
+
this.emit("progress", progress);
|
|
184
|
+
});
|
|
125
185
|
let client = null;
|
|
126
186
|
if (!this.disableProvideCommentsWhenRecording) {
|
|
127
187
|
client = new HuYaDanMu(this.channelId);
|
|
128
188
|
client.on("message", (msg) => {
|
|
129
|
-
const extraDataController =
|
|
189
|
+
const extraDataController = recorder.getExtraDataController();
|
|
130
190
|
if (!extraDataController)
|
|
131
191
|
return;
|
|
132
192
|
switch (msg.type) {
|
|
@@ -170,71 +230,27 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, }) {
|
|
|
170
230
|
client.on("error", (e) => {
|
|
171
231
|
this.emit("DebugLog", { type: "common", text: String(e) });
|
|
172
232
|
});
|
|
173
|
-
|
|
174
|
-
await client.start();
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
this.state = "idle";
|
|
178
|
-
throw err;
|
|
179
|
-
}
|
|
233
|
+
client.start();
|
|
180
234
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (isEnded)
|
|
184
|
-
return;
|
|
185
|
-
isEnded = true;
|
|
186
|
-
this.emit("DebugLog", {
|
|
187
|
-
type: "common",
|
|
188
|
-
text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
|
|
189
|
-
});
|
|
190
|
-
const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
191
|
-
this.recordHandle?.stop(reason);
|
|
192
|
-
};
|
|
193
|
-
const isInvalidStream = utils.createInvalidStreamChecker();
|
|
194
|
-
const timeoutChecker = utils.createTimeoutChecker(() => onEnd("ffmpeg timeout"), 10e3);
|
|
195
|
-
const command = createFFMPEGBuilder()
|
|
196
|
-
.input(stream.url)
|
|
197
|
-
.addInputOptions("-user_agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0")
|
|
198
|
-
.outputOptions(ffmpegOutputOptions)
|
|
199
|
-
.output(streamManager.videoFilePath)
|
|
200
|
-
.on("start", () => {
|
|
201
|
-
streamManager.handleVideoStarted();
|
|
202
|
-
})
|
|
203
|
-
.on("error", onEnd)
|
|
204
|
-
.on("end", () => onEnd("finished"))
|
|
205
|
-
.on("stderr", async (stderrLine) => {
|
|
206
|
-
if (utils.isFfmpegStartSegment(stderrLine)) {
|
|
207
|
-
await streamManager.handleVideoStarted(stderrLine);
|
|
208
|
-
}
|
|
209
|
-
assertStringType(stderrLine);
|
|
210
|
-
this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
|
|
211
|
-
if (isInvalidStream(stderrLine)) {
|
|
212
|
-
onEnd("invalid stream");
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
.on("stderr", timeoutChecker.update);
|
|
216
|
-
if (hasSegment) {
|
|
217
|
-
command.outputOptions("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
|
|
218
|
-
}
|
|
219
|
-
const ffmpegArgs = command._getArguments();
|
|
220
|
-
command.run();
|
|
235
|
+
const ffmpegArgs = recorder.getArguments();
|
|
236
|
+
recorder.run();
|
|
221
237
|
const stop = utils.singleton(async (reason) => {
|
|
222
238
|
if (!this.recordHandle)
|
|
223
239
|
return;
|
|
224
240
|
this.state = "stopping-record";
|
|
225
|
-
// TODO: emit update event
|
|
226
|
-
timeoutChecker.stop();
|
|
227
|
-
// @ts-ignore
|
|
228
|
-
command.ffmpegProc?.stdin?.write("q");
|
|
229
|
-
// TODO: 这里可能会有内存泄露,因为事件还没清,之后再检查下看看。
|
|
230
241
|
client?.stop();
|
|
242
|
+
try {
|
|
243
|
+
await recorder.stop();
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
this.emit("DebugLog", {
|
|
247
|
+
type: "common",
|
|
248
|
+
text: `stop ffmpeg error: ${String(err)}`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
231
251
|
this.usedStream = undefined;
|
|
232
252
|
this.usedSource = undefined;
|
|
233
|
-
// TODO: other codes
|
|
234
|
-
// TODO: emit update event
|
|
235
|
-
await streamManager.handleVideoCompleted();
|
|
236
253
|
this.emit("RecordStop", { recordHandle: this.recordHandle, reason });
|
|
237
|
-
this.off("videoFileCreated", handleVideoCreated);
|
|
238
254
|
this.recordHandle = undefined;
|
|
239
255
|
this.liveInfo = undefined;
|
|
240
256
|
this.state = "idle";
|
package/lib/stream.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Recorder } from "@bililive-tools/manager";
|
|
2
|
-
import { SourceProfile, StreamProfile } from "./huya_api.js";
|
|
3
2
|
export declare function getInfo(channelId: string): Promise<{
|
|
4
3
|
living: boolean;
|
|
5
4
|
owner: string;
|
|
@@ -8,10 +7,10 @@ export declare function getInfo(channelId: string): Promise<{
|
|
|
8
7
|
avatar: string;
|
|
9
8
|
cover: string;
|
|
10
9
|
startTime: Date;
|
|
11
|
-
liveId
|
|
10
|
+
liveId: string;
|
|
12
11
|
}>;
|
|
13
|
-
export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities"> & {
|
|
14
|
-
|
|
12
|
+
export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities" | "api" | "formatName"> & {
|
|
13
|
+
strictQuality?: boolean;
|
|
15
14
|
}): Promise<{
|
|
16
15
|
currentStream: {
|
|
17
16
|
name: string;
|
|
@@ -25,8 +24,11 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" |
|
|
|
25
24
|
roomId: number;
|
|
26
25
|
avatar: string;
|
|
27
26
|
cover: string;
|
|
28
|
-
streams: StreamProfile[];
|
|
29
|
-
sources:
|
|
27
|
+
streams: import("./huya_mobile_api.js").StreamProfile[];
|
|
28
|
+
sources: {
|
|
29
|
+
name: string;
|
|
30
|
+
url: string;
|
|
31
|
+
}[];
|
|
30
32
|
startTime: Date;
|
|
31
33
|
liveId: string;
|
|
32
34
|
}>;
|
package/lib/stream.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Qualities } from "@bililive-tools/manager";
|
|
2
|
-
import { getRoomInfo } from "./huya_api.js";
|
|
3
|
-
import { getValuesFromArrayLikeFlexSpaceBetween } from "./utils.js";
|
|
4
1
|
import { sortBy } from "lodash-es";
|
|
2
|
+
import { HuYaQualities } from "@bililive-tools/manager";
|
|
3
|
+
import { getRoomInfo as getRoomInfoByWeb } from "./huya_api.js";
|
|
4
|
+
import { getRoomInfo as getRoomInfoByMobile } from "./huya_mobile_api.js";
|
|
5
|
+
import { assert } from "./utils.js";
|
|
5
6
|
export async function getInfo(channelId) {
|
|
6
|
-
const info = await
|
|
7
|
+
const info = await getRoomInfoByWeb(channelId);
|
|
7
8
|
return {
|
|
8
9
|
living: info.living,
|
|
9
10
|
owner: info.owner,
|
|
@@ -15,24 +16,43 @@ export async function getInfo(channelId) {
|
|
|
15
16
|
liveId: info.liveId,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
19
|
+
async function getRoomInfo(channelId, options) {
|
|
20
|
+
if (options.api == "auto") {
|
|
21
|
+
const info = await getRoomInfoByWeb(channelId, options.formatName);
|
|
22
|
+
if (info.gid == 1663) {
|
|
23
|
+
return getRoomInfoByMobile(channelId, options.formatName);
|
|
24
|
+
}
|
|
25
|
+
return info;
|
|
26
|
+
}
|
|
27
|
+
else if (options.api == "mp") {
|
|
28
|
+
return getRoomInfoByMobile(channelId, options.formatName);
|
|
29
|
+
}
|
|
30
|
+
else if (options.api == "web") {
|
|
31
|
+
return getRoomInfoByWeb(channelId, options.formatName);
|
|
32
|
+
}
|
|
33
|
+
assert(false, "Invalid api");
|
|
34
|
+
}
|
|
18
35
|
export async function getStream(opts) {
|
|
19
|
-
const info = await getRoomInfo(opts.channelId
|
|
36
|
+
const info = await getRoomInfo(opts.channelId, {
|
|
37
|
+
api: opts.api ?? "auto",
|
|
38
|
+
formatName: opts.formatName ?? "auto",
|
|
39
|
+
});
|
|
20
40
|
if (!info.living) {
|
|
21
41
|
throw new Error("It must be called getStream when living");
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (streamsWithPriority.length > 0) {
|
|
26
|
-
// 通过优先级来选择对应流
|
|
27
|
-
expectStream = streamsWithPriority[0];
|
|
43
|
+
if (info.streams.length === 0) {
|
|
44
|
+
throw new Error(`No stream found in huya ${opts.channelId} room`);
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expectStream =
|
|
46
|
+
const qn = (HuYaQualities.includes(opts.quality) ? opts.quality : 0);
|
|
47
|
+
let expectStream = info.streams.find((stream) => stream.bitRate === qn);
|
|
48
|
+
if (!expectStream && opts.strictQuality) {
|
|
49
|
+
throw new Error("Can not get expect quality because of strictQuality");
|
|
50
|
+
}
|
|
51
|
+
if (!expectStream) {
|
|
52
|
+
expectStream = info.streams[0];
|
|
53
|
+
}
|
|
54
|
+
if (!expectStream) {
|
|
55
|
+
throw new Error("未找到对应的流");
|
|
36
56
|
}
|
|
37
57
|
let expectSource = null;
|
|
38
58
|
const sourcesWithPriority = sortAndFilterSourcesByPriority(info.sources, opts.sourcePriorities);
|
|
@@ -40,7 +60,15 @@ export async function getStream(opts) {
|
|
|
40
60
|
expectSource = sourcesWithPriority[0];
|
|
41
61
|
}
|
|
42
62
|
else {
|
|
43
|
-
expectSource = info.sources
|
|
63
|
+
expectSource = info.sources.find((source) => source.name === "TX") ?? null;
|
|
64
|
+
if (!expectSource) {
|
|
65
|
+
expectSource = info.sources[0];
|
|
66
|
+
}
|
|
67
|
+
if (expectSource.name === "TX") {
|
|
68
|
+
expectSource.url = expectSource.url
|
|
69
|
+
.replace("&ctype=tars_mp", "&ctype=huya_webh5")
|
|
70
|
+
.replace("&fs=bhct", "&fs=bgct");
|
|
71
|
+
}
|
|
44
72
|
}
|
|
45
73
|
return {
|
|
46
74
|
...info,
|
|
@@ -51,21 +79,27 @@ export async function getStream(opts) {
|
|
|
51
79
|
},
|
|
52
80
|
};
|
|
53
81
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
function sortAndFilterStreamsByPriority(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
// /**
|
|
83
|
+
// * 按提供的流优先级去给流列表排序,并过滤掉不在优先级配置中的流
|
|
84
|
+
// */
|
|
85
|
+
// function sortAndFilterStreamsByPriority(
|
|
86
|
+
// streams: StreamProfile[],
|
|
87
|
+
// streamPriorities: Recorder["streamPriorities"],
|
|
88
|
+
// ): (StreamProfile & {
|
|
89
|
+
// priority: number;
|
|
90
|
+
// })[] {
|
|
91
|
+
// if (streamPriorities.length === 0) return [];
|
|
92
|
+
// return sortBy(
|
|
93
|
+
// // 分配优先级属性,数字越大优先级越高
|
|
94
|
+
// streams
|
|
95
|
+
// .map((stream) => ({
|
|
96
|
+
// ...stream,
|
|
97
|
+
// priority: streamPriorities.toReversed().indexOf(stream.desc),
|
|
98
|
+
// }))
|
|
99
|
+
// .filter(({ priority }) => priority !== -1),
|
|
100
|
+
// "priority",
|
|
101
|
+
// );
|
|
102
|
+
// }
|
|
69
103
|
/**
|
|
70
104
|
* 按提供的源优先级去给源列表排序,并过滤掉不在优先级配置中的源
|
|
71
105
|
*/
|
package/lib/utils.d.ts
CHANGED
|
@@ -20,3 +20,4 @@ export declare function assert(assertion: unknown, msg?: string): asserts assert
|
|
|
20
20
|
export declare function assertStringType(data: unknown, msg?: string): asserts data is string;
|
|
21
21
|
export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
|
|
22
22
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
23
|
+
export declare function createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean;
|
package/lib/utils.js
CHANGED
|
@@ -59,3 +59,28 @@ 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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/huya-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "bililive-tools huya recorder implemention",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,13 +37,10 @@
|
|
|
37
37
|
"mitt": "^3.0.1",
|
|
38
38
|
"lodash-es": "^4.17.21",
|
|
39
39
|
"axios": "^1.7.8",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"@bililive-tools/manager": "^1.1.0",
|
|
41
|
+
"huya-danma-listener": "0.1.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"@bililive-tools/manager": "*"
|
|
46
|
-
},
|
|
47
44
|
"scripts": {
|
|
48
45
|
"build": "tsc",
|
|
49
46
|
"watch": "tsc -w"
|