@bililive-tools/manager 1.8.0 → 1.10.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 +16 -13
- package/lib/cache.d.ts +55 -16
- package/lib/cache.js +61 -33
- package/lib/common.d.ts +1 -2
- package/lib/common.js +0 -1
- package/lib/{recorder/BililiveRecorder.d.ts → downloader/BililiveDownloader.d.ts} +4 -4
- package/lib/{recorder/BililiveRecorder.js → downloader/BililiveDownloader.js} +17 -10
- package/lib/{recorder/FFMPEGRecorder.d.ts → downloader/FFmpegDownloader.d.ts} +8 -6
- package/lib/{recorder/FFMPEGRecorder.js → downloader/FFmpegDownloader.js} +38 -12
- package/lib/{recorder/IRecorder.d.ts → downloader/IDownloader.d.ts} +11 -7
- package/lib/downloader/index.d.ts +48 -0
- package/lib/{recorder → downloader}/index.js +16 -13
- package/lib/{recorder/mesioRecorder.d.ts → downloader/mesioDownloader.d.ts} +4 -4
- package/lib/{recorder/mesioRecorder.js → downloader/mesioDownloader.js} +17 -9
- package/lib/{recorder → downloader}/streamManager.d.ts +5 -6
- package/lib/{recorder → downloader}/streamManager.js +8 -25
- package/lib/index.d.ts +6 -2
- package/lib/index.js +4 -2
- package/lib/manager.d.ts +12 -8
- package/lib/manager.js +29 -38
- package/lib/record_extra_data_controller.d.ts +1 -1
- package/lib/recorder.d.ts +15 -13
- package/lib/utils.d.ts +31 -1
- package/lib/utils.js +131 -2
- package/lib/xml_stream_controller.d.ts +2 -2
- package/lib/xml_stream_controller.js +26 -6
- package/package.json +1 -1
- package/lib/recorder/index.d.ts +0 -48
- /package/lib/{recorder/IRecorder.js → downloader/IDownloader.js} +0 -0
package/lib/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DebouncedFunc } from "lodash-es";
|
|
2
|
+
import type { Recorder } from "./recorder.js";
|
|
2
3
|
export type AnyObject = Record<string, any>;
|
|
3
4
|
export type UnknownObject = Record<string, unknown>;
|
|
4
5
|
export type PickRequired<T, K extends keyof T> = T & Pick<Required<T>, K>;
|
|
@@ -34,7 +35,7 @@ export declare function assertStringType(data: unknown, msg?: string): asserts d
|
|
|
34
35
|
export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
|
|
35
36
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
36
37
|
export declare function formatDate(date: Date, format: string): string;
|
|
37
|
-
export declare function removeSystemReservedChars(
|
|
38
|
+
export declare function removeSystemReservedChars(str: string): string;
|
|
38
39
|
export declare function isFfmpegStartSegment(line: string): boolean;
|
|
39
40
|
export declare function isMesioStartSegment(line: string): boolean;
|
|
40
41
|
export declare function isBililiveStartSegment(line: string): boolean;
|
|
@@ -74,6 +75,14 @@ export declare function sortByKeyOrder<T, K extends keyof T>(objects: T[], order
|
|
|
74
75
|
export declare function retry<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
|
|
75
76
|
export declare const isBetweenTimeRange: (range: undefined | [] | [string | null, string | null]) => boolean;
|
|
76
77
|
export declare const sleep: (ms: number) => Promise<unknown>;
|
|
78
|
+
/**
|
|
79
|
+
* 判断是否应该使用严格画质模式
|
|
80
|
+
* @param qualityRetryLeft 剩余的画质重试次数
|
|
81
|
+
* @param qualityRetry 初始画质重试次数配置
|
|
82
|
+
* @param isManualStart 是否手动启动
|
|
83
|
+
* @returns 是否使用严格画质模式
|
|
84
|
+
*/
|
|
85
|
+
export declare function shouldUseStrictQuality(qualityRetryLeft: number, qualityRetry: number, isManualStart?: boolean): boolean;
|
|
77
86
|
/**
|
|
78
87
|
* 检查标题是否包含黑名单关键词
|
|
79
88
|
*/
|
|
@@ -82,6 +91,24 @@ declare function hasBlockedTitleKeywords(title: string, titleKeywords: string |
|
|
|
82
91
|
* 检查是否需要进行标题关键词检查
|
|
83
92
|
*/
|
|
84
93
|
declare function shouldCheckTitleKeywords(isManualStart: boolean | undefined, titleKeywords: string | undefined): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* 逆向格式化"xxxB", "xxxKB", "xxxMB", "xxxGB"为字节数,如果值为空返回0,如果为数字则直接返回数字,如果带单位则转换为字节数
|
|
96
|
+
* @param sizeStr 大小字符串
|
|
97
|
+
* @returns 字节数
|
|
98
|
+
*/
|
|
99
|
+
export declare function parseSizeToBytes(sizeStr: string): number | string;
|
|
100
|
+
export declare const byte2MB: (bytes: number) => number;
|
|
101
|
+
export declare function checkTitleKeywordsWhileRecording(recorder: Recorder, isManualStart: boolean | undefined, getInfo: (channelId: string) => Promise<{
|
|
102
|
+
title: string;
|
|
103
|
+
}>): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* 检查开始录制前的标题关键词
|
|
106
|
+
* @param title 直播间标题
|
|
107
|
+
* @param recorder 录制器实例
|
|
108
|
+
* @param isManualStart 是否手动启动
|
|
109
|
+
* @returns 如果标题包含关键词返回 true(不应录制),否则返回 false
|
|
110
|
+
*/
|
|
111
|
+
export declare function checkTitleKeywordsBeforeRecord(title: string, recorder: Recorder, isManualStart: boolean | undefined): boolean;
|
|
85
112
|
declare const _default: {
|
|
86
113
|
replaceExtName: typeof replaceExtName;
|
|
87
114
|
singleton: typeof singleton;
|
|
@@ -103,6 +130,9 @@ declare const _default: {
|
|
|
103
130
|
isBetweenTimeRange: (range: undefined | [] | [string | null, string | null]) => boolean;
|
|
104
131
|
hasBlockedTitleKeywords: typeof hasBlockedTitleKeywords;
|
|
105
132
|
shouldCheckTitleKeywords: typeof shouldCheckTitleKeywords;
|
|
133
|
+
shouldUseStrictQuality: typeof shouldUseStrictQuality;
|
|
106
134
|
sleep: (ms: number) => Promise<unknown>;
|
|
135
|
+
checkTitleKeywordsWhileRecording: typeof checkTitleKeywordsWhileRecording;
|
|
136
|
+
checkTitleKeywordsBeforeRecord: typeof checkTitleKeywordsBeforeRecord;
|
|
107
137
|
};
|
|
108
138
|
export default _default;
|
package/lib/utils.js
CHANGED
|
@@ -120,8 +120,8 @@ export function formatDate(date, format) {
|
|
|
120
120
|
};
|
|
121
121
|
return format.replace(/yyyy|MM|dd|HH|mm|ss/g, (matched) => map[matched]);
|
|
122
122
|
}
|
|
123
|
-
export function removeSystemReservedChars(
|
|
124
|
-
return filenamify(
|
|
123
|
+
export function removeSystemReservedChars(str) {
|
|
124
|
+
return filenamify(str, { replacement: "_" });
|
|
125
125
|
}
|
|
126
126
|
export function isFfmpegStartSegment(line) {
|
|
127
127
|
return line.includes("Opening ") && line.includes("for writing");
|
|
@@ -327,6 +327,32 @@ function isBetweenTime(currentTime, timeRange) {
|
|
|
327
327
|
return start <= current && current <= end;
|
|
328
328
|
}
|
|
329
329
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
330
|
+
/**
|
|
331
|
+
* 判断是否应该使用严格画质模式
|
|
332
|
+
* @param qualityRetryLeft 剩余的画质重试次数
|
|
333
|
+
* @param qualityRetry 初始画质重试次数配置
|
|
334
|
+
* @param isManualStart 是否手动启动
|
|
335
|
+
* @returns 是否使用严格画质模式
|
|
336
|
+
*/
|
|
337
|
+
export function shouldUseStrictQuality(qualityRetryLeft, qualityRetry, isManualStart) {
|
|
338
|
+
// 手动启动时不使用严格模式
|
|
339
|
+
if (isManualStart) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
// 如果配置为0,不使用严格模式
|
|
343
|
+
if (qualityRetry === 0) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
// 如果还有重试次数,使用严格模式
|
|
347
|
+
if (qualityRetryLeft > 0) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
// 如果配置为负数(无限重试),使用严格模式
|
|
351
|
+
if (qualityRetry < 0) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
330
356
|
/**
|
|
331
357
|
* 检查标题是否包含黑名单关键词
|
|
332
358
|
*/
|
|
@@ -343,6 +369,106 @@ function hasBlockedTitleKeywords(title, titleKeywords) {
|
|
|
343
369
|
function shouldCheckTitleKeywords(isManualStart, titleKeywords) {
|
|
344
370
|
return (!isManualStart && !!titleKeywords && typeof titleKeywords === "string" && !!titleKeywords.trim());
|
|
345
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* 逆向格式化"xxxB", "xxxKB", "xxxMB", "xxxGB"为字节数,如果值为空返回0,如果为数字则直接返回数字,如果带单位则转换为字节数
|
|
374
|
+
* @param sizeStr 大小字符串
|
|
375
|
+
* @returns 字节数
|
|
376
|
+
*/
|
|
377
|
+
export function parseSizeToBytes(sizeStr) {
|
|
378
|
+
if (!sizeStr) {
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
// 字符类型的数字
|
|
382
|
+
if (!isNaN(Number(sizeStr))) {
|
|
383
|
+
return Number(sizeStr);
|
|
384
|
+
}
|
|
385
|
+
const sizePattern = /^(\d+(?:\.\d+)?)(B|KB|MB|GB)$/i;
|
|
386
|
+
const match = sizeStr.toUpperCase().trim().match(sizePattern);
|
|
387
|
+
if (match) {
|
|
388
|
+
const size = parseFloat(match[1]);
|
|
389
|
+
const unit = match[2];
|
|
390
|
+
switch (unit) {
|
|
391
|
+
case "B":
|
|
392
|
+
return String(size);
|
|
393
|
+
case "KB":
|
|
394
|
+
return String(size * 1024);
|
|
395
|
+
case "MB":
|
|
396
|
+
return String(size * 1024 * 1024);
|
|
397
|
+
case "GB":
|
|
398
|
+
return String(size * 1024 * 1024 * 1024);
|
|
399
|
+
default:
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
export const byte2MB = (bytes) => {
|
|
408
|
+
return bytes / (1024 * 1024);
|
|
409
|
+
};
|
|
410
|
+
/*
|
|
411
|
+
* 检查录制中的标题关键词
|
|
412
|
+
* @param recorder 录制器实例
|
|
413
|
+
* @param isManualStart 是否手动启动
|
|
414
|
+
* @param getInfo 获取直播间信息的函数
|
|
415
|
+
* @returns 如果标题包含关键词返回 true(需要停止),否则返回 false
|
|
416
|
+
*/
|
|
417
|
+
export async function checkTitleKeywordsWhileRecording(recorder, isManualStart, getInfo) {
|
|
418
|
+
// 只有当设置了标题关键词时,并且不是手动启动的录制,才获取最新的直播间信息
|
|
419
|
+
if (!shouldCheckTitleKeywords(isManualStart, recorder.titleKeywords)) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const now = Date.now();
|
|
423
|
+
// 每5分钟检查一次标题变化
|
|
424
|
+
const titleCheckInterval = 5 * 60 * 1000; // 5分钟
|
|
425
|
+
// 获取上次检查时间
|
|
426
|
+
const lastCheckTime = await recorder.cache.get("lastTitleCheckTime");
|
|
427
|
+
// 如果距离上次检查时间不足指定间隔,则跳过检查
|
|
428
|
+
if (lastCheckTime && now - lastCheckTime < titleCheckInterval) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
// 更新检查时间
|
|
432
|
+
await recorder.cache.set("lastTitleCheckTime", now);
|
|
433
|
+
// 获取直播间信息
|
|
434
|
+
const liveInfo = await getInfo(recorder.channelId);
|
|
435
|
+
const { title } = liveInfo;
|
|
436
|
+
// 检查标题是否包含关键词
|
|
437
|
+
if (hasBlockedTitleKeywords(title, recorder.titleKeywords)) {
|
|
438
|
+
recorder.state = "title-blocked";
|
|
439
|
+
recorder.emit("DebugLog", {
|
|
440
|
+
type: "common",
|
|
441
|
+
text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
|
|
442
|
+
});
|
|
443
|
+
// 停止录制
|
|
444
|
+
await recorder?.recordHandle?.stop("直播间标题包含关键词");
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* 检查开始录制前的标题关键词
|
|
451
|
+
* @param title 直播间标题
|
|
452
|
+
* @param recorder 录制器实例
|
|
453
|
+
* @param isManualStart 是否手动启动
|
|
454
|
+
* @returns 如果标题包含关键词返回 true(不应录制),否则返回 false
|
|
455
|
+
*/
|
|
456
|
+
export function checkTitleKeywordsBeforeRecord(title, recorder, isManualStart) {
|
|
457
|
+
// 检查标题是否包含关键词,如果包含则不自动录制
|
|
458
|
+
// 手动开始录制时不检查标题关键词
|
|
459
|
+
if (!shouldCheckTitleKeywords(isManualStart, recorder.titleKeywords)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (hasBlockedTitleKeywords(title, recorder.titleKeywords)) {
|
|
463
|
+
recorder.state = "title-blocked";
|
|
464
|
+
recorder.emit("DebugLog", {
|
|
465
|
+
type: "common",
|
|
466
|
+
text: `跳过录制:直播间标题 "${title}" 包含关键词 "${recorder.titleKeywords}"`,
|
|
467
|
+
});
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
346
472
|
export default {
|
|
347
473
|
replaceExtName,
|
|
348
474
|
singleton,
|
|
@@ -364,5 +490,8 @@ export default {
|
|
|
364
490
|
isBetweenTimeRange,
|
|
365
491
|
hasBlockedTitleKeywords,
|
|
366
492
|
shouldCheckTitleKeywords,
|
|
493
|
+
shouldUseStrictQuality,
|
|
367
494
|
sleep,
|
|
495
|
+
checkTitleKeywordsWhileRecording,
|
|
496
|
+
checkTitleKeywordsBeforeRecord,
|
|
368
497
|
};
|
|
@@ -5,7 +5,7 @@ export interface XmlStreamData {
|
|
|
5
5
|
recordStartTimestamp: number;
|
|
6
6
|
recordStopTimestamp?: number;
|
|
7
7
|
liveStartTimestamp?: number;
|
|
8
|
-
|
|
8
|
+
downloaderArgs?: string[];
|
|
9
9
|
platform?: string;
|
|
10
10
|
user_name?: string;
|
|
11
11
|
room_id?: string;
|
|
@@ -17,7 +17,7 @@ export interface XmlStreamController {
|
|
|
17
17
|
/** 设计上来说,外部程序不应该能直接修改 data 上的东西 */
|
|
18
18
|
readonly data: XmlStreamData;
|
|
19
19
|
addMessage: (message: Message) => void;
|
|
20
|
-
setMeta: (meta: Partial<XmlStreamData["meta"]>) => void
|
|
20
|
+
setMeta: (meta: Partial<XmlStreamData["meta"]>) => Promise<void>;
|
|
21
21
|
flush: () => Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
export declare function createRecordExtraDataController(savePath: string): XmlStreamController;
|
|
@@ -70,13 +70,16 @@ export function createRecordExtraDataController(savePath) {
|
|
|
70
70
|
initializeFile().catch(console.error);
|
|
71
71
|
scheduleWrite();
|
|
72
72
|
};
|
|
73
|
-
const setMeta = (meta) => {
|
|
73
|
+
const setMeta = async (meta) => {
|
|
74
74
|
if (hasCompleted)
|
|
75
75
|
return;
|
|
76
76
|
data.meta = {
|
|
77
77
|
...data.meta,
|
|
78
78
|
...meta,
|
|
79
79
|
};
|
|
80
|
+
// 确保文件已初始化,然后立即更新文件中的metadata
|
|
81
|
+
await initializeFile().catch(console.error);
|
|
82
|
+
await updateMetadataInFile(savePath, data.meta).catch(console.error);
|
|
80
83
|
};
|
|
81
84
|
const flush = async () => {
|
|
82
85
|
if (hasCompleted)
|
|
@@ -89,7 +92,7 @@ export function createRecordExtraDataController(savePath) {
|
|
|
89
92
|
await writeToFile();
|
|
90
93
|
}
|
|
91
94
|
// 完成XML文件(添加结束标签等)
|
|
92
|
-
await finalizeXmlFile(savePath
|
|
95
|
+
await finalizeXmlFile(savePath);
|
|
93
96
|
// 清理内存
|
|
94
97
|
data.pendingMessages = [];
|
|
95
98
|
};
|
|
@@ -206,9 +209,9 @@ async function appendToXmlFile(filePath, content) {
|
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
/**
|
|
209
|
-
*
|
|
212
|
+
* 更新XML文件中的metadata
|
|
210
213
|
*/
|
|
211
|
-
async function
|
|
214
|
+
async function updateMetadataInFile(filePath, metadata) {
|
|
212
215
|
try {
|
|
213
216
|
const builder = new XMLBuilder({
|
|
214
217
|
ignoreAttributes: false,
|
|
@@ -228,8 +231,25 @@ async function finalizeXmlFile(filePath, metadata) {
|
|
|
228
231
|
});
|
|
229
232
|
// 读取文件内容
|
|
230
233
|
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
231
|
-
// 替换占位符为实际的metadata
|
|
232
|
-
const
|
|
234
|
+
// 替换占位符为实际的metadata
|
|
235
|
+
const updatedContent = content.replace("<!--METADATA_PLACEHOLDER-->", metadataXml);
|
|
236
|
+
// 写回文件
|
|
237
|
+
await fs.promises.writeFile(filePath, updatedContent);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error(`更新XML文件metadata失败: ${filePath}`, error);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 完成XML文件写入
|
|
246
|
+
*/
|
|
247
|
+
async function finalizeXmlFile(filePath) {
|
|
248
|
+
try {
|
|
249
|
+
// 读取文件内容
|
|
250
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
251
|
+
// 添加结束标签
|
|
252
|
+
const finalContent = content + "</i>";
|
|
233
253
|
// 写回文件
|
|
234
254
|
await fs.promises.writeFile(filePath, finalContent);
|
|
235
255
|
}
|
package/package.json
CHANGED
package/lib/recorder/index.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { FFMPEGRecorder } from "./FFMPEGRecorder.js";
|
|
2
|
-
import { MesioRecorder } from "./mesioRecorder.js";
|
|
3
|
-
import { BililiveRecorder } from "./BililiveRecorder.js";
|
|
4
|
-
export { FFMPEGRecorder } from "./FFMPEGRecorder.js";
|
|
5
|
-
export { MesioRecorder } from "./mesioRecorder.js";
|
|
6
|
-
export { BililiveRecorder } from "./BililiveRecorder.js";
|
|
7
|
-
import type { IRecorder, FFMPEGRecorderOptions, MesioRecorderOptions, BililiveRecorderOptions } from "./IRecorder.js";
|
|
8
|
-
/**
|
|
9
|
-
* 录制器类型
|
|
10
|
-
*/
|
|
11
|
-
export type RecorderType = "ffmpeg" | "mesio" | "bililive";
|
|
12
|
-
export type FormatName = "flv" | "ts" | "fmp4";
|
|
13
|
-
/**
|
|
14
|
-
* 根据录制器类型获取对应的配置选项类型
|
|
15
|
-
*/
|
|
16
|
-
export type RecorderOptions<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorderOptions : T extends "mesio" ? MesioRecorderOptions : BililiveRecorderOptions;
|
|
17
|
-
/**
|
|
18
|
-
* 根据录制器类型获取对应的录制器实例类型
|
|
19
|
-
*/
|
|
20
|
-
export type RecorderInstance<T extends RecorderType> = T extends "ffmpeg" ? FFMPEGRecorder : T extends "mesio" ? MesioRecorder : BililiveRecorder;
|
|
21
|
-
type RecorderOpts = FFMPEGRecorderOptions | MesioRecorderOptions | BililiveRecorderOptions;
|
|
22
|
-
/**
|
|
23
|
-
* 创建录制器的工厂函数
|
|
24
|
-
*/
|
|
25
|
-
export declare function createRecorder<T extends RecorderType>(type: T, opts: RecorderOptions<T> & {
|
|
26
|
-
onlyAudio?: boolean;
|
|
27
|
-
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
28
|
-
title?: string;
|
|
29
|
-
cover?: string;
|
|
30
|
-
}>): IRecorder;
|
|
31
|
-
/**
|
|
32
|
-
* 选择录制器
|
|
33
|
-
*/
|
|
34
|
-
export declare function selectRecorder(preferredRecorder: "auto" | RecorderType | undefined): RecorderType;
|
|
35
|
-
/**
|
|
36
|
-
* 判断原始录制流格式,flv, ts, m4s
|
|
37
|
-
*/
|
|
38
|
-
export declare function getSourceFormatName(streamUrl: string, formatName: FormatName | undefined): FormatName;
|
|
39
|
-
type PickPartial<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>;
|
|
40
|
-
/**
|
|
41
|
-
* 创建录制器的工厂函数
|
|
42
|
-
*/
|
|
43
|
-
export declare function createBaseRecorder(type: "auto" | RecorderType | undefined, opts: PickPartial<RecorderOpts, "formatName"> & {
|
|
44
|
-
onlyAudio?: boolean;
|
|
45
|
-
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
46
|
-
title?: string;
|
|
47
|
-
cover?: string;
|
|
48
|
-
}>): IRecorder;
|
|
File without changes
|