@bililive-tools/manager 1.9.0 → 1.11.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/lib/cache.d.ts +55 -16
- package/lib/cache.js +61 -33
- package/lib/{recorder/BililiveRecorder.d.ts → downloader/BililiveDownloader.d.ts} +7 -4
- package/lib/{recorder/BililiveRecorder.js → downloader/BililiveDownloader.js} +32 -14
- package/lib/{recorder/FFMPEGRecorder.d.ts → downloader/FFmpegDownloader.d.ts} +5 -3
- package/lib/{recorder/FFMPEGRecorder.js → downloader/FFmpegDownloader.js} +25 -8
- package/lib/{recorder/IRecorder.d.ts → downloader/IDownloader.d.ts} +6 -3
- package/lib/downloader/index.d.ts +49 -0
- package/lib/{recorder → downloader}/index.js +17 -13
- package/lib/{recorder/mesioRecorder.d.ts → downloader/mesioDownloader.d.ts} +6 -4
- package/lib/{recorder/mesioRecorder.js → downloader/mesioDownloader.js} +27 -17
- package/lib/{recorder → downloader}/streamManager.d.ts +5 -5
- package/lib/{recorder → downloader}/streamManager.js +5 -16
- package/lib/index.d.ts +5 -2
- package/lib/index.js +4 -2
- package/lib/manager.d.ts +10 -7
- package/lib/manager.js +25 -29
- package/lib/record_extra_data_controller.d.ts +1 -1
- package/lib/recorder.d.ts +13 -15
- package/lib/utils.d.ts +30 -0
- package/lib/utils.js +129 -0
- package/lib/xml_stream_controller.d.ts +1 -1
- 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/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
|
}
|
|
@@ -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;
|
|
@@ -12,10 +12,11 @@ declare class BililiveRecorderCommand extends EventEmitter {
|
|
|
12
12
|
inputOptions(...options: string[]): BililiveRecorderCommand;
|
|
13
13
|
_getArguments(): string[];
|
|
14
14
|
run(): void;
|
|
15
|
-
kill(
|
|
15
|
+
kill(): void;
|
|
16
|
+
cut(): void;
|
|
16
17
|
}
|
|
17
18
|
export declare const createBililiveBuilder: () => BililiveRecorderCommand;
|
|
18
|
-
export declare class
|
|
19
|
+
export declare class BililiveDownloader extends EventEmitter implements IDownloader {
|
|
19
20
|
private onEnd;
|
|
20
21
|
private onUpdateLiveInfo;
|
|
21
22
|
type: "bililive";
|
|
@@ -26,7 +27,7 @@ export declare class BililiveRecorder extends EventEmitter implements IRecorder
|
|
|
26
27
|
startTime: number;
|
|
27
28
|
title?: string;
|
|
28
29
|
}) => string;
|
|
29
|
-
readonly segment:
|
|
30
|
+
readonly segment: Segment;
|
|
30
31
|
readonly inputOptions: string[];
|
|
31
32
|
readonly disableDanma: boolean;
|
|
32
33
|
readonly url: string;
|
|
@@ -46,5 +47,7 @@ export declare class BililiveRecorder extends EventEmitter implements IRecorder
|
|
|
46
47
|
getArguments(): string[];
|
|
47
48
|
stop(): Promise<void>;
|
|
48
49
|
getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
|
|
50
|
+
get videoFilePath(): string;
|
|
51
|
+
cut(): void;
|
|
49
52
|
}
|
|
50
53
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { DEFAULT_USER_AGENT } from "./index.js";
|
|
3
4
|
import { StreamManager, getBililivePath } from "../index.js";
|
|
5
|
+
import { byte2MB } from "../utils.js";
|
|
4
6
|
// Bililive command builder class similar to ffmpeg
|
|
5
7
|
class BililiveRecorderCommand extends EventEmitter {
|
|
6
8
|
_input = "";
|
|
@@ -63,7 +65,6 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
63
65
|
this.process.on("error", (error) => {
|
|
64
66
|
this.emit("error", error);
|
|
65
67
|
});
|
|
66
|
-
[];
|
|
67
68
|
this.process.on("close", (code) => {
|
|
68
69
|
if (code === 0) {
|
|
69
70
|
this.emit("end");
|
|
@@ -73,9 +74,15 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
|
-
kill(
|
|
77
|
+
kill() {
|
|
77
78
|
if (this.process) {
|
|
78
|
-
this.process.
|
|
79
|
+
this.process.stdin?.write("q\n");
|
|
80
|
+
// this.process.kill("SIGTERM");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
cut() {
|
|
84
|
+
if (this.process) {
|
|
85
|
+
this.process.stdin?.write("s\n");
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
}
|
|
@@ -83,7 +90,7 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
83
90
|
export const createBililiveBuilder = () => {
|
|
84
91
|
return new BililiveRecorderCommand();
|
|
85
92
|
};
|
|
86
|
-
export class
|
|
93
|
+
export class BililiveDownloader extends EventEmitter {
|
|
87
94
|
onEnd;
|
|
88
95
|
onUpdateLiveInfo;
|
|
89
96
|
type = "bililive";
|
|
@@ -101,19 +108,23 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
101
108
|
super();
|
|
102
109
|
this.onEnd = onEnd;
|
|
103
110
|
this.onUpdateLiveInfo = onUpdateLiveInfo;
|
|
111
|
+
// 存在自动分段,永远为true
|
|
104
112
|
const hasSegment = true;
|
|
113
|
+
this.hasSegment = hasSegment;
|
|
105
114
|
this.disableDanma = opts.disableDanma ?? false;
|
|
106
115
|
this.debugLevel = opts.debugLevel ?? "none";
|
|
107
116
|
let videoFormat = "flv";
|
|
108
117
|
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "bililive", videoFormat, {
|
|
109
118
|
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
110
119
|
});
|
|
111
|
-
this.hasSegment = hasSegment;
|
|
112
120
|
this.getSavePath = opts.getSavePath;
|
|
113
121
|
this.inputOptions = [];
|
|
114
122
|
this.url = opts.url;
|
|
115
123
|
this.segment = opts.segment;
|
|
116
|
-
this.headers =
|
|
124
|
+
this.headers = {
|
|
125
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
126
|
+
...(opts.headers || {}),
|
|
127
|
+
};
|
|
117
128
|
this.command = this.createCommand();
|
|
118
129
|
this.streamManager.on("videoFileCreated", ({ filename, cover, rawFilename, title }) => {
|
|
119
130
|
this.emit("videoFileCreated", { filename, cover, rawFilename, title });
|
|
@@ -126,11 +137,7 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
126
137
|
});
|
|
127
138
|
}
|
|
128
139
|
createCommand() {
|
|
129
|
-
const inputOptions = [
|
|
130
|
-
...this.inputOptions,
|
|
131
|
-
"-h",
|
|
132
|
-
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
|
|
133
|
-
];
|
|
140
|
+
const inputOptions = [...this.inputOptions, "--disable-log-file", "true"];
|
|
134
141
|
if (this.debugLevel === "verbose") {
|
|
135
142
|
inputOptions.push("-l", "Debug");
|
|
136
143
|
}
|
|
@@ -141,8 +148,13 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
141
148
|
inputOptions.push("-h", `${key}: ${value}`);
|
|
142
149
|
});
|
|
143
150
|
}
|
|
144
|
-
if (this.
|
|
145
|
-
|
|
151
|
+
if (this.segment) {
|
|
152
|
+
if (typeof this.segment === "number") {
|
|
153
|
+
inputOptions.push("-d", `${this.segment}`);
|
|
154
|
+
}
|
|
155
|
+
else if (typeof this.segment === "string") {
|
|
156
|
+
inputOptions.push("-m", byte2MB(Number(this.segment)).toFixed(2));
|
|
157
|
+
}
|
|
146
158
|
}
|
|
147
159
|
const command = createBililiveBuilder()
|
|
148
160
|
.input(this.url)
|
|
@@ -182,7 +194,7 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
182
194
|
async stop() {
|
|
183
195
|
try {
|
|
184
196
|
// 直接发送SIGINT信号,会导致数据丢失
|
|
185
|
-
this.command.kill(
|
|
197
|
+
this.command.kill();
|
|
186
198
|
await this.streamManager.handleVideoCompleted();
|
|
187
199
|
}
|
|
188
200
|
catch (err) {
|
|
@@ -192,4 +204,10 @@ export class BililiveRecorder extends EventEmitter {
|
|
|
192
204
|
getExtraDataController() {
|
|
193
205
|
return this.streamManager?.getExtraDataController();
|
|
194
206
|
}
|
|
207
|
+
get videoFilePath() {
|
|
208
|
+
return this.streamManager.videoFilePath;
|
|
209
|
+
}
|
|
210
|
+
cut() {
|
|
211
|
+
this.command.cut();
|
|
212
|
+
}
|
|
195
213
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
-
import {
|
|
2
|
+
import { IDownloader, FFMPEGRecorderOptions, Segment } from "./IDownloader.js";
|
|
3
3
|
import { FormatName } from "./index.js";
|
|
4
4
|
import type { VideoFormat } from "../index.js";
|
|
5
|
-
export declare class
|
|
5
|
+
export declare class FFmpegDownloader extends EventEmitter implements IDownloader {
|
|
6
6
|
private onEnd;
|
|
7
7
|
private onUpdateLiveInfo;
|
|
8
8
|
type: "ffmpeg";
|
|
@@ -14,7 +14,7 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
14
14
|
startTime: number;
|
|
15
15
|
title?: string;
|
|
16
16
|
}) => string;
|
|
17
|
-
readonly segment:
|
|
17
|
+
readonly segment: Segment;
|
|
18
18
|
ffmpegOutputOptions: string[];
|
|
19
19
|
readonly inputOptions: string[];
|
|
20
20
|
readonly isHls: boolean;
|
|
@@ -39,4 +39,6 @@ export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
|
39
39
|
getArguments(): string[];
|
|
40
40
|
stop(): Promise<void>;
|
|
41
41
|
getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
|
|
42
|
+
get videoFilePath(): string;
|
|
43
|
+
cut(): void;
|
|
42
44
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
-
|
|
4
|
+
import { DEFAULT_USER_AGENT } from "./index.js";
|
|
5
|
+
export class FFmpegDownloader extends EventEmitter {
|
|
5
6
|
onEnd;
|
|
6
7
|
onUpdateLiveInfo;
|
|
7
8
|
type = "ffmpeg";
|
|
@@ -24,7 +25,11 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
24
25
|
super();
|
|
25
26
|
this.onEnd = onEnd;
|
|
26
27
|
this.onUpdateLiveInfo = onUpdateLiveInfo;
|
|
27
|
-
|
|
28
|
+
let hasSegment = false;
|
|
29
|
+
// 只有数字才表示时间分段,只有时间分段才会在ffmpeg走分段逻辑
|
|
30
|
+
if (opts.segment && typeof opts.segment === "number") {
|
|
31
|
+
hasSegment = true;
|
|
32
|
+
}
|
|
28
33
|
this.hasSegment = hasSegment;
|
|
29
34
|
this.debugLevel = opts.debugLevel ?? "none";
|
|
30
35
|
this.formatName = opts.formatName;
|
|
@@ -48,7 +53,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
48
53
|
}
|
|
49
54
|
this.videoFormat = videoFormat;
|
|
50
55
|
this.disableDanma = opts.disableDanma ?? false;
|
|
51
|
-
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "ffmpeg", this.videoFormat, {
|
|
56
|
+
this.streamManager = new StreamManager(opts.getSavePath, this.hasSegment, this.disableDanma, "ffmpeg", this.videoFormat, {
|
|
52
57
|
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
53
58
|
});
|
|
54
59
|
this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
|
|
@@ -76,7 +81,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
76
81
|
const inputOptions = [
|
|
77
82
|
...this.inputOptions,
|
|
78
83
|
"-user_agent",
|
|
79
|
-
"
|
|
84
|
+
this.headers?.["User-Agent"] ?? DEFAULT_USER_AGENT,
|
|
80
85
|
];
|
|
81
86
|
if (this.isHls) {
|
|
82
87
|
inputOptions.push(...["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "3"]);
|
|
@@ -87,8 +92,8 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
87
92
|
if (this.headers) {
|
|
88
93
|
const headers = [];
|
|
89
94
|
Object.entries(this.headers).forEach(([key, value]) => {
|
|
90
|
-
if (!value)
|
|
91
|
-
return;
|
|
95
|
+
if (!value || key === "User-Agent")
|
|
96
|
+
return; // User-Agent单独处理
|
|
92
97
|
headers.push(`${key}:${value}`);
|
|
93
98
|
});
|
|
94
99
|
if (headers.length) {
|
|
@@ -123,8 +128,14 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
123
128
|
const options = [];
|
|
124
129
|
options.push(...this.ffmpegOutputOptions);
|
|
125
130
|
options.push("-c", "copy", "-movflags", "+frag_keyframe+empty_moov+separate_moof", "-fflags", "+genpts+igndts", "-min_frag_duration", "10000000");
|
|
126
|
-
if (this.
|
|
127
|
-
|
|
131
|
+
if (this.segment) {
|
|
132
|
+
if (typeof this.segment === "number") {
|
|
133
|
+
options.push("-f", "segment", "-segment_time", String(this.segment * 60));
|
|
134
|
+
}
|
|
135
|
+
else if (typeof this.segment === "string") {
|
|
136
|
+
options.push("-fs", String(this.segment));
|
|
137
|
+
}
|
|
138
|
+
options.push("-reset_timestamps", "1");
|
|
128
139
|
if (this.videoFormat === "m4s") {
|
|
129
140
|
options.push("-segment_format", "mp4");
|
|
130
141
|
}
|
|
@@ -175,4 +186,10 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
175
186
|
getExtraDataController() {
|
|
176
187
|
return this.streamManager?.getExtraDataController();
|
|
177
188
|
}
|
|
189
|
+
get videoFilePath() {
|
|
190
|
+
return this.streamManager.videoFilePath;
|
|
191
|
+
}
|
|
192
|
+
cut() {
|
|
193
|
+
throw new Error("FFmpeg downloader does not support cut operation.");
|
|
194
|
+
}
|
|
178
195
|
}
|
|
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import type { VideoFormat } from "../index.js";
|
|
3
3
|
import type { FormatName } from "./index.js";
|
|
4
4
|
import type { XmlStreamController } from "../xml_stream_controller.js";
|
|
5
|
+
export type Segment = number | string | undefined;
|
|
5
6
|
/**
|
|
6
7
|
* 录制器构造函数选项的基础接口
|
|
7
8
|
*/
|
|
@@ -11,7 +12,7 @@ export interface BaseRecorderOptions {
|
|
|
11
12
|
startTime: number;
|
|
12
13
|
title?: string;
|
|
13
14
|
}) => string;
|
|
14
|
-
segment:
|
|
15
|
+
segment: Segment;
|
|
15
16
|
inputOptions?: string[];
|
|
16
17
|
disableDanma?: boolean;
|
|
17
18
|
formatName: FormatName;
|
|
@@ -24,10 +25,10 @@ export interface BaseRecorderOptions {
|
|
|
24
25
|
/**
|
|
25
26
|
* 录制器接口定义
|
|
26
27
|
*/
|
|
27
|
-
export interface
|
|
28
|
+
export interface IDownloader extends EventEmitter {
|
|
28
29
|
type: "ffmpeg" | "mesio" | "bililive";
|
|
29
30
|
readonly hasSegment: boolean;
|
|
30
|
-
readonly segment:
|
|
31
|
+
readonly segment: Segment;
|
|
31
32
|
readonly inputOptions: string[];
|
|
32
33
|
readonly disableDanma: boolean;
|
|
33
34
|
readonly url: string;
|
|
@@ -40,9 +41,11 @@ export interface IRecorder extends EventEmitter {
|
|
|
40
41
|
}) => string;
|
|
41
42
|
run(): void;
|
|
42
43
|
stop(): Promise<void>;
|
|
44
|
+
cut(): void;
|
|
43
45
|
getArguments(): string[];
|
|
44
46
|
getExtraDataController(): XmlStreamController | null;
|
|
45
47
|
createCommand(): any;
|
|
48
|
+
get videoFilePath(): string;
|
|
46
49
|
on(event: "videoFileCreated", listener: (data: {
|
|
47
50
|
filename: string;
|
|
48
51
|
cover?: string;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";
|
|
23
|
+
/**
|
|
24
|
+
* 创建录制器的工厂函数
|
|
25
|
+
*/
|
|
26
|
+
export declare function createBaseDownloader<T extends DownloaderType>(type: T, opts: RecorderOptions<T> & {
|
|
27
|
+
onlyAudio?: boolean;
|
|
28
|
+
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
29
|
+
title?: string;
|
|
30
|
+
cover?: string;
|
|
31
|
+
}>): IDownloader;
|
|
32
|
+
/**
|
|
33
|
+
* 选择录制器
|
|
34
|
+
*/
|
|
35
|
+
export declare function selectRecorder(preferredRecorder: "auto" | DownloaderType | undefined): DownloaderType;
|
|
36
|
+
/**
|
|
37
|
+
* 判断原始录制流格式,flv, ts, m4s
|
|
38
|
+
*/
|
|
39
|
+
export declare function getSourceFormatName(streamUrl: string, formatName: FormatName | undefined): FormatName;
|
|
40
|
+
type PickPartial<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>;
|
|
41
|
+
/**
|
|
42
|
+
* 创建录制器的工厂函数
|
|
43
|
+
*/
|
|
44
|
+
export declare function createDownloader(type: "auto" | DownloaderType | undefined, opts: PickPartial<RecorderOpts, "formatName"> & {
|
|
45
|
+
onlyAudio?: boolean;
|
|
46
|
+
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
47
|
+
title?: string;
|
|
48
|
+
cover?: string;
|
|
49
|
+
}>): IDownloader;
|
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
1
|
+
import { FFmpegDownloader } from "./FFmpegDownloader.js";
|
|
2
|
+
import { mesioDownloader } from "./mesioDownloader.js";
|
|
3
|
+
import { BililiveDownloader } from "./BililiveDownloader.js";
|
|
4
|
+
import { parseSizeToBytes } from "../utils.js";
|
|
5
|
+
export { FFmpegDownloader } from "./FFmpegDownloader.js";
|
|
6
|
+
export { mesioDownloader } from "./mesioDownloader.js";
|
|
7
|
+
export { BililiveDownloader } from "./BililiveDownloader.js";
|
|
8
|
+
export const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";
|
|
7
9
|
/**
|
|
8
10
|
* 创建录制器的工厂函数
|
|
9
11
|
*/
|
|
10
|
-
export function
|
|
12
|
+
export function createBaseDownloader(type, opts, onEnd, onUpdateLiveInfo) {
|
|
13
|
+
const segment = parseSizeToBytes(String(opts.segment));
|
|
14
|
+
const newOpts = { ...opts, segment };
|
|
11
15
|
if (type === "ffmpeg") {
|
|
12
|
-
return new
|
|
16
|
+
return new FFmpegDownloader(newOpts, onEnd, onUpdateLiveInfo);
|
|
13
17
|
}
|
|
14
18
|
else if (type === "mesio") {
|
|
15
|
-
return new
|
|
19
|
+
return new mesioDownloader(newOpts, onEnd, onUpdateLiveInfo);
|
|
16
20
|
}
|
|
17
21
|
else if (type === "bililive") {
|
|
18
22
|
if (opts.formatName === "flv") {
|
|
19
23
|
// 录播姬引擎不支持只录音频
|
|
20
24
|
if (!opts.onlyAudio) {
|
|
21
|
-
return new
|
|
25
|
+
return new BililiveDownloader(newOpts, onEnd, onUpdateLiveInfo);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
|
-
return new
|
|
28
|
+
return new FFmpegDownloader(newOpts, onEnd, onUpdateLiveInfo);
|
|
25
29
|
}
|
|
26
30
|
else {
|
|
27
31
|
throw new Error(`Unsupported recorder type: ${type}`);
|
|
@@ -71,8 +75,8 @@ export function getSourceFormatName(streamUrl, formatName) {
|
|
|
71
75
|
/**
|
|
72
76
|
* 创建录制器的工厂函数
|
|
73
77
|
*/
|
|
74
|
-
export function
|
|
78
|
+
export function createDownloader(type, opts, onEnd, onUpdateLiveInfo) {
|
|
75
79
|
const recorderType = selectRecorder(type);
|
|
76
80
|
const sourceFormatName = getSourceFormatName(opts.url, opts.formatName);
|
|
77
|
-
return
|
|
81
|
+
return createBaseDownloader(recorderType, { ...opts, formatName: sourceFormatName }, onEnd, onUpdateLiveInfo);
|
|
78
82
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
-
import {
|
|
2
|
+
import { IDownloader, MesioRecorderOptions, Segment } from "./IDownloader.js";
|
|
3
3
|
declare class MesioCommand extends EventEmitter {
|
|
4
4
|
private _input;
|
|
5
5
|
private _output;
|
|
@@ -12,10 +12,10 @@ declare class MesioCommand extends EventEmitter {
|
|
|
12
12
|
inputOptions(...options: string[]): MesioCommand;
|
|
13
13
|
_getArguments(): string[];
|
|
14
14
|
run(): void;
|
|
15
|
-
kill(
|
|
15
|
+
kill(): void;
|
|
16
16
|
}
|
|
17
17
|
export declare const createMesioBuilder: () => MesioCommand;
|
|
18
|
-
export declare class
|
|
18
|
+
export declare class mesioDownloader extends EventEmitter implements IDownloader {
|
|
19
19
|
private onEnd;
|
|
20
20
|
private onUpdateLiveInfo;
|
|
21
21
|
type: "mesio";
|
|
@@ -26,7 +26,7 @@ export declare class MesioRecorder 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
31
|
readonly disableDanma: boolean;
|
|
32
32
|
readonly url: string;
|
|
@@ -43,5 +43,7 @@ export declare class MesioRecorder extends EventEmitter implements IRecorder {
|
|
|
43
43
|
getArguments(): string[];
|
|
44
44
|
stop(): Promise<void>;
|
|
45
45
|
getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
|
|
46
|
+
get videoFilePath(): string;
|
|
47
|
+
cut(): void;
|
|
46
48
|
}
|
|
47
49
|
export {};
|