@bililive-tools/manager 1.12.0 → 1.13.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 +1 -0
- package/lib/downloader/BililiveDownloader.d.ts +2 -0
- package/lib/downloader/BililiveDownloader.js +17 -5
- package/lib/downloader/FFmpegDownloader.js +2 -2
- package/lib/manager.d.ts +3 -1
- package/lib/manager.js +6 -1
- package/lib/recorder.d.ts +2 -0
- package/lib/utils.d.ts +2 -2
- package/lib/utils.js +4 -3
- package/lib/xml_stream_controller.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ declare class BililiveRecorderCommand extends EventEmitter {
|
|
|
12
12
|
inputOptions(...options: string[]): BililiveRecorderCommand;
|
|
13
13
|
_getArguments(): string[];
|
|
14
14
|
run(): void;
|
|
15
|
+
stop(): void;
|
|
15
16
|
kill(): void;
|
|
16
17
|
cut(): void;
|
|
17
18
|
}
|
|
@@ -22,6 +23,7 @@ export declare class BililiveDownloader extends EventEmitter implements IDownloa
|
|
|
22
23
|
type: "bililive";
|
|
23
24
|
private command;
|
|
24
25
|
private streamManager;
|
|
26
|
+
private timeoutChecker;
|
|
25
27
|
readonly hasSegment: boolean;
|
|
26
28
|
readonly getSavePath: (data: {
|
|
27
29
|
startTime: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { DEFAULT_USER_AGENT } from "./index.js";
|
|
4
|
-
import { StreamManager, getBililivePath } from "../index.js";
|
|
4
|
+
import { StreamManager, getBililivePath, utils } from "../index.js";
|
|
5
5
|
import { byte2MB } from "../utils.js";
|
|
6
6
|
// Bililive command builder class similar to ffmpeg
|
|
7
7
|
class BililiveRecorderCommand extends EventEmitter {
|
|
@@ -75,10 +75,14 @@ class BililiveRecorderCommand extends EventEmitter {
|
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
stop() {
|
|
79
79
|
if (this.process) {
|
|
80
80
|
this.process.stdin?.write("q\n");
|
|
81
|
-
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
kill() {
|
|
84
|
+
if (this.process) {
|
|
85
|
+
this.process.kill("SIGINT");
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
cut() {
|
|
@@ -97,6 +101,7 @@ export class BililiveDownloader extends EventEmitter {
|
|
|
97
101
|
type = "bililive";
|
|
98
102
|
command;
|
|
99
103
|
streamManager;
|
|
104
|
+
timeoutChecker;
|
|
100
105
|
hasSegment;
|
|
101
106
|
getSavePath;
|
|
102
107
|
segment;
|
|
@@ -118,6 +123,11 @@ export class BililiveDownloader extends EventEmitter {
|
|
|
118
123
|
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "bililive", videoFormat, {
|
|
119
124
|
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
120
125
|
});
|
|
126
|
+
this.timeoutChecker = utils.createTimeoutChecker(() => {
|
|
127
|
+
this.emit("DebugLog", { type: "error", text: "bililive timeout, killing process" });
|
|
128
|
+
this.command?.kill();
|
|
129
|
+
this.onEnd("bililive timeout");
|
|
130
|
+
}, 20 * 1000, false);
|
|
121
131
|
this.getSavePath = opts.getSavePath;
|
|
122
132
|
this.inputOptions = [];
|
|
123
133
|
this.url = opts.url;
|
|
@@ -138,6 +148,7 @@ export class BililiveDownloader extends EventEmitter {
|
|
|
138
148
|
});
|
|
139
149
|
}
|
|
140
150
|
createCommand() {
|
|
151
|
+
this.timeoutChecker?.start();
|
|
141
152
|
const inputOptions = [...this.inputOptions, "--disable-log-file", "true"];
|
|
142
153
|
if (this.debugLevel === "verbose") {
|
|
143
154
|
inputOptions.push("-l", "Debug");
|
|
@@ -164,6 +175,7 @@ export class BililiveDownloader extends EventEmitter {
|
|
|
164
175
|
.on("error", this.onEnd)
|
|
165
176
|
.on("end", () => this.onEnd("finished"))
|
|
166
177
|
.on("stderr", async (stderrLine) => {
|
|
178
|
+
this.timeoutChecker?.update();
|
|
167
179
|
this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
|
|
168
180
|
await this.streamManager.handleVideoStarted(stderrLine);
|
|
169
181
|
const info = this.formatLine(stderrLine);
|
|
@@ -193,9 +205,9 @@ export class BililiveDownloader extends EventEmitter {
|
|
|
193
205
|
return this.command._getArguments();
|
|
194
206
|
}
|
|
195
207
|
async stop() {
|
|
208
|
+
this.timeoutChecker?.stop();
|
|
196
209
|
try {
|
|
197
|
-
|
|
198
|
-
this.command.kill();
|
|
210
|
+
this.command.stop();
|
|
199
211
|
await this.streamManager.handleVideoCompleted();
|
|
200
212
|
}
|
|
201
213
|
catch (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import { createFFMPEGBuilder, StreamManager, utils } from "../index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createFFmpegInvalidStreamChecker, assert } from "../utils.js";
|
|
4
4
|
import { DEFAULT_USER_AGENT } from "./index.js";
|
|
5
5
|
export class FFmpegDownloader extends EventEmitter {
|
|
6
6
|
onEnd;
|
|
@@ -77,7 +77,7 @@ export class FFmpegDownloader extends EventEmitter {
|
|
|
77
77
|
createCommand() {
|
|
78
78
|
this.timeoutChecker?.start();
|
|
79
79
|
const invalidCount = this.isHls ? 35 : 18;
|
|
80
|
-
const isInvalidStream =
|
|
80
|
+
const isInvalidStream = createFFmpegInvalidStreamChecker(invalidCount);
|
|
81
81
|
const inputOptions = [
|
|
82
82
|
...this.inputOptions,
|
|
83
83
|
"-user_agent",
|
package/lib/manager.d.ts
CHANGED
|
@@ -78,7 +78,9 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
78
78
|
addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: RecorderCreateOpts<E>) => Recorder<E>;
|
|
79
79
|
removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
|
|
80
80
|
getRecorder: (this: RecorderManager<ME, P, PE, E>, id: string) => Recorder<E> | null;
|
|
81
|
-
startRecord: (this: RecorderManager<ME, P, PE, E>, id: string
|
|
81
|
+
startRecord: (this: RecorderManager<ME, P, PE, E>, id: string, opts?: {
|
|
82
|
+
ignoreDataLimit?: boolean;
|
|
83
|
+
}) => Promise<Recorder<E> | undefined>;
|
|
82
84
|
stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
83
85
|
cutRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
84
86
|
autoCheckInterval: number;
|
package/lib/manager.js
CHANGED
|
@@ -198,12 +198,16 @@ export function createRecorderManager(opts) {
|
|
|
198
198
|
const recorder = this.recorders.find((item) => item.id === id);
|
|
199
199
|
return recorder ?? null;
|
|
200
200
|
},
|
|
201
|
-
async startRecord(id) {
|
|
201
|
+
async startRecord(id, iOpts = {}) {
|
|
202
|
+
const { ignoreDataLimit = true } = iOpts;
|
|
202
203
|
const recorder = this.recorders.find((item) => item.id === id);
|
|
203
204
|
if (recorder == null)
|
|
204
205
|
return;
|
|
205
206
|
if (recorder.recordHandle != null)
|
|
206
207
|
return;
|
|
208
|
+
// 如果手动开启且需要判断限制时间,再时间限制内时,就不启动录制
|
|
209
|
+
if (!ignoreDataLimit && isBetweenTimeRange(recorder.handleTime))
|
|
210
|
+
return;
|
|
207
211
|
await recorder.checkLiveStatusAndRecord({
|
|
208
212
|
getSavePath(data) {
|
|
209
213
|
return genSavePathFromRule(manager, recorder, data);
|
|
@@ -321,6 +325,7 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
321
325
|
hour: formatDate(now, "HH"),
|
|
322
326
|
min: formatDate(now, "mm"),
|
|
323
327
|
sec: formatDate(now, "ss"),
|
|
328
|
+
ms: formatDate(now, "SSS"),
|
|
324
329
|
...extData,
|
|
325
330
|
startTime: now,
|
|
326
331
|
owner: owner,
|
package/lib/recorder.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
36
36
|
useM3U8Proxy?: boolean;
|
|
37
37
|
/**B站m3u8代理url */
|
|
38
38
|
m3u8ProxyUrl?: string;
|
|
39
|
+
/** B站自定义Host,用于替换直播流链接中的host */
|
|
40
|
+
customHost?: string;
|
|
39
41
|
/** 流格式 */
|
|
40
42
|
formatName?: FormatName;
|
|
41
43
|
/** 流编码 */
|
package/lib/utils.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare const formatTemplate: (string: string, ...args: any[]) => string;
|
|
|
50
50
|
* "receive invalid aac stream": ADTS无法被解析的flv流
|
|
51
51
|
* "invalid stream": 一段时间内帧数不变
|
|
52
52
|
*/
|
|
53
|
-
export declare function
|
|
53
|
+
export declare function createFFmpegInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => [boolean, string];
|
|
54
54
|
export declare function createTimeoutChecker(onTimeout: () => void, time: number, autoStart?: boolean): {
|
|
55
55
|
update: () => void;
|
|
56
56
|
stop: () => void;
|
|
@@ -125,7 +125,7 @@ declare const _default: {
|
|
|
125
125
|
assertObjectType: typeof assertObjectType;
|
|
126
126
|
asyncThrottle: typeof asyncThrottle;
|
|
127
127
|
isFfmpegStartSegment: typeof isFfmpegStartSegment;
|
|
128
|
-
|
|
128
|
+
createFFmpegInvalidStreamChecker: typeof createFFmpegInvalidStreamChecker;
|
|
129
129
|
createTimeoutChecker: typeof createTimeoutChecker;
|
|
130
130
|
downloadImage: typeof downloadImage;
|
|
131
131
|
md5: (str: string) => string;
|
package/lib/utils.js
CHANGED
|
@@ -117,8 +117,9 @@ export function formatDate(date, format) {
|
|
|
117
117
|
HH: date.getHours().toString().padStart(2, "0"),
|
|
118
118
|
mm: date.getMinutes().toString().padStart(2, "0"),
|
|
119
119
|
ss: date.getSeconds().toString().padStart(2, "0"),
|
|
120
|
+
SSS: date.getMilliseconds().toString().padStart(3, "0"),
|
|
120
121
|
};
|
|
121
|
-
return format.replace(/yyyy|MM|dd|HH|mm|ss/g, (matched) => map[matched]);
|
|
122
|
+
return format.replace(/yyyy|MM|dd|HH|mm|ss|SSS/g, (matched) => map[matched]);
|
|
122
123
|
}
|
|
123
124
|
export function removeSystemReservedChars(str) {
|
|
124
125
|
return filenamify(str, { replacement: "_" });
|
|
@@ -173,7 +174,7 @@ export const formatTemplate = function template(string, ...args) {
|
|
|
173
174
|
* "receive invalid aac stream": ADTS无法被解析的flv流
|
|
174
175
|
* "invalid stream": 一段时间内帧数不变
|
|
175
176
|
*/
|
|
176
|
-
export function
|
|
177
|
+
export function createFFmpegInvalidStreamChecker(count = 15) {
|
|
177
178
|
let prevFrame = 0;
|
|
178
179
|
let frameUnchangedCount = 0;
|
|
179
180
|
return (ffmpegLogLine) => {
|
|
@@ -504,7 +505,7 @@ export default {
|
|
|
504
505
|
assertObjectType,
|
|
505
506
|
asyncThrottle,
|
|
506
507
|
isFfmpegStartSegment,
|
|
507
|
-
|
|
508
|
+
createFFmpegInvalidStreamChecker,
|
|
508
509
|
createTimeoutChecker,
|
|
509
510
|
downloadImage,
|
|
510
511
|
md5,
|
|
@@ -23,7 +23,7 @@ export function createRecordExtraDataController(savePath) {
|
|
|
23
23
|
isInitialized = true;
|
|
24
24
|
try {
|
|
25
25
|
// 创建XML文件头,使用占位符预留metadata位置
|
|
26
|
-
const header = `<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet type="text/xsl" href="#s"?>\n<i>\n<!--METADATA_PLACEHOLDER-->\n<
|
|
26
|
+
const header = `<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet type="text/xsl" href="#s"?>\n<i>\n<!--METADATA_PLACEHOLDER-->\n<RecorderXmlStyle><z:stylesheet version="1.0" id="s" xml:id="s" xmlns:z="http://www.w3.org/1999/XSL/Transform"><z:output method="html"/><z:template match="/"><html><meta name="viewport" content="width=device-width"/><title>弹幕文件 <z:value-of select="/i/metadata/user_name/text()"/></title><style>body{margin:0}h1,h2,p,table{margin-left:5px}table{border-spacing:0}td,th{border:1px solid grey;padding:1px 5px}th{position:sticky;top:0;background:#4098de}tr:hover{background:#d9f4ff}div{overflow:auto;max-height:80vh;max-width:100vw;width:fit-content}</style><h1>弹幕XML文件</h1><p>本文件不支持在 IE 浏览器里预览,请使用 Chrome Firefox Edge 等浏览器。</p><p>文件用法参考文档 <a href="https://rec.danmuji.org/user/danmaku/">https://rec.danmuji.org/user/danmaku/</a></p><table><tr><td>房间号</td><td><z:value-of select="/i/metadata/room_id/text()"/></td></tr><tr><td>主播名</td><td><z:value-of select="/i/metadata/user_name/text()"/></td></tr><tr><td><a href="#d">弹幕</a></td><td>共<z:value-of select="count(/i/d)"/>条记录</td></tr><tr><td><a href="#guard">上船</a></td><td>共<z:value-of select="count(/i/guard)"/>条记录</td></tr><tr><td><a href="#sc">SC</a></td><td>共<z:value-of select="count(/i/sc)"/>条记录</td></tr><tr><td><a href="#gift">礼物</a></td><td>共<z:value-of select="count(/i/gift)"/>条记录</td></tr></table><h2 id="d">弹幕</h2><div id="dm"><table><tr><th>用户名</th><th>出现时间</th><th>用户ID</th><th>弹幕</th><th>参数</th></tr><z:for-each select="/i/d"><tr><td><z:value-of select="@user"/></td><td></td><td></td><td><z:value-of select="."/></td><td><z:value-of select="@p"/></td></tr></z:for-each></table></div><script>Array.from(document.querySelectorAll('#dm tr')).slice(1).map(t=>t.querySelectorAll('td')).forEach(t=>{let p=t[4].textContent.split(','),a=p[0];t[1].textContent=\`\u0024{(Math.floor(a/60/60)+'').padStart(2,0)}:\u0024{(Math.floor(a/60%60)+'').padStart(2,0)}:\u0024{(a%60).toFixed(3).padStart(6,0)}\`;t[2].innerHTML=\`<a target=_blank rel="nofollow noreferrer" ">\u0024{p[6]}</a>\`})</script><h2 id="guard">舰长购买</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select="/i/guard"><tr><td><z:value-of select="@user"/></td><td><a rel="nofollow noreferrer"><z:attribute name="href"><z:text></z:text><z:value-of select="@uid" /></z:attribute><z:value-of select="@uid"/></a></td><td><z:value-of select="@level"/></td><td><z:value-of select="@count"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="sc">SuperChat 醒目留言</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select="/i/sc"><tr><td><z:value-of select="@user"/></td><td><a rel="nofollow noreferrer"><z:attribute name="href"><z:text></z:text><z:value-of select="@uid" /></z:attribute><z:value-of select="@uid"/></a></td><td><z:value-of select="."/></td><td><z:value-of select="@time"/></td><td><z:value-of select="@price"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="gift">礼物</h2><div><table><tr><th>用户名</th><th>用户ID</th><th>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select="/i/gift"><tr><td><z:value-of select="@user"/></td><td><span rel="nofollow noreferrer"><z:attribute name="href"></z:attribute><z:value-of select="@uid"/></span></td><td><z:value-of select="@giftname"/></td><td><z:value-of select="@giftcount"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div></html></z:template></z:stylesheet></RecorderXmlStyle>`;
|
|
27
27
|
await fs.promises.writeFile(savePath, header);
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|