@bililive-tools/manager 1.5.0 → 1.6.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
@@ -72,14 +72,16 @@ manager.startCheckLoop();
72
72
 
73
73
  手动开启录制
74
74
 
75
- ### setFFMPEGPath
76
-
77
- 设置ffmpeg可执行路径
75
+ ### setFFMPEGPath & setMesioPath
78
76
 
79
77
  ```ts
80
- import { setFFMPEGPath } from "@bililive-tools/manager";
78
+ import { setFFMPEGPath, setMesioPath } from "@bililive-tools/manager";
81
79
 
80
+ // 设置ffmpeg可执行路径
82
81
  setFFMPEGPath("ffmpeg.exe");
82
+
83
+ // 设置mesio可执行文件路径
84
+ setMesioPath("mesio.exe");
83
85
  ```
84
86
 
85
87
  ## savePathRule 占位符参数
package/lib/cache.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare class Cache {
2
+ private static instance;
3
+ private data;
4
+ private constructor();
5
+ static getInstance(): Cache;
6
+ set(key: string, value: any): void;
7
+ get(key: string): any;
8
+ has(key: string): boolean;
9
+ delete(key: string): boolean;
10
+ clear(): void;
11
+ get size(): number;
12
+ keys(): IterableIterator<string>;
13
+ values(): IterableIterator<any>;
14
+ entries(): IterableIterator<[string, any]>;
15
+ forEach(callbackfn: (value: any, key: string, map: Map<string, any>) => void, thisArg?: any): void;
16
+ [Symbol.iterator](): IterableIterator<[string, any]>;
17
+ }
package/lib/cache.js ADDED
@@ -0,0 +1,47 @@
1
+ export class Cache {
2
+ static instance;
3
+ data;
4
+ constructor() {
5
+ this.data = new Map();
6
+ }
7
+ static getInstance() {
8
+ if (!Cache.instance) {
9
+ Cache.instance = new Cache();
10
+ }
11
+ return Cache.instance;
12
+ }
13
+ set(key, value) {
14
+ this.data.set(key, value);
15
+ }
16
+ get(key) {
17
+ return this.data.get(key);
18
+ }
19
+ has(key) {
20
+ return this.data.has(key);
21
+ }
22
+ delete(key) {
23
+ return this.data.delete(key);
24
+ }
25
+ clear() {
26
+ this.data.clear();
27
+ }
28
+ get size() {
29
+ return this.data.size;
30
+ }
31
+ keys() {
32
+ return this.data.keys();
33
+ }
34
+ values() {
35
+ return this.data.values();
36
+ }
37
+ entries() {
38
+ return this.data.entries();
39
+ }
40
+ forEach(callbackfn, thisArg) {
41
+ this.data.forEach(callbackfn, thisArg);
42
+ }
43
+ // 实现 Symbol.iterator 接口,支持 for...of 循环
44
+ [Symbol.iterator]() {
45
+ return this.data[Symbol.iterator]();
46
+ }
47
+ }
package/lib/common.d.ts CHANGED
@@ -3,7 +3,7 @@ export type ChannelId = string;
3
3
  export declare const Qualities: readonly ["lowest", "low", "medium", "high", "highest"];
4
4
  export declare const BiliQualities: readonly [30000, 20000, 10000, 400, 250, 150, 80];
5
5
  export declare const DouyuQualities: readonly [0, 2, 3, 4, 8];
6
- export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
6
+ export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500, -1];
7
7
  export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
8
8
  export type Quality = (typeof Qualities)[number] | (typeof BiliQualities)[number] | (typeof DouyuQualities)[number] | (typeof HuYaQualities)[number] | (typeof DouYinQualities)[number];
9
9
  export interface MessageSender<E extends AnyObject = UnknownObject> {
package/lib/common.js CHANGED
@@ -2,5 +2,7 @@ export const Qualities = ["lowest", "low", "medium", "high", "highest"];
2
2
  export const BiliQualities = [30000, 20000, 10000, 400, 250, 150, 80];
3
3
  export const DouyuQualities = [0, 2, 3, 4, 8];
4
4
  // 14100: 2K HDR;14000:2K;4200:HDR(10M);0:原画;8000:蓝光8M;4000:蓝光4M;2000:超清;500:流畅
5
- export const HuYaQualities = [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
5
+ export const HuYaQualities = [
6
+ 0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500, -1,
7
+ ];
6
8
  export const DouYinQualities = ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
package/lib/index.d.ts CHANGED
@@ -7,7 +7,8 @@ export * from "./common.js";
7
7
  export * from "./recorder.js";
8
8
  export * from "./manager.js";
9
9
  export * from "./record_extra_data_controller.js";
10
- export * from "./FFMPEGRecorder.js";
10
+ export * from "./recorder/FFMPEGRecorder.js";
11
+ export { createBaseRecorder } from "./recorder/index.js";
11
12
  export { utils };
12
13
  /**
13
14
  * 提供一些 utils
@@ -17,5 +18,7 @@ export declare function defaultToJSON<E extends AnyObject>(provider: RecorderPro
17
18
  export declare function genRecorderUUID(): Recorder["id"];
18
19
  export declare function genRecordUUID(): RecordHandle["id"];
19
20
  export declare function setFFMPEGPath(newPath: string): void;
20
- export declare const createFFMPEGBuilder: (input?: string | import("stream").Readable | undefined, options?: ffmpeg.FfmpegCommandOptions | undefined) => ffmpeg.FfmpegCommand;
21
+ export declare const createFFMPEGBuilder: (...args: Parameters<typeof ffmpeg>) => ffmpeg.FfmpegCommand;
22
+ export declare function setMesioPath(newPath: string): void;
23
+ export declare function getMesioPath(): string;
21
24
  export declare function getDataFolderPath<E extends AnyObject>(provider: RecorderProvider<E>): string;
package/lib/index.js CHANGED
@@ -6,7 +6,8 @@ export * from "./common.js";
6
6
  export * from "./recorder.js";
7
7
  export * from "./manager.js";
8
8
  export * from "./record_extra_data_controller.js";
9
- export * from "./FFMPEGRecorder.js";
9
+ export * from "./recorder/FFMPEGRecorder.js";
10
+ export { createBaseRecorder } from "./recorder/index.js";
10
11
  export { utils };
11
12
  /**
12
13
  * 提供一些 utils
@@ -21,7 +22,6 @@ export function defaultToJSON(provider, recorder) {
21
22
  ...pick(recorder, [
22
23
  "id",
23
24
  "channelId",
24
- "owner",
25
25
  "remarks",
26
26
  "disableAutoCheck",
27
27
  "quality",
@@ -36,6 +36,7 @@ export function defaultToJSON(provider, recorder) {
36
36
  "liveInfo",
37
37
  "uid",
38
38
  "titleKeywords",
39
+ // "recordHandle",
39
40
  ]),
40
41
  };
41
42
  }
@@ -55,6 +56,14 @@ export const createFFMPEGBuilder = (...args) => {
55
56
  ffmpeg.setFfmpegPath(ffmpegPath);
56
57
  return ffmpeg(...args);
57
58
  };
59
+ // Mesio path management
60
+ let mesioPath = "mesio";
61
+ export function setMesioPath(newPath) {
62
+ mesioPath = newPath;
63
+ }
64
+ export function getMesioPath() {
65
+ return mesioPath;
66
+ }
58
67
  export function getDataFolderPath(provider) {
59
68
  return "./" + provider.id;
60
69
  }
package/lib/manager.d.ts CHANGED
@@ -2,7 +2,8 @@ import { Emitter } from "mitt";
2
2
  import { ChannelId, Message } from "./common.js";
3
3
  import { RecorderCreateOpts, Recorder, SerializedRecorder, RecordHandle, DebugLog, Progress } from "./recorder.js";
4
4
  import { AnyObject, UnknownObject } from "./utils.js";
5
- import { StreamManager } from "./streamManager.js";
5
+ import { StreamManager } from "./recorder/streamManager.js";
6
+ import { Cache } from "./cache.js";
6
7
  export interface RecorderProvider<E extends AnyObject> {
7
8
  id: string;
8
9
  name: string;
@@ -12,14 +13,14 @@ export interface RecorderProvider<E extends AnyObject> {
12
13
  id: ChannelId;
13
14
  title: string;
14
15
  owner: string;
15
- uid?: number;
16
+ uid?: number | string;
16
17
  avatar?: string;
17
18
  } | null>;
18
19
  createRecorder: (this: RecorderProvider<E>, opts: Omit<RecorderCreateOpts<E>, "providerId">) => Recorder<E>;
19
20
  fromJSON: <T extends SerializedRecorder<E>>(this: RecorderProvider<E>, json: T) => Recorder<E>;
20
21
  setFFMPEGOutputArgs: (this: RecorderProvider<E>, args: string[]) => void;
21
22
  }
22
- declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckInterval", "ffmpegOutputArgs", "biliBatchQuery"];
23
+ declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckInterval", "ffmpegOutputArgs", "biliBatchQuery", "recordRetryImmediately"];
23
24
  type ConfigurableProp = (typeof configurableProps)[number];
24
25
  export interface RecorderManager<ME extends UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> extends Emitter<{
25
26
  error: {
@@ -27,44 +28,44 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
27
28
  err: unknown;
28
29
  };
29
30
  RecordStart: {
30
- recorder: Recorder<E>;
31
+ recorder: SerializedRecorder<E>;
31
32
  recordHandle: RecordHandle;
32
33
  };
33
34
  RecordSegment: {
34
- recorder: Recorder<E>;
35
+ recorder: SerializedRecorder<E>;
35
36
  recordHandle?: RecordHandle;
36
37
  };
37
38
  videoFileCreated: {
38
- recorder: Recorder<E>;
39
+ recorder: SerializedRecorder<E>;
39
40
  filename: string;
40
41
  cover?: string;
41
42
  };
42
43
  videoFileCompleted: {
43
- recorder: Recorder<E>;
44
+ recorder: SerializedRecorder<E>;
44
45
  filename: string;
45
46
  };
46
47
  RecorderProgress: {
47
- recorder: Recorder<E>;
48
+ recorder: SerializedRecorder<E>;
48
49
  progress: Progress;
49
50
  };
50
51
  RecoderLiveStart: {
51
52
  recorder: Recorder<E>;
52
53
  };
53
54
  RecordStop: {
54
- recorder: Recorder<E>;
55
+ recorder: SerializedRecorder<E>;
55
56
  recordHandle: RecordHandle;
56
57
  reason?: string;
57
58
  };
58
59
  Message: {
59
- recorder: Recorder<E>;
60
+ recorder: SerializedRecorder<E>;
60
61
  message: Message;
61
62
  };
62
63
  RecorderUpdated: {
63
- recorder: Recorder<E>;
64
+ recorder: SerializedRecorder<E>;
64
65
  keys: (string | keyof Recorder<E>)[];
65
66
  };
66
- RecorderAdded: Recorder<E>;
67
- RecorderRemoved: Recorder<E>;
67
+ RecorderAdded: SerializedRecorder<E>;
68
+ RecorderRemoved: SerializedRecorder<E>;
68
69
  RecorderDebugLog: DebugLog & {
69
70
  recorder: Recorder<E>;
70
71
  };
@@ -73,7 +74,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
73
74
  providers: P[];
74
75
  getChannelURLMatchedRecorderProviders: (this: RecorderManager<ME, P, PE, E>, channelURL: string) => P[];
75
76
  recorders: Recorder<E>[];
76
- addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: RecorderCreateOpts<E>) => Recorder<E>;
77
+ addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: Omit<RecorderCreateOpts<E>, "cache">) => Recorder<E>;
77
78
  removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
78
79
  startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
79
80
  stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
@@ -87,6 +88,10 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
87
88
  ffmpegOutputArgs: string;
88
89
  /** b站使用批量查询接口 */
89
90
  biliBatchQuery: boolean;
91
+ /** 测试:录制错误立即重试 */
92
+ recordRetryImmediately: boolean;
93
+ /** 缓存实例 */
94
+ cache: Cache;
90
95
  }
91
96
  export type RecorderManagerCreateOpts<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> = Partial<Pick<RecorderManager<ME, P, PE, E>, ConfigurableProp>> & {
92
97
  providers: P[];
@@ -98,4 +103,4 @@ export declare function genSavePathFromRule<ME extends AnyObject, P extends Reco
98
103
  startTime?: number;
99
104
  }): string;
100
105
  export type GetProviderExtra<P> = P extends RecorderProvider<infer E> ? E : never;
101
- export { StreamManager };
106
+ export { StreamManager, Cache };
package/lib/manager.js CHANGED
@@ -5,13 +5,15 @@ import { omit, range } from "lodash-es";
5
5
  import { parseArgsStringToArgv } from "string-argv";
6
6
  import { getBiliStatusInfoByRoomIds } from "./api.js";
7
7
  import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, } from "./utils.js";
8
- import { StreamManager } from "./streamManager.js";
8
+ import { StreamManager } from "./recorder/streamManager.js";
9
+ import { Cache } from "./cache.js";
9
10
  const configurableProps = [
10
11
  "savePathRule",
11
12
  "autoRemoveSystemReservedChars",
12
13
  "autoCheckInterval",
13
14
  "ffmpegOutputArgs",
14
15
  "biliBatchQuery",
16
+ "recordRetryImmediately",
15
17
  ];
16
18
  function isConfigurableProp(prop) {
17
19
  return configurableProps.includes(prop);
@@ -89,6 +91,10 @@ export function createRecorderManager(opts) {
89
91
  const tempBanObj = {};
90
92
  // 用于是否触发LiveStart事件,不要重复触发
91
93
  const liveStartObj = {};
94
+ // 用于记录触发重试直播场次的次数
95
+ const retryCountObj = {};
96
+ // 获取缓存单例
97
+ const cache = Cache.getInstance();
92
98
  const manager = {
93
99
  // @ts-ignore
94
100
  ...mitt(),
@@ -103,24 +109,62 @@ export function createRecorderManager(opts) {
103
109
  throw new Error("Cant find provider " + opts.providerId);
104
110
  // TODO: 因为泛型函数内部是不持有具体泛型的,这里被迫用了 as,没什么好的思路处理,除非
105
111
  // provider.createRecorder 能返回 Recorder<PE> 才能进一步优化。
106
- const recorder = provider.createRecorder(omit(opts, ["providerId"]));
112
+ const recorder = provider.createRecorder({
113
+ ...omit(opts, ["providerId"]),
114
+ cache,
115
+ });
107
116
  this.recorders.push(recorder);
108
- recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder, recordHandle }));
109
- recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder, recordHandle }));
117
+ recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder: recorder.toJSON(), recordHandle }));
118
+ recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder: recorder.toJSON(), recordHandle }));
110
119
  recorder.on("videoFileCreated", ({ filename, cover }) => {
111
120
  if (recorder.saveCover && recorder?.liveInfo?.cover) {
112
121
  const coverPath = replaceExtName(filename, ".jpg");
113
122
  downloadImage(cover ?? recorder?.liveInfo?.cover, coverPath);
114
123
  }
115
- this.emit("videoFileCreated", { recorder, filename });
124
+ this.emit("videoFileCreated", { recorder: recorder.toJSON(), filename });
125
+ });
126
+ recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename }));
127
+ recorder.on("Message", (message) => this.emit("Message", { recorder: recorder.toJSON(), message }));
128
+ recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder: recorder.toJSON(), keys }));
129
+ recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
130
+ recorder.on("RecordStop", ({ recordHandle, reason }) => {
131
+ this.emit("RecordStop", { recorder: recorder.toJSON(), recordHandle, reason });
132
+ // 如果reason中存在"invalid stream",说明直播由于某些原因中断了,虽然会在下一次周期检查中继续,但是会遗漏一段时间。
133
+ // 这时候可以触发一次检查,但出于直播可能抽风的原因,为避免风控,一场直播最多触发五次。
134
+ // 测试阶段,还需要一个开关,默认关闭,几个版本后转正使用
135
+ // 也许之后还能链接复用,但也会引入更多复杂度,需要谨慎考虑
136
+ // 虎牙直播结束后可能额外触发导致错误,忽略虎牙直播间:https://www.huya.com/910323
137
+ if (manager.recordRetryImmediately &&
138
+ recorder.providerId !== "HuYa" &&
139
+ reason &&
140
+ reason.includes("invalid stream") &&
141
+ recorder?.liveInfo?.liveId) {
142
+ const key = `${recorder.channelId}-${recorder.liveInfo?.liveId}`;
143
+ if (retryCountObj[key] > 5)
144
+ return;
145
+ if (!retryCountObj[key]) {
146
+ retryCountObj[key] = 0;
147
+ }
148
+ if (retryCountObj[key] < 5) {
149
+ retryCountObj[key]++;
150
+ }
151
+ this.emit("RecorderDebugLog", {
152
+ recorder,
153
+ type: "common",
154
+ text: `录制${recorder?.channelId}因“${reason}”中断,触发重试直播(${retryCountObj[key]})`,
155
+ });
156
+ // 触发一次检查,等待一秒使状态清理完毕
157
+ setTimeout(() => {
158
+ recorder.checkLiveStatusAndRecord({
159
+ getSavePath(data) {
160
+ return genSavePathFromRule(manager, recorder, data);
161
+ },
162
+ });
163
+ }, 1000);
164
+ }
116
165
  });
117
- recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder, filename }));
118
- recorder.on("RecordStop", ({ recordHandle, reason }) => this.emit("RecordStop", { recorder, recordHandle, reason }));
119
- recorder.on("Message", (message) => this.emit("Message", { recorder, message }));
120
- recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder, keys }));
121
- recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder, ...log }));
122
166
  recorder.on("progress", (progress) => {
123
- this.emit("RecorderProgress", { recorder, progress });
167
+ this.emit("RecorderProgress", { recorder: recorder.toJSON(), progress });
124
168
  });
125
169
  recorder.on("videoFileCreated", () => {
126
170
  if (!recorder.liveInfo?.liveId)
@@ -129,9 +173,9 @@ export function createRecorderManager(opts) {
129
173
  if (liveStartObj[key])
130
174
  return;
131
175
  liveStartObj[key] = true;
132
- this.emit("RecoderLiveStart", { recorder });
176
+ this.emit("RecoderLiveStart", { recorder: recorder });
133
177
  });
134
- this.emit("RecorderAdded", recorder);
178
+ this.emit("RecorderAdded", recorder.toJSON());
135
179
  return recorder;
136
180
  },
137
181
  removeRecorder(recorder) {
@@ -141,7 +185,7 @@ export function createRecorderManager(opts) {
141
185
  recorder.recordHandle?.stop("remove recorder");
142
186
  this.recorders.splice(idx, 1);
143
187
  delete tempBanObj[recorder.channelId];
144
- this.emit("RecorderRemoved", recorder);
188
+ this.emit("RecorderRemoved", recorder.toJSON());
145
189
  },
146
190
  async startRecord(id) {
147
191
  const recorder = this.recorders.find((item) => item.id === id);
@@ -218,6 +262,7 @@ export function createRecorderManager(opts) {
218
262
  path.join(process.cwd(), "{platform}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}"),
219
263
  autoRemoveSystemReservedChars: opts.autoRemoveSystemReservedChars ?? true,
220
264
  biliBatchQuery: opts.biliBatchQuery ?? false,
265
+ recordRetryImmediately: opts.recordRetryImmediately ?? false,
221
266
  ffmpegOutputArgs: opts.ffmpegOutputArgs ??
222
267
  "-c copy" +
223
268
  /**
@@ -236,6 +281,7 @@ export function createRecorderManager(opts) {
236
281
  * TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
237
282
  */
238
283
  " -min_frag_duration 10000000",
284
+ cache,
239
285
  };
240
286
  const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
241
287
  const args = parseArgsStringToArgv(ffmpegOutputArgs);
@@ -260,6 +306,8 @@ export function genSavePathFromRule(manager, recorder, extData) {
260
306
  // TODO: 这里随便写的,后面再优化
261
307
  const provider = manager.providers.find((p) => p.id === recorder.toJSON().providerId);
262
308
  const now = extData?.startTime ? new Date(extData.startTime) : new Date();
309
+ const owner = (extData?.owner ?? "").replaceAll("%", "_");
310
+ const title = (extData?.title ?? "").replaceAll("%", "_");
263
311
  const params = {
264
312
  platform: provider?.name ?? "unknown",
265
313
  channelId: recorder.channelId,
@@ -271,6 +319,8 @@ export function genSavePathFromRule(manager, recorder, extData) {
271
319
  min: formatDate(now, "mm"),
272
320
  sec: formatDate(now, "ss"),
273
321
  ...extData,
322
+ owner: owner,
323
+ title: title,
274
324
  };
275
325
  if (manager.autoRemoveSystemReservedChars) {
276
326
  for (const key in params) {
@@ -286,4 +336,4 @@ export function genSavePathFromRule(manager, recorder, extData) {
286
336
  }
287
337
  return formatTemplate(savePathRule, params);
288
338
  }
289
- export { StreamManager };
339
+ export { StreamManager, Cache };
@@ -0,0 +1,37 @@
1
+ import EventEmitter from "node:events";
2
+ import { IRecorder, FFMPEGRecorderOptions } from "./IRecorder.js";
3
+ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
4
+ private onEnd;
5
+ private onUpdateLiveInfo;
6
+ private command;
7
+ private streamManager;
8
+ private timeoutChecker;
9
+ readonly hasSegment: boolean;
10
+ readonly getSavePath: (data: {
11
+ startTime: number;
12
+ title?: string;
13
+ }) => string;
14
+ readonly segment: number;
15
+ ffmpegOutputOptions: string[];
16
+ readonly inputOptions: string[];
17
+ readonly isHls: boolean;
18
+ readonly disableDanma: boolean;
19
+ readonly url: string;
20
+ formatName: "flv" | "ts" | "fmp4";
21
+ videoFormat: "ts" | "mkv" | "mp4";
22
+ readonly headers: {
23
+ [key: string]: string | undefined;
24
+ } | undefined;
25
+ constructor(opts: FFMPEGRecorderOptions, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
26
+ title?: string;
27
+ cover?: string;
28
+ }>);
29
+ createCommand(): import("@renmu/fluent-ffmpeg").FfmpegCommand;
30
+ formatLine(line: string): {
31
+ time: string | null;
32
+ } | null;
33
+ run(): void;
34
+ getArguments(): string[];
35
+ stop(): Promise<void>;
36
+ getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
37
+ }
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from "node:events";
2
- import { createFFMPEGBuilder, StreamManager, utils } from "./index.js";
3
- import { createInvalidStreamChecker, assert } from "./utils.js";
2
+ import { createFFMPEGBuilder, StreamManager, utils } from "../index.js";
3
+ import { createInvalidStreamChecker, assert } from "../utils.js";
4
4
  export class FFMPEGRecorder extends EventEmitter {
5
5
  onEnd;
6
6
  onUpdateLiveInfo;
@@ -15,30 +15,50 @@ export class FFMPEGRecorder extends EventEmitter {
15
15
  isHls;
16
16
  disableDanma = false;
17
17
  url;
18
+ formatName;
19
+ videoFormat;
18
20
  headers;
19
21
  constructor(opts, onEnd, onUpdateLiveInfo) {
20
22
  super();
21
23
  this.onEnd = onEnd;
22
24
  this.onUpdateLiveInfo = onUpdateLiveInfo;
23
25
  const hasSegment = !!opts.segment;
26
+ this.hasSegment = hasSegment;
27
+ let formatName = "flv";
28
+ if (opts.url.includes(".m3u8")) {
29
+ formatName = "ts";
30
+ }
31
+ this.formatName = opts.formatName ?? formatName;
32
+ if (this.formatName === "fmp4" || this.formatName === "ts") {
33
+ this.isHls = true;
34
+ }
35
+ else {
36
+ this.isHls = false;
37
+ }
38
+ let videoFormat = opts.videoFormat ?? "auto";
39
+ if (videoFormat === "auto") {
40
+ if (!this.hasSegment) {
41
+ videoFormat = "mp4";
42
+ if (this.formatName === "ts") {
43
+ videoFormat = "ts";
44
+ }
45
+ }
46
+ else {
47
+ videoFormat = "ts";
48
+ }
49
+ }
50
+ this.videoFormat = videoFormat;
24
51
  this.disableDanma = opts.disableDanma ?? false;
25
- this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat, {
52
+ this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "ffmpeg", this.videoFormat, {
26
53
  onUpdateLiveInfo: this.onUpdateLiveInfo,
27
54
  });
28
55
  this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
29
- this.hasSegment = hasSegment;
30
56
  this.getSavePath = opts.getSavePath;
31
57
  this.ffmpegOutputOptions = opts.outputOptions;
32
58
  this.inputOptions = opts.inputOptions ?? [];
33
59
  this.url = opts.url;
34
60
  this.segment = opts.segment;
35
61
  this.headers = opts.headers;
36
- if (opts.isHls === undefined) {
37
- this.isHls = this.url.includes("m3u8");
38
- }
39
- else {
40
- this.isHls = opts.isHls;
41
- }
42
62
  this.command = this.createCommand();
43
63
  this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
44
64
  this.emit("videoFileCreated", { filename, cover });
@@ -79,15 +99,16 @@ export class FFMPEGRecorder extends EventEmitter {
79
99
  .on("end", () => this.onEnd("finished"))
80
100
  .on("stderr", async (stderrLine) => {
81
101
  assert(typeof stderrLine === "string");
82
- await this.streamManager.handleVideoStarted(stderrLine);
83
102
  this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
103
+ const [isInvalid, reason] = isInvalidStream(stderrLine);
104
+ if (isInvalid) {
105
+ this.onEnd(reason);
106
+ }
107
+ await this.streamManager.handleVideoStarted(stderrLine);
84
108
  const info = this.formatLine(stderrLine);
85
109
  if (info) {
86
110
  this.emit("progress", info);
87
111
  }
88
- if (isInvalidStream(stderrLine)) {
89
- this.onEnd("invalid stream");
90
- }
91
112
  })
92
113
  .on("stderr", this.timeoutChecker?.update);
93
114
  if (this.hasSegment) {
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from "node:events";
2
+ /**
3
+ * 录制器构造函数选项的基础接口
4
+ */
5
+ export interface BaseRecorderOptions {
6
+ url: string;
7
+ getSavePath: (data: {
8
+ startTime: number;
9
+ title?: string;
10
+ }) => string;
11
+ segment: number;
12
+ inputOptions?: string[];
13
+ disableDanma?: boolean;
14
+ formatName?: "flv" | "ts" | "fmp4";
15
+ headers?: {
16
+ [key: string]: string | undefined;
17
+ };
18
+ }
19
+ /**
20
+ * 录制器接口定义
21
+ */
22
+ export interface IRecorder extends EventEmitter {
23
+ readonly hasSegment: boolean;
24
+ readonly segment: number;
25
+ readonly inputOptions: string[];
26
+ readonly isHls: boolean;
27
+ readonly disableDanma: boolean;
28
+ readonly url: string;
29
+ readonly headers: {
30
+ [key: string]: string | undefined;
31
+ } | undefined;
32
+ readonly getSavePath: (data: {
33
+ startTime: number;
34
+ title?: string;
35
+ }) => string;
36
+ run(): void;
37
+ stop(): Promise<void>;
38
+ getArguments(): string[];
39
+ getExtraDataController(): any;
40
+ createCommand(): any;
41
+ on(event: "videoFileCreated", listener: (data: {
42
+ filename: string;
43
+ cover?: string;
44
+ }) => void): this;
45
+ on(event: "videoFileCompleted", listener: (data: {
46
+ filename: string;
47
+ }) => void): this;
48
+ on(event: "DebugLog", listener: (data: {
49
+ type: string;
50
+ text: string;
51
+ }) => void): this;
52
+ on(event: "progress", listener: (info: any) => void): this;
53
+ on(event: string, listener: (...args: any[]) => void): this;
54
+ emit(event: "videoFileCreated", data: {
55
+ filename: string;
56
+ cover?: string;
57
+ }): boolean;
58
+ emit(event: "videoFileCompleted", data: {
59
+ filename: string;
60
+ }): boolean;
61
+ emit(event: "DebugLog", data: {
62
+ type: string;
63
+ text: string;
64
+ }): boolean;
65
+ emit(event: "progress", info: any): boolean;
66
+ emit(event: string, ...args: any[]): boolean;
67
+ }
68
+ /**
69
+ * FFMPEG录制器特定选项
70
+ */
71
+ export interface FFMPEGRecorderOptions extends BaseRecorderOptions {
72
+ outputOptions: string[];
73
+ videoFormat?: "auto" | "ts" | "mkv" | "mp4";
74
+ }
75
+ /**
76
+ * Mesio录制器特定选项
77
+ */
78
+ export interface MesioRecorderOptions extends BaseRecorderOptions {
79
+ outputOptions?: string[];
80
+ isHls?: boolean;
81
+ }
@@ -0,0 +1 @@
1
+ export {};