@bililive-tools/huya-recorder 1.2.0 → 1.3.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 +1 -1
- package/lib/huya_api.d.ts +3 -13
- package/lib/huya_api.js +4 -3
- package/lib/huya_mobile_api.d.ts +3 -13
- package/lib/huya_mobile_api.js +8 -4
- package/lib/index.js +33 -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,7 +38,7 @@ 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; // 禁用弹幕录制
|
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,7 @@ 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);
|
|
65
66
|
return {
|
|
66
67
|
living: vMultiStreamInfo.length > 0 && data.gameStreamInfoList.length > 0,
|
|
67
68
|
id: data.gameLiveInfo.profileRoom,
|
|
@@ -71,7 +72,7 @@ export async function getRoomInfo(roomIdOrShortId, formatName = "auto") {
|
|
|
71
72
|
avatar: data.gameLiveInfo.avatar180,
|
|
72
73
|
cover: data.gameLiveInfo.screenshot,
|
|
73
74
|
streams,
|
|
74
|
-
sources:
|
|
75
|
+
sources: formatSources?.sources ?? [],
|
|
75
76
|
startTime,
|
|
76
77
|
liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
|
|
77
78
|
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,14 +73,12 @@ 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
83
|
if (liveInfo.liveId === banLiveId) {
|
|
86
84
|
this.tempStopIntervalCheck = true;
|
|
@@ -92,7 +90,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
92
90
|
return null;
|
|
93
91
|
if (!living)
|
|
94
92
|
return null;
|
|
95
|
-
this.emit("LiveStart", { liveId });
|
|
96
93
|
let res;
|
|
97
94
|
// TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
|
|
98
95
|
try {
|
|
@@ -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,10 +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",
|
|
149
|
-
}, onEnd)
|
|
151
|
+
}, onEnd, async () => {
|
|
152
|
+
const info = await getInfo(this.channelId);
|
|
153
|
+
return info;
|
|
154
|
+
});
|
|
150
155
|
const savePath = getSavePath({
|
|
151
156
|
owner,
|
|
152
157
|
title,
|
|
@@ -158,8 +163,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
158
163
|
this.state = "idle";
|
|
159
164
|
throw err;
|
|
160
165
|
}
|
|
161
|
-
const handleVideoCreated = async ({ filename }) => {
|
|
162
|
-
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
|
+
}
|
|
163
174
|
const extraDataController = recorder.getExtraDataController();
|
|
164
175
|
extraDataController?.setMeta({
|
|
165
176
|
room_id: this.channelId,
|
|
@@ -235,6 +246,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
235
246
|
}
|
|
236
247
|
const ffmpegArgs = recorder.getArguments();
|
|
237
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
|
+
});
|
|
238
259
|
const stop = utils.singleton(async (reason) => {
|
|
239
260
|
if (!this.recordHandle)
|
|
240
261
|
return;
|
|
@@ -264,6 +285,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
264
285
|
ffmpegArgs,
|
|
265
286
|
savePath: savePath,
|
|
266
287
|
stop,
|
|
288
|
+
cut,
|
|
267
289
|
};
|
|
268
290
|
this.emit("RecordStart", this.recordHandle);
|
|
269
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.1",
|
|
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
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"huya-danma-listener": "0.1.1",
|
|
41
|
+
"@bililive-tools/manager": "^1.3.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {},
|
|
44
44
|
"scripts": {
|