@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 CHANGED
@@ -38,7 +38,7 @@ 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; // 禁用弹幕录制
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,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: formatName === "hls" ? sources.hls : sources.flv,
75
+ sources: formatSources?.sources ?? [],
75
76
  startTime,
76
77
  liveId: utils.md5(`${roomIdOrShortId}-${startTime?.getTime()}`),
77
78
  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,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, liveId } = liveInfo;
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
- 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,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" | "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.2.0",
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
- "@bililive-tools/manager": "^1.2.0",
41
- "huya-danma-listener": "0.1.1"
40
+ "huya-danma-listener": "0.1.1",
41
+ "@bililive-tools/manager": "^1.3.0"
42
42
  },
43
43
  "devDependencies": {},
44
44
  "scripts": {