@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 CHANGED
@@ -38,13 +38,14 @@ interface Options {
38
38
  qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
39
39
  streamPriorities: []; // 废弃
40
40
  sourcePriorities: []; // 按提供的源优先级去给CDN列表排序,并过滤掉不在优先级配置中的源,在未匹配到的情况下会优先使用TX的CDN,具体参数见 CDN 参数
41
- formatName?: "auto" | "flv" | "hls"; // 支持 flv,hls参数,默认使用flv,具体见文档
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
- export declare function getRoomInfo(roomIdOrShortId: string, formatName?: "auto" | "flv" | "hls"): Promise<{
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, formatName = "auto") {
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: formatName === "hls" ? sources.hls : sources.flv,
78
+ sources: formatSources.sources,
75
79
  startTime,
76
80
  liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
77
81
  gid: data.gameLiveInfo.gid,
@@ -1,4 +1,5 @@
1
- export declare function getRoomInfo(roomIdOrShortId: string, formatName?: "auto" | "flv" | "hls"): Promise<{
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
- }
@@ -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, formatName = "auto") {
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: formatName === "hls" ? sources.hls : sources.flv,
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
- formatName: opts.formatName ?? "auto",
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
- "60000000",
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, liveId } = liveInfo;
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
- formatName: this.formatName,
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
- }, onEnd);
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" | "formatName"> & {
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: import("./huya_mobile_api.js").StreamProfile[];
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.formatName);
21
+ const info = await getRoomInfoByWeb(channelId, options.formatPriorities);
22
22
  if (info.gid == 1663) {
23
- return getRoomInfoByMobile(channelId, options.formatName);
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.formatName);
28
+ return getRoomInfoByMobile(channelId, options.formatPriorities);
29
29
  }
30
30
  else if (options.api == "web") {
31
- return getRoomInfoByWeb(channelId, options.formatName);
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
- formatName: opts.formatName ?? "auto",
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.1.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.1.0",
41
- "huya-danma-listener": "0.1.0"
40
+ "@bililive-tools/manager": "^1.3.0",
41
+ "huya-danma-listener": "0.1.1"
42
42
  },
43
43
  "devDependencies": {},
44
44
  "scripts": {