@bililive-tools/huya-recorder 1.1.1 → 1.3.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 +2 -1
- package/lib/huya_api.d.ts +3 -13
- package/lib/huya_api.js +7 -3
- package/lib/huya_mobile_api.d.ts +3 -13
- package/lib/huya_mobile_api.js +8 -4
- package/lib/index.js +34 -11
- package/lib/stream.d.ts +4 -6
- package/lib/stream.js +5 -5
- package/lib/types.d.ts +18 -0
- package/lib/types.js +1 -0
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +15 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -38,13 +38,14 @@ interface Options {
|
|
|
38
38
|
qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
|
|
39
39
|
streamPriorities: []; // 废弃
|
|
40
40
|
sourcePriorities: []; // 按提供的源优先级去给CDN列表排序,并过滤掉不在优先级配置中的源,在未匹配到的情况下会优先使用TX的CDN,具体参数见 CDN 参数
|
|
41
|
-
|
|
41
|
+
formatPriorities?: string[]; // 支持,`flv`和`hls` 参数,默认为['flv','hls']
|
|
42
42
|
disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
|
|
43
43
|
segment?: number; // 分段参数,单位分钟
|
|
44
44
|
disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
|
|
45
45
|
saveGiftDanma?: boolean; // 保存礼物弹幕
|
|
46
46
|
saveCover?: boolean; // 保存封面
|
|
47
47
|
api?: "auto" | "mp" | "web"; // 默认为auto,在星秀区使用mp接口,其他使用web接口,你也可以强制指定
|
|
48
|
+
videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
|
|
48
49
|
}
|
|
49
50
|
```
|
|
50
51
|
|
package/lib/huya_api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { StreamProfile } from "./types.js";
|
|
2
|
+
export declare function getRoomInfo(roomIdOrShortId: string, formatPriorities?: Array<"flv" | "hls">): Promise<{
|
|
2
3
|
living: boolean;
|
|
3
4
|
id: number;
|
|
4
5
|
owner: string;
|
|
@@ -7,19 +8,8 @@ export declare function getRoomInfo(roomIdOrShortId: string, formatName?: "auto"
|
|
|
7
8
|
avatar: string;
|
|
8
9
|
cover: string;
|
|
9
10
|
streams: StreamProfile[];
|
|
10
|
-
sources:
|
|
11
|
-
name: string;
|
|
12
|
-
url: string;
|
|
13
|
-
}[];
|
|
11
|
+
sources: import("./types.js").SourceProfile[];
|
|
14
12
|
startTime: Date;
|
|
15
13
|
liveId: string;
|
|
16
14
|
gid: number;
|
|
17
15
|
}>;
|
|
18
|
-
export interface StreamProfile {
|
|
19
|
-
desc: string;
|
|
20
|
-
bitRate: number;
|
|
21
|
-
}
|
|
22
|
-
export interface SourceProfile {
|
|
23
|
-
name: string;
|
|
24
|
-
url: string;
|
|
25
|
-
}
|
package/lib/huya_api.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { utils } from "@bililive-tools/manager";
|
|
3
|
-
import { assert } from "./utils.js";
|
|
3
|
+
import { assert, getFormatSources } from "./utils.js";
|
|
4
4
|
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, formatPriorities = ["flv", "hls"]) {
|
|
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 = ({[^]+?};)/);
|
|
@@ -62,6 +62,10 @@ export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
const startTime = new Date(data.gameLiveInfo?.startTime * 1000);
|
|
65
|
+
const formatSources = getFormatSources(sources, formatPriorities);
|
|
66
|
+
if (!formatSources) {
|
|
67
|
+
throw new Error("No format sources found");
|
|
68
|
+
}
|
|
65
69
|
return {
|
|
66
70
|
living: vMultiStreamInfo.length > 0 && data.gameStreamInfoList.length > 0,
|
|
67
71
|
id: data.gameLiveInfo.profileRoom,
|
|
@@ -71,7 +75,7 @@ export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
|
71
75
|
avatar: data.gameLiveInfo.avatar180,
|
|
72
76
|
cover: data.gameLiveInfo.screenshot,
|
|
73
77
|
streams,
|
|
74
|
-
sources:
|
|
78
|
+
sources: formatSources.sources,
|
|
75
79
|
startTime,
|
|
76
80
|
liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
|
|
77
81
|
gid: data.gameLiveInfo.gid,
|
package/lib/huya_mobile_api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { StreamProfile } from "./types.js";
|
|
2
|
+
export declare function getRoomInfo(roomIdOrShortId: string, formatPriorities?: Array<"flv" | "hls">): Promise<{
|
|
2
3
|
living: boolean;
|
|
3
4
|
id: number;
|
|
4
5
|
owner: string;
|
|
@@ -7,18 +8,7 @@ export declare function getRoomInfo(roomIdOrShortId: string, formatName?: "auto"
|
|
|
7
8
|
avatar: string;
|
|
8
9
|
cover: string;
|
|
9
10
|
streams: StreamProfile[];
|
|
10
|
-
sources:
|
|
11
|
-
name: string;
|
|
12
|
-
url: string;
|
|
13
|
-
}[];
|
|
11
|
+
sources: import("./types.js").SourceProfile[];
|
|
14
12
|
startTime: Date;
|
|
15
13
|
liveId: string;
|
|
16
14
|
}>;
|
|
17
|
-
export interface StreamProfile {
|
|
18
|
-
desc: string;
|
|
19
|
-
bitRate: number;
|
|
20
|
-
}
|
|
21
|
-
export interface SourceProfile {
|
|
22
|
-
name: string;
|
|
23
|
-
url: string;
|
|
24
|
-
}
|
package/lib/huya_mobile_api.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// import { URLSearchParams } from "node:url";
|
|
3
3
|
import axios from "axios";
|
|
4
4
|
import { utils } from "@bililive-tools/manager";
|
|
5
|
-
import { assert } from "./utils.js";
|
|
5
|
+
import { assert, getFormatSources } from "./utils.js";
|
|
6
6
|
const requester = axios.create({
|
|
7
7
|
timeout: 10e3,
|
|
8
8
|
headers: {
|
|
9
9
|
"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko)",
|
|
10
10
|
},
|
|
11
11
|
});
|
|
12
|
-
export async function getRoomInfo(roomIdOrShortId,
|
|
12
|
+
export async function getRoomInfo(roomIdOrShortId, formatPriorities = ["flv", "hls"]) {
|
|
13
13
|
const res = await requester.get(`https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=${roomIdOrShortId}`);
|
|
14
14
|
const html = res.data;
|
|
15
15
|
assert(html, `Unexpected resp, hyPlayerConfig is null`);
|
|
@@ -55,6 +55,10 @@ export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
|
55
55
|
})),
|
|
56
56
|
};
|
|
57
57
|
const startTime = new Date(profile.liveData?.startTime * 1000);
|
|
58
|
+
const formatSources = getFormatSources(sources, formatPriorities);
|
|
59
|
+
if (!formatSources) {
|
|
60
|
+
throw new Error("No format sources found");
|
|
61
|
+
}
|
|
58
62
|
return {
|
|
59
63
|
living: profile.liveStatus === "ON",
|
|
60
64
|
id: profile.liveData.profileRoom,
|
|
@@ -63,8 +67,8 @@ export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
|
63
67
|
roomId: profile.liveData.profileRoom,
|
|
64
68
|
avatar: profile.liveData.avatar180,
|
|
65
69
|
cover: profile.liveData.screenshot,
|
|
66
|
-
streams: formatName === "hls" ? streams.hls : streams.flv,
|
|
67
|
-
sources:
|
|
70
|
+
streams: formatSources.formatName === "hls" ? streams.hls : streams.flv,
|
|
71
|
+
sources: formatSources.sources,
|
|
68
72
|
startTime,
|
|
69
73
|
liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
|
|
70
74
|
};
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,7 @@ function createRecorder(opts) {
|
|
|
19
19
|
qualityRetry: opts.qualityRetry ?? 0,
|
|
20
20
|
state: "idle",
|
|
21
21
|
api: opts.api ?? "auto",
|
|
22
|
-
|
|
22
|
+
formatPriorities: opts.formatPriorities ?? ["flv", "hls"],
|
|
23
23
|
getChannelURL() {
|
|
24
24
|
return `https://www.huya.com/${this.channelId}`;
|
|
25
25
|
},
|
|
@@ -62,7 +62,7 @@ const ffmpegOutputOptions = [
|
|
|
62
62
|
"-movflags",
|
|
63
63
|
"faststart+frag_keyframe+empty_moov",
|
|
64
64
|
"-min_frag_duration",
|
|
65
|
-
"
|
|
65
|
+
"10000000",
|
|
66
66
|
];
|
|
67
67
|
const ffmpegInputOptions = [
|
|
68
68
|
"-reconnect",
|
|
@@ -73,16 +73,13 @@ const ffmpegInputOptions = [
|
|
|
73
73
|
"10",
|
|
74
74
|
"-rw_timeout",
|
|
75
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
76
|
];
|
|
79
77
|
const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
|
|
80
78
|
if (this.recordHandle != null)
|
|
81
79
|
return this.recordHandle;
|
|
82
80
|
const liveInfo = await getInfo(this.channelId);
|
|
83
|
-
const { living, owner, title
|
|
81
|
+
const { living, owner, title } = liveInfo;
|
|
84
82
|
this.liveInfo = liveInfo;
|
|
85
|
-
this.emit("LiveStart", { liveId });
|
|
86
83
|
if (liveInfo.liveId === banLiveId) {
|
|
87
84
|
this.tempStopIntervalCheck = true;
|
|
88
85
|
}
|
|
@@ -113,7 +110,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
113
110
|
sourcePriorities: this.sourcePriorities,
|
|
114
111
|
api: this.api,
|
|
115
112
|
strictQuality,
|
|
116
|
-
|
|
113
|
+
formatPriorities: this.formatPriorities,
|
|
117
114
|
});
|
|
118
115
|
}
|
|
119
116
|
catch (err) {
|
|
@@ -127,7 +124,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
127
124
|
this.usedStream = stream.name;
|
|
128
125
|
this.usedSource = stream.source;
|
|
129
126
|
let isEnded = false;
|
|
127
|
+
let isCutting = false;
|
|
130
128
|
const onEnd = (...args) => {
|
|
129
|
+
if (isCutting) {
|
|
130
|
+
isCutting = false;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
131
133
|
if (isEnded)
|
|
132
134
|
return;
|
|
133
135
|
isEnded = true;
|
|
@@ -143,9 +145,13 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
143
145
|
outputOptions: ffmpegOutputOptions,
|
|
144
146
|
inputOptions: ffmpegInputOptions,
|
|
145
147
|
segment: this.segment ?? 0,
|
|
146
|
-
getSavePath: (opts) => getSavePath({ owner, title, startTime: opts.startTime }),
|
|
148
|
+
getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
|
|
147
149
|
disableDanma: this.disableProvideCommentsWhenRecording,
|
|
148
|
-
|
|
150
|
+
videoFormat: this.videoFormat ?? "auto",
|
|
151
|
+
}, onEnd, async () => {
|
|
152
|
+
const info = await getInfo(this.channelId);
|
|
153
|
+
return info;
|
|
154
|
+
});
|
|
149
155
|
const savePath = getSavePath({
|
|
150
156
|
owner,
|
|
151
157
|
title,
|
|
@@ -157,8 +163,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
157
163
|
this.state = "idle";
|
|
158
164
|
throw err;
|
|
159
165
|
}
|
|
160
|
-
const handleVideoCreated = async ({ filename }) => {
|
|
161
|
-
this.emit("videoFileCreated", { filename });
|
|
166
|
+
const handleVideoCreated = async ({ filename, title, cover }) => {
|
|
167
|
+
this.emit("videoFileCreated", { filename, cover });
|
|
168
|
+
if (title && this?.liveInfo) {
|
|
169
|
+
this.liveInfo.title = title;
|
|
170
|
+
}
|
|
171
|
+
if (cover && this?.liveInfo) {
|
|
172
|
+
this.liveInfo.cover = cover;
|
|
173
|
+
}
|
|
162
174
|
const extraDataController = recorder.getExtraDataController();
|
|
163
175
|
extraDataController?.setMeta({
|
|
164
176
|
room_id: this.channelId,
|
|
@@ -234,6 +246,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
234
246
|
}
|
|
235
247
|
const ffmpegArgs = recorder.getArguments();
|
|
236
248
|
recorder.run();
|
|
249
|
+
const cut = utils.singleton(async () => {
|
|
250
|
+
if (!this.recordHandle)
|
|
251
|
+
return;
|
|
252
|
+
if (isCutting)
|
|
253
|
+
return;
|
|
254
|
+
isCutting = true;
|
|
255
|
+
await recorder.stop();
|
|
256
|
+
recorder.createCommand();
|
|
257
|
+
recorder.run();
|
|
258
|
+
});
|
|
237
259
|
const stop = utils.singleton(async (reason) => {
|
|
238
260
|
if (!this.recordHandle)
|
|
239
261
|
return;
|
|
@@ -263,6 +285,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
263
285
|
ffmpegArgs,
|
|
264
286
|
savePath: savePath,
|
|
265
287
|
stop,
|
|
288
|
+
cut,
|
|
266
289
|
};
|
|
267
290
|
this.emit("RecordStart", this.recordHandle);
|
|
268
291
|
return this.recordHandle;
|
package/lib/stream.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Recorder } from "@bililive-tools/manager";
|
|
2
|
+
import type { SourceProfile, StreamProfile } from "./types.js";
|
|
2
3
|
export declare function getInfo(channelId: string): Promise<{
|
|
3
4
|
living: boolean;
|
|
4
5
|
owner: string;
|
|
@@ -9,7 +10,7 @@ export declare function getInfo(channelId: string): Promise<{
|
|
|
9
10
|
startTime: Date;
|
|
10
11
|
liveId: string;
|
|
11
12
|
}>;
|
|
12
|
-
export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities" | "api" | "
|
|
13
|
+
export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities" | "api" | "formatPriorities"> & {
|
|
13
14
|
strictQuality?: boolean;
|
|
14
15
|
}): Promise<{
|
|
15
16
|
currentStream: {
|
|
@@ -24,11 +25,8 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" |
|
|
|
24
25
|
roomId: number;
|
|
25
26
|
avatar: string;
|
|
26
27
|
cover: string;
|
|
27
|
-
streams:
|
|
28
|
-
sources:
|
|
29
|
-
name: string;
|
|
30
|
-
url: string;
|
|
31
|
-
}[];
|
|
28
|
+
streams: StreamProfile[];
|
|
29
|
+
sources: SourceProfile[];
|
|
32
30
|
startTime: Date;
|
|
33
31
|
liveId: string;
|
|
34
32
|
}>;
|
package/lib/stream.js
CHANGED
|
@@ -18,24 +18,24 @@ export async function getInfo(channelId) {
|
|
|
18
18
|
}
|
|
19
19
|
async function getRoomInfo(channelId, options) {
|
|
20
20
|
if (options.api == "auto") {
|
|
21
|
-
const info = await getRoomInfoByWeb(channelId, options.
|
|
21
|
+
const info = await getRoomInfoByWeb(channelId, options.formatPriorities);
|
|
22
22
|
if (info.gid == 1663) {
|
|
23
|
-
return getRoomInfoByMobile(channelId, options.
|
|
23
|
+
return getRoomInfoByMobile(channelId, options.formatPriorities);
|
|
24
24
|
}
|
|
25
25
|
return info;
|
|
26
26
|
}
|
|
27
27
|
else if (options.api == "mp") {
|
|
28
|
-
return getRoomInfoByMobile(channelId, options.
|
|
28
|
+
return getRoomInfoByMobile(channelId, options.formatPriorities);
|
|
29
29
|
}
|
|
30
30
|
else if (options.api == "web") {
|
|
31
|
-
return getRoomInfoByWeb(channelId, options.
|
|
31
|
+
return getRoomInfoByWeb(channelId, options.formatPriorities);
|
|
32
32
|
}
|
|
33
33
|
assert(false, "Invalid api");
|
|
34
34
|
}
|
|
35
35
|
export async function getStream(opts) {
|
|
36
36
|
const info = await getRoomInfo(opts.channelId, {
|
|
37
37
|
api: opts.api ?? "auto",
|
|
38
|
-
|
|
38
|
+
formatPriorities: opts.formatPriorities ?? ["flv", "hls"],
|
|
39
39
|
});
|
|
40
40
|
if (!info.living) {
|
|
41
41
|
throw new Error("It must be called getStream when living");
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StreamResult {
|
|
2
|
+
flv: {
|
|
3
|
+
name: string;
|
|
4
|
+
url: string;
|
|
5
|
+
}[];
|
|
6
|
+
hls: {
|
|
7
|
+
name: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export interface StreamProfile {
|
|
12
|
+
desc: string;
|
|
13
|
+
bitRate: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SourceProfile {
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StreamResult, SourceProfile } from "./types.js";
|
|
1
2
|
/**
|
|
2
3
|
* 从数组中按照特定算法提取一些值(允许同个索引重复提取)。
|
|
3
4
|
* 算法的行为类似 flex 的 space-between。
|
|
@@ -21,3 +22,7 @@ export declare function assertStringType(data: unknown, msg?: string): asserts d
|
|
|
21
22
|
export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
|
|
22
23
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
23
24
|
export declare function createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean;
|
|
25
|
+
export declare function getFormatSources(sources: StreamResult, formatPriorities?: Array<"flv" | "hls">): {
|
|
26
|
+
sources: SourceProfile[];
|
|
27
|
+
formatName: "flv" | "hls";
|
|
28
|
+
} | null;
|
package/lib/utils.js
CHANGED
|
@@ -84,3 +84,18 @@ export function createInvalidStreamChecker() {
|
|
|
84
84
|
return false;
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
// 根据formatPriorities获取最终的sources
|
|
88
|
+
// 如果formatPriorities为空或者undefined,则使用['flv','hls']
|
|
89
|
+
// 如果有参数,按照顺序进行匹配,如果匹配的值不存在或者为空,则使用下一个参数,最后返回的是流数组
|
|
90
|
+
export function getFormatSources(sources, formatPriorities = ["flv", "hls"]) {
|
|
91
|
+
for (const format of formatPriorities) {
|
|
92
|
+
if (sources[format] && sources[format].length > 0) {
|
|
93
|
+
return {
|
|
94
|
+
sources: sources[format],
|
|
95
|
+
formatName: format,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 如果没有匹配到任何格式,使用默认的flv格式
|
|
100
|
+
return null;
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/huya-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "bililive-tools huya recorder implemention",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"mitt": "^3.0.1",
|
|
38
38
|
"lodash-es": "^4.17.21",
|
|
39
39
|
"axios": "^1.7.8",
|
|
40
|
-
"@bililive-tools/manager": "^1.
|
|
41
|
-
"huya-danma-listener": "0.1.
|
|
40
|
+
"@bililive-tools/manager": "^1.3.0",
|
|
41
|
+
"huya-danma-listener": "0.1.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {},
|
|
44
44
|
"scripts": {
|