@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/README.md
CHANGED
|
@@ -94,19 +94,22 @@ setBililivePath("BililiveRecorder.Cli.exe");
|
|
|
94
94
|
|
|
95
95
|
默认值为 `{platform}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}`
|
|
96
96
|
|
|
97
|
-
| 值
|
|
98
|
-
|
|
|
99
|
-
| {platform}
|
|
100
|
-
| {channelId}
|
|
101
|
-
| {remarks}
|
|
102
|
-
| {owner}
|
|
103
|
-
| {title}
|
|
104
|
-
| {year}
|
|
105
|
-
| {month}
|
|
106
|
-
| {date}
|
|
107
|
-
| {hour}
|
|
108
|
-
| {min}
|
|
109
|
-
| {sec}
|
|
97
|
+
| 值 | 标签 |
|
|
98
|
+
| ----------------- | ------------------------------------------ |
|
|
99
|
+
| {platform} | 平台 |
|
|
100
|
+
| {channelId} | 房间号 |
|
|
101
|
+
| {remarks} | 备注 |
|
|
102
|
+
| {owner} | 主播名 |
|
|
103
|
+
| {title} | 标题 |
|
|
104
|
+
| {year} | 年 |
|
|
105
|
+
| {month} | 月 |
|
|
106
|
+
| {date} | 日 |
|
|
107
|
+
| {hour} | 时 |
|
|
108
|
+
| {min} | 分 |
|
|
109
|
+
| {sec} | 秒 |
|
|
110
|
+
| {startTime} | 分段开始时间,Date对象 |
|
|
111
|
+
| {recordStartTime} | 录制开始时间,Date对象 |
|
|
112
|
+
| {liveStartTime} | 直播开始时间,Date对象,抖音同录制开始时间 |
|
|
110
113
|
|
|
111
114
|
## 事件
|
|
112
115
|
|
package/lib/cache.d.ts
CHANGED
|
@@ -1,17 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cache system for RecorderManager
|
|
3
|
+
* 提供统一的缓存接口用于处理持久化事务
|
|
4
|
+
*/
|
|
5
|
+
export interface CacheStore {
|
|
6
|
+
get<T = any>(key: string): Promise<T | undefined>;
|
|
7
|
+
set<T = any>(key: string, value: T, ttl?: number): Promise<void>;
|
|
8
|
+
delete(key: string): Promise<void>;
|
|
9
|
+
clear(): Promise<void>;
|
|
10
|
+
has(key: string): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export interface RecorderCache {
|
|
13
|
+
/**
|
|
14
|
+
* 为每个录制器创建独立的命名空间
|
|
15
|
+
* @param recorderId 录制器 ID
|
|
16
|
+
*/
|
|
17
|
+
createNamespace(recorderId: string): NamespacedCache;
|
|
18
|
+
/**
|
|
19
|
+
* 获取全局缓存
|
|
20
|
+
*/
|
|
21
|
+
global(): NamespacedCache;
|
|
22
|
+
}
|
|
23
|
+
export interface NamespacedCache {
|
|
24
|
+
/**
|
|
25
|
+
* 通用的 key-value 存储
|
|
26
|
+
*/
|
|
27
|
+
get<T = any>(key: string): Promise<T | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* 通用的 key-value 存储
|
|
30
|
+
*/
|
|
31
|
+
set<T = any>(key: string, value: T): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* 删除指定 key
|
|
34
|
+
*/
|
|
35
|
+
delete(key: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 内存缓存实现
|
|
39
|
+
*/
|
|
40
|
+
export declare class MemoryCacheStore implements CacheStore {
|
|
41
|
+
private store;
|
|
42
|
+
get<T = any>(key: string): Promise<T | undefined>;
|
|
43
|
+
set<T = any>(key: string, value: T, ttl?: number): Promise<void>;
|
|
44
|
+
delete(key: string): Promise<void>;
|
|
45
|
+
clear(): Promise<void>;
|
|
46
|
+
has(key: string): Promise<boolean>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* RecorderCache 实现
|
|
50
|
+
*/
|
|
51
|
+
export declare class RecorderCacheImpl implements RecorderCache {
|
|
52
|
+
private store;
|
|
53
|
+
constructor(store: CacheStore);
|
|
54
|
+
createNamespace(recorderId: string): NamespacedCache;
|
|
55
|
+
global(): NamespacedCache;
|
|
17
56
|
}
|
package/lib/cache.js
CHANGED
|
@@ -1,47 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cache system for RecorderManager
|
|
3
|
+
* 提供统一的缓存接口用于处理持久化事务
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 内存缓存实现
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryCacheStore {
|
|
9
|
+
store = new Map();
|
|
10
|
+
async get(key) {
|
|
11
|
+
const item = this.store.get(key);
|
|
12
|
+
if (!item)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (item.expireAt && Date.now() > item.expireAt) {
|
|
15
|
+
this.store.delete(key);
|
|
16
|
+
return undefined;
|
|
10
17
|
}
|
|
11
|
-
return
|
|
18
|
+
return item.value;
|
|
12
19
|
}
|
|
13
|
-
set(key, value) {
|
|
14
|
-
|
|
20
|
+
async set(key, value, ttl) {
|
|
21
|
+
const item = { value };
|
|
22
|
+
if (ttl) {
|
|
23
|
+
item.expireAt = Date.now() + ttl;
|
|
24
|
+
}
|
|
25
|
+
this.store.set(key, item);
|
|
26
|
+
}
|
|
27
|
+
async delete(key) {
|
|
28
|
+
this.store.delete(key);
|
|
15
29
|
}
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
async clear() {
|
|
31
|
+
this.store.clear();
|
|
18
32
|
}
|
|
19
|
-
has(key) {
|
|
20
|
-
|
|
33
|
+
async has(key) {
|
|
34
|
+
const value = await this.get(key);
|
|
35
|
+
return value !== undefined;
|
|
21
36
|
}
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* RecorderCache 实现
|
|
40
|
+
*/
|
|
41
|
+
export class RecorderCacheImpl {
|
|
42
|
+
store;
|
|
43
|
+
constructor(store) {
|
|
44
|
+
this.store = store;
|
|
24
45
|
}
|
|
25
|
-
|
|
26
|
-
this.
|
|
46
|
+
createNamespace(recorderId) {
|
|
47
|
+
return new NamespacedCacheImpl(this.store, `recorder:${recorderId}`);
|
|
27
48
|
}
|
|
28
|
-
|
|
29
|
-
return this.
|
|
49
|
+
global() {
|
|
50
|
+
return new NamespacedCacheImpl(this.store, "global");
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 命名空间缓存实现
|
|
55
|
+
*/
|
|
56
|
+
class NamespacedCacheImpl {
|
|
57
|
+
store;
|
|
58
|
+
namespace;
|
|
59
|
+
constructor(store, namespace) {
|
|
60
|
+
this.store = store;
|
|
61
|
+
this.namespace = namespace;
|
|
33
62
|
}
|
|
34
|
-
|
|
35
|
-
return this.
|
|
63
|
+
getKey(key) {
|
|
64
|
+
return `${this.namespace}:${key}`;
|
|
36
65
|
}
|
|
37
|
-
|
|
38
|
-
return this.
|
|
66
|
+
async get(key) {
|
|
67
|
+
return this.store.get(this.getKey(key));
|
|
39
68
|
}
|
|
40
|
-
|
|
41
|
-
this.
|
|
69
|
+
async set(key, value) {
|
|
70
|
+
return this.store.set(this.getKey(key), value);
|
|
42
71
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return this.data[Symbol.iterator]();
|
|
72
|
+
async delete(key) {
|
|
73
|
+
return this.store.delete(this.getKey(key));
|
|
46
74
|
}
|
|
47
75
|
}
|
package/lib/common.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { AnyObject, UnknownObject } from "./utils.js";
|
|
2
2
|
export type ChannelId = string;
|
|
3
3
|
export declare const Qualities: readonly ["lowest", "low", "medium", "high", "highest"];
|
|
4
|
-
export declare const BiliQualities: readonly [30000, 20000, 10000, 400, 250, 150, 80];
|
|
5
4
|
export declare const DouyuQualities: readonly [0, 2, 3, 4, 8];
|
|
6
5
|
export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500, -1];
|
|
7
6
|
export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
|
|
8
|
-
export type Quality =
|
|
7
|
+
export type Quality = string | number;
|
|
9
8
|
export interface MessageSender<E extends AnyObject = UnknownObject> {
|
|
10
9
|
uid?: string;
|
|
11
10
|
name: string;
|
package/lib/common.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const Qualities = ["lowest", "low", "medium", "high", "highest"];
|
|
2
|
-
export const BiliQualities = [30000, 20000, 10000, 400, 250, 150, 80];
|
|
3
2
|
export const DouyuQualities = [0, 2, 3, 4, 8];
|
|
4
3
|
// 14100: 2K HDR;14000:2K;4200:HDR(10M);0:原画;8000:蓝光8M;4000:蓝光4M;2000:超清;500:流畅
|
|
5
4
|
export const HuYaQualities = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
-
import {
|
|
2
|
+
import { IDownloader, BililiveRecorderOptions, Segment } from "./IDownloader.js";
|
|
3
3
|
declare class BililiveRecorderCommand extends EventEmitter {
|
|
4
4
|
private _input;
|
|
5
5
|
private _output;
|
|
@@ -15,7 +15,7 @@ declare class BililiveRecorderCommand extends EventEmitter {
|
|
|
15
15
|
kill(signal?: NodeJS.Signals): void;
|
|
16
16
|
}
|
|
17
17
|
export declare const createBililiveBuilder: () => BililiveRecorderCommand;
|
|
18
|
-
export declare class
|
|
18
|
+
export declare class BililiveDownloader extends EventEmitter implements IDownloader {
|
|
19
19
|
private onEnd;
|
|
20
20
|
private onUpdateLiveInfo;
|
|
21
21
|
type: "bililive";
|
|
@@ -26,9 +26,8 @@ export declare class BililiveRecorder extends EventEmitter implements IRecorder
|
|
|
26
26
|
startTime: number;
|
|
27
27
|
title?: string;
|
|
28
28
|
}) => string;
|
|
29
|
-
readonly segment:
|
|
29
|
+
readonly segment: Segment;
|
|
30
30
|
readonly inputOptions: string[];
|
|
31
|
-
readonly disableDanma: boolean;
|
|
32
31
|
readonly url: string;
|
|
33
32
|
readonly debugLevel: "none" | "basic" | "verbose";
|
|
34
33
|
readonly headers: {
|
|
@@ -46,5 +45,6 @@ export declare class BililiveRecorder extends EventEmitter implements IRecorder
|
|
|
46
45
|
getArguments(): string[];
|
|
47
46
|
stop(): Promise<void>;
|
|
48
47
|
getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
|
|
48
|
+
get videoFilePath(): string;
|
|
49
49
|
}
|
|
50
50
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { StreamManager, getBililivePath } from "../index.js";
|
|
4
|
+
import { byte2MB } from "../utils.js";
|
|
4
5
|
// Bililive command builder class similar to ffmpeg
|
|
5
6
|
class BililiveRecorderCommand extends EventEmitter {
|
|
6
7
|
_input = "";
|
|
@@ -43,7 +44,6 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
43
44
|
run() {
|
|
44
45
|
const args = this._getArguments();
|
|
45
46
|
const bililiveExecutable = getBililivePath();
|
|
46
|
-
console.log("Starting BililiveRecorder with args:", bililiveExecutable, args);
|
|
47
47
|
this.process = spawn(bililiveExecutable, args, {
|
|
48
48
|
stdio: ["pipe", "pipe", "pipe"],
|
|
49
49
|
});
|
|
@@ -84,7 +84,7 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
84
84
|
export const createBililiveBuilder = () => {
|
|
85
85
|
return new BililiveRecorderCommand();
|
|
86
86
|
};
|
|
87
|
-
export class
|
|
87
|
+
export class BililiveDownloader extends EventEmitter {
|
|
88
88
|
onEnd;
|
|
89
89
|
onUpdateLiveInfo;
|
|
90
90
|
type = "bililive";
|
|
@@ -94,7 +94,6 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
94
94
|
getSavePath;
|
|
95
95
|
segment;
|
|
96
96
|
inputOptions = [];
|
|
97
|
-
disableDanma = false;
|
|
98
97
|
url;
|
|
99
98
|
debugLevel = "none";
|
|
100
99
|
headers;
|
|
@@ -102,22 +101,22 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
102
101
|
super();
|
|
103
102
|
this.onEnd = onEnd;
|
|
104
103
|
this.onUpdateLiveInfo = onUpdateLiveInfo;
|
|
104
|
+
// 存在自动分段,永远为true
|
|
105
105
|
const hasSegment = true;
|
|
106
|
-
this.
|
|
106
|
+
this.hasSegment = hasSegment;
|
|
107
107
|
this.debugLevel = opts.debugLevel ?? "none";
|
|
108
108
|
let videoFormat = "flv";
|
|
109
|
-
this.streamManager = new StreamManager(opts.getSavePath, hasSegment,
|
|
109
|
+
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, "bililive", videoFormat, {
|
|
110
110
|
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
111
111
|
});
|
|
112
|
-
this.hasSegment = hasSegment;
|
|
113
112
|
this.getSavePath = opts.getSavePath;
|
|
114
113
|
this.inputOptions = [];
|
|
115
114
|
this.url = opts.url;
|
|
116
115
|
this.segment = opts.segment;
|
|
117
116
|
this.headers = opts.headers;
|
|
118
117
|
this.command = this.createCommand();
|
|
119
|
-
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
|
|
120
|
-
this.emit("videoFileCreated", { filename, cover, rawFilename });
|
|
118
|
+
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
|
|
119
|
+
this.emit("videoFileCreated", { filename, cover, rawFilename, title });
|
|
121
120
|
});
|
|
122
121
|
this.streamManager.on("videoFileCompleted", ({ filename }) => {
|
|
123
122
|
this.emit("videoFileCompleted", { filename });
|
|
@@ -142,8 +141,13 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
142
141
|
inputOptions.push("-h", `${key}: ${value}`);
|
|
143
142
|
});
|
|
144
143
|
}
|
|
145
|
-
if (this.
|
|
146
|
-
|
|
144
|
+
if (this.segment) {
|
|
145
|
+
if (typeof this.segment === "number") {
|
|
146
|
+
inputOptions.push("-d", `${this.segment}`);
|
|
147
|
+
}
|
|
148
|
+
else if (typeof this.segment === "string") {
|
|
149
|
+
inputOptions.push("-m", byte2MB(Number(this.segment)).toFixed(2));
|
|
150
|
+
}
|
|
147
151
|
}
|
|
148
152
|
const command = createBililiveBuilder()
|
|
149
153
|
.input(this.url)
|
|
@@ -193,4 +197,7 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
193
197
|
getExtraDataController() {
|
|
194
198
|
return this.streamManager?.getExtraDataController();
|
|
195
199
|
}
|
|
200
|
+
get videoFilePath() {
|
|
201
|
+
return this.streamManager.videoFilePath;
|
|
202
|
+
}
|
|
196
203
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
2
|
+
import { IDownloader, FFMPEGRecorderOptions, Segment } from "./IDownloader.js";
|
|
3
|
+
import { FormatName } from "./index.js";
|
|
4
|
+
import type { VideoFormat } from "../index.js";
|
|
5
|
+
export declare class FFmpegDownloader extends EventEmitter implements IDownloader {
|
|
5
6
|
private onEnd;
|
|
6
7
|
private onUpdateLiveInfo;
|
|
7
8
|
type: "ffmpeg";
|
|
@@ -13,14 +14,13 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
13
14
|
startTime: number;
|
|
14
15
|
title?: string;
|
|
15
16
|
}) => string;
|
|
16
|
-
readonly segment:
|
|
17
|
+
readonly segment: Segment;
|
|
17
18
|
ffmpegOutputOptions: string[];
|
|
18
19
|
readonly inputOptions: string[];
|
|
19
20
|
readonly isHls: boolean;
|
|
20
|
-
readonly disableDanma: boolean;
|
|
21
21
|
readonly url: string;
|
|
22
22
|
formatName: FormatName;
|
|
23
|
-
videoFormat:
|
|
23
|
+
videoFormat: VideoFormat;
|
|
24
24
|
readonly debugLevel: "none" | "basic" | "verbose";
|
|
25
25
|
readonly headers: {
|
|
26
26
|
[key: string]: string | undefined;
|
|
@@ -30,6 +30,7 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
30
30
|
cover?: string;
|
|
31
31
|
}>);
|
|
32
32
|
createCommand(): import("@renmu/fluent-ffmpeg").FfmpegCommand;
|
|
33
|
+
buildOutputOptions(): string[];
|
|
33
34
|
formatLine(line: string): {
|
|
34
35
|
time: string | null;
|
|
35
36
|
} | null;
|
|
@@ -37,4 +38,5 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
37
38
|
getArguments(): string[];
|
|
38
39
|
stop(): Promise<void>;
|
|
39
40
|
getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
|
|
41
|
+
get videoFilePath(): string;
|
|
40
42
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { createFFMPEGBuilder, StreamManager, utils } from "../index.js";
|
|
3
3
|
import { createInvalidStreamChecker, assert } from "../utils.js";
|
|
4
|
-
export class
|
|
4
|
+
export class FFmpegDownloader extends EventEmitter {
|
|
5
5
|
onEnd;
|
|
6
6
|
onUpdateLiveInfo;
|
|
7
7
|
type = "ffmpeg";
|
|
@@ -14,7 +14,6 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
14
14
|
ffmpegOutputOptions = [];
|
|
15
15
|
inputOptions = [];
|
|
16
16
|
isHls;
|
|
17
|
-
disableDanma = false;
|
|
18
17
|
url;
|
|
19
18
|
formatName;
|
|
20
19
|
videoFormat;
|
|
@@ -24,7 +23,11 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
24
23
|
super();
|
|
25
24
|
this.onEnd = onEnd;
|
|
26
25
|
this.onUpdateLiveInfo = onUpdateLiveInfo;
|
|
27
|
-
|
|
26
|
+
let hasSegment = false;
|
|
27
|
+
// 只有数字才表示时间分段,只有时间分段才会在ffmpeg走分段逻辑
|
|
28
|
+
if (opts.segment && typeof opts.segment === "number") {
|
|
29
|
+
hasSegment = true;
|
|
30
|
+
}
|
|
28
31
|
this.hasSegment = hasSegment;
|
|
29
32
|
this.debugLevel = opts.debugLevel ?? "none";
|
|
30
33
|
this.formatName = opts.formatName;
|
|
@@ -37,7 +40,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
37
40
|
let videoFormat = opts.videoFormat ?? "auto";
|
|
38
41
|
if (videoFormat === "auto") {
|
|
39
42
|
if (!this.hasSegment) {
|
|
40
|
-
videoFormat = "
|
|
43
|
+
videoFormat = "m4s";
|
|
41
44
|
if (this.formatName === "ts") {
|
|
42
45
|
videoFormat = "ts";
|
|
43
46
|
}
|
|
@@ -47,8 +50,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
this.videoFormat = videoFormat;
|
|
50
|
-
this.
|
|
51
|
-
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "ffmpeg", this.videoFormat, {
|
|
53
|
+
this.streamManager = new StreamManager(opts.getSavePath, this.hasSegment, "ffmpeg", this.videoFormat, {
|
|
52
54
|
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
53
55
|
});
|
|
54
56
|
this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
|
|
@@ -59,8 +61,8 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
59
61
|
this.segment = opts.segment;
|
|
60
62
|
this.headers = opts.headers;
|
|
61
63
|
this.command = this.createCommand();
|
|
62
|
-
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename }) => {
|
|
63
|
-
this.emit("videoFileCreated", { filename, cover, rawFilename });
|
|
64
|
+
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
|
|
65
|
+
this.emit("videoFileCreated", { filename, cover, rawFilename, title });
|
|
64
66
|
});
|
|
65
67
|
this.streamManager.on("videoFileCompleted", ({ filename }) => {
|
|
66
68
|
this.emit("videoFileCompleted", { filename });
|
|
@@ -95,10 +97,11 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
95
97
|
inputOptions.push("-headers", headers.join("\\r\\n"));
|
|
96
98
|
}
|
|
97
99
|
}
|
|
100
|
+
const outputOptions = this.buildOutputOptions();
|
|
98
101
|
const command = createFFMPEGBuilder()
|
|
99
102
|
.input(this.url)
|
|
100
103
|
.inputOptions(inputOptions)
|
|
101
|
-
.outputOptions(
|
|
104
|
+
.outputOptions(outputOptions)
|
|
102
105
|
.output(this.streamManager.videoFilePath)
|
|
103
106
|
.on("error", this.onEnd)
|
|
104
107
|
.on("end", () => this.onEnd("finished"))
|
|
@@ -116,11 +119,31 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
116
119
|
}
|
|
117
120
|
})
|
|
118
121
|
.on("stderr", this.timeoutChecker?.update);
|
|
119
|
-
if (this.hasSegment) {
|
|
120
|
-
command.outputOptions("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
|
|
121
|
-
}
|
|
122
122
|
return command;
|
|
123
123
|
}
|
|
124
|
+
buildOutputOptions() {
|
|
125
|
+
const options = [];
|
|
126
|
+
options.push(...this.ffmpegOutputOptions);
|
|
127
|
+
options.push("-c", "copy", "-movflags", "+frag_keyframe+empty_moov+separate_moof", "-fflags", "+genpts+igndts", "-min_frag_duration", "10000000");
|
|
128
|
+
if (this.segment) {
|
|
129
|
+
if (typeof this.segment === "number") {
|
|
130
|
+
options.push("-f", "segment", "-segment_time", String(this.segment * 60));
|
|
131
|
+
}
|
|
132
|
+
else if (typeof this.segment === "string") {
|
|
133
|
+
options.push("-fs", String(this.segment));
|
|
134
|
+
}
|
|
135
|
+
options.push("-reset_timestamps", "1");
|
|
136
|
+
if (this.videoFormat === "m4s") {
|
|
137
|
+
options.push("-segment_format", "mp4");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (this.videoFormat === "m4s") {
|
|
142
|
+
options.push("-f", "mp4");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return options;
|
|
146
|
+
}
|
|
124
147
|
formatLine(line) {
|
|
125
148
|
if (!line.includes("time=")) {
|
|
126
149
|
return null;
|
|
@@ -160,4 +183,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
160
183
|
getExtraDataController() {
|
|
161
184
|
return this.streamManager?.getExtraDataController();
|
|
162
185
|
}
|
|
186
|
+
get videoFilePath() {
|
|
187
|
+
return this.streamManager.videoFilePath;
|
|
188
|
+
}
|
|
163
189
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { VideoFormat } from "../index.js";
|
|
2
3
|
import type { FormatName } from "./index.js";
|
|
4
|
+
import type { XmlStreamController } from "../xml_stream_controller.js";
|
|
5
|
+
export type Segment = number | string | undefined;
|
|
3
6
|
/**
|
|
4
7
|
* 录制器构造函数选项的基础接口
|
|
5
8
|
*/
|
|
@@ -9,25 +12,23 @@ export interface BaseRecorderOptions {
|
|
|
9
12
|
startTime: number;
|
|
10
13
|
title?: string;
|
|
11
14
|
}) => string;
|
|
12
|
-
segment:
|
|
15
|
+
segment: Segment;
|
|
13
16
|
inputOptions?: string[];
|
|
14
|
-
disableDanma?: boolean;
|
|
15
17
|
formatName: FormatName;
|
|
16
18
|
debugLevel?: "none" | "basic" | "verbose";
|
|
17
19
|
headers?: {
|
|
18
20
|
[key: string]: string | undefined;
|
|
19
21
|
};
|
|
20
|
-
videoFormat?:
|
|
22
|
+
videoFormat?: VideoFormat;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* 录制器接口定义
|
|
24
26
|
*/
|
|
25
|
-
export interface
|
|
27
|
+
export interface IDownloader extends EventEmitter {
|
|
26
28
|
type: "ffmpeg" | "mesio" | "bililive";
|
|
27
29
|
readonly hasSegment: boolean;
|
|
28
|
-
readonly segment:
|
|
30
|
+
readonly segment: Segment;
|
|
29
31
|
readonly inputOptions: string[];
|
|
30
|
-
readonly disableDanma: boolean;
|
|
31
32
|
readonly url: string;
|
|
32
33
|
readonly headers: {
|
|
33
34
|
[key: string]: string | undefined;
|
|
@@ -39,12 +40,14 @@ export interface IRecorder extends EventEmitter {
|
|
|
39
40
|
run(): void;
|
|
40
41
|
stop(): Promise<void>;
|
|
41
42
|
getArguments(): string[];
|
|
42
|
-
getExtraDataController():
|
|
43
|
+
getExtraDataController(): XmlStreamController | null;
|
|
43
44
|
createCommand(): any;
|
|
45
|
+
get videoFilePath(): string;
|
|
44
46
|
on(event: "videoFileCreated", listener: (data: {
|
|
45
47
|
filename: string;
|
|
46
48
|
cover?: string;
|
|
47
49
|
rawFilename?: string;
|
|
50
|
+
title?: string;
|
|
48
51
|
}) => void): this;
|
|
49
52
|
on(event: "videoFileCompleted", listener: (data: {
|
|
50
53
|
filename: string;
|
|
@@ -59,6 +62,7 @@ export interface IRecorder extends EventEmitter {
|
|
|
59
62
|
filename: string;
|
|
60
63
|
cover?: string;
|
|
61
64
|
rawFilename?: string;
|
|
65
|
+
title?: string;
|
|
62
66
|
}): boolean;
|
|
63
67
|
emit(event: "videoFileCompleted", data: {
|
|
64
68
|
filename: string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { FFmpegDownloader } from "./FFmpegDownloader.js";
|
|
2
|
+
import { mesioDownloader } from "./mesioDownloader.js";
|
|
3
|
+
import { BililiveDownloader } from "./BililiveDownloader.js";
|
|
4
|
+
export { FFmpegDownloader } from "./FFmpegDownloader.js";
|
|
5
|
+
export { mesioDownloader } from "./mesioDownloader.js";
|
|
6
|
+
export { BililiveDownloader } from "./BililiveDownloader.js";
|
|
7
|
+
import type { IDownloader, FFMPEGRecorderOptions, MesioRecorderOptions, BililiveRecorderOptions } from "./IDownloader.js";
|
|
8
|
+
/**
|
|
9
|
+
* 录制器类型
|
|
10
|
+
*/
|
|
11
|
+
export type DownloaderType = "ffmpeg" | "mesio" | "bililive";
|
|
12
|
+
export type FormatName = "flv" | "ts" | "fmp4";
|
|
13
|
+
/**
|
|
14
|
+
* 根据录制器类型获取对应的配置选项类型
|
|
15
|
+
*/
|
|
16
|
+
export type RecorderOptions<T extends DownloaderType> = T extends "ffmpeg" ? FFMPEGRecorderOptions : T extends "mesio" ? MesioRecorderOptions : BililiveRecorderOptions;
|
|
17
|
+
/**
|
|
18
|
+
* 根据录制器类型获取对应的录制器实例类型
|
|
19
|
+
*/
|
|
20
|
+
export type RecorderInstance<T extends DownloaderType> = T extends "ffmpeg" ? FFmpegDownloader : T extends "mesio" ? mesioDownloader : BililiveDownloader;
|
|
21
|
+
type RecorderOpts = FFMPEGRecorderOptions | MesioRecorderOptions | BililiveRecorderOptions;
|
|
22
|
+
/**
|
|
23
|
+
* 创建录制器的工厂函数
|
|
24
|
+
*/
|
|
25
|
+
export declare function createBaseDownloader<T extends DownloaderType>(type: T, opts: RecorderOptions<T> & {
|
|
26
|
+
onlyAudio?: boolean;
|
|
27
|
+
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
28
|
+
title?: string;
|
|
29
|
+
cover?: string;
|
|
30
|
+
}>): IDownloader;
|
|
31
|
+
/**
|
|
32
|
+
* 选择录制器
|
|
33
|
+
*/
|
|
34
|
+
export declare function selectRecorder(preferredRecorder: "auto" | DownloaderType | undefined): DownloaderType;
|
|
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 createDownloader(type: "auto" | DownloaderType | undefined, opts: PickPartial<RecorderOpts, "formatName"> & {
|
|
44
|
+
onlyAudio?: boolean;
|
|
45
|
+
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
46
|
+
title?: string;
|
|
47
|
+
cover?: string;
|
|
48
|
+
}>): IDownloader;
|