@bililive-tools/manager 1.17.2 → 1.17.3
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/manager.js +71 -1
- package/lib/recorder.d.ts +6 -0
- package/package.json +1 -1
package/lib/manager.js
CHANGED
|
@@ -8,6 +8,14 @@ import { getBiliStatusInfoByRoomIds } from "./api.js";
|
|
|
8
8
|
import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, sleep, replaceFourByteUnicode, } from "./utils.js";
|
|
9
9
|
import { StreamManager } from "./downloader/streamManager.js";
|
|
10
10
|
const RECORDER_TIMELINE_LIMIT = 40;
|
|
11
|
+
const SHORT_RECORDING_THRESHOLD = 12 * 1000;
|
|
12
|
+
const SHORT_RECORDING_RETRY_COUNT = 2;
|
|
13
|
+
const SHORT_RECORDING_REASON_BLACKLIST = new Set([
|
|
14
|
+
"manual stop",
|
|
15
|
+
"remove recorder",
|
|
16
|
+
"直播间标题包含关键词",
|
|
17
|
+
"标题变更分段",
|
|
18
|
+
]);
|
|
11
19
|
function normalizeTimelineText(text) {
|
|
12
20
|
return text.trim();
|
|
13
21
|
}
|
|
@@ -63,6 +71,18 @@ function isConfigurableProp(prop) {
|
|
|
63
71
|
}
|
|
64
72
|
export function createRecorderManager(opts) {
|
|
65
73
|
const recorders = [];
|
|
74
|
+
// 短录制检测仅用于当前进程,不需要持久化。
|
|
75
|
+
const recordAttempts = new Map();
|
|
76
|
+
const shortRecordingStates = new Map();
|
|
77
|
+
const getStreamRetryHint = (recorder) => {
|
|
78
|
+
const state = shortRecordingStates.get(recorder.id);
|
|
79
|
+
if (!state?.preferAlternativeStream)
|
|
80
|
+
return undefined;
|
|
81
|
+
return {
|
|
82
|
+
liveId: state.liveId,
|
|
83
|
+
preferAlternativeStream: true,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
66
86
|
// 存储每个 provider 的 timer,key 为 providerId
|
|
67
87
|
const checkLoopTimers = new Map();
|
|
68
88
|
const multiThreadCheck = async (manager, providerId, providerConfig) => {
|
|
@@ -78,6 +98,7 @@ export function createRecorderManager(opts) {
|
|
|
78
98
|
return genSavePathFromRule(manager, recorder, data);
|
|
79
99
|
},
|
|
80
100
|
banLiveId: tempBanObj[recorder.channelId],
|
|
101
|
+
streamRetryHint: getStreamRetryHint(recorder),
|
|
81
102
|
});
|
|
82
103
|
}
|
|
83
104
|
}
|
|
@@ -120,6 +141,7 @@ export function createRecorderManager(opts) {
|
|
|
120
141
|
return genSavePathFromRule(manager, recorder, data);
|
|
121
142
|
},
|
|
122
143
|
banLiveId,
|
|
144
|
+
streamRetryHint: getStreamRetryHint(recorder),
|
|
123
145
|
});
|
|
124
146
|
};
|
|
125
147
|
threads.push(...range(0, providerConfig.maxThreadCount).map(async () => {
|
|
@@ -170,6 +192,21 @@ export function createRecorderManager(opts) {
|
|
|
170
192
|
ensureRecorderTimeline(recorder);
|
|
171
193
|
this.recorders.push(recorder);
|
|
172
194
|
recorder.on("RecordStart", (recordHandle) => {
|
|
195
|
+
const liveId = recorder.liveInfo?.liveId;
|
|
196
|
+
recordAttempts.set(recordHandle.id, {
|
|
197
|
+
recorderId: recorder.id,
|
|
198
|
+
liveId,
|
|
199
|
+
startedAt: Date.now(),
|
|
200
|
+
});
|
|
201
|
+
const shortRecordingState = shortRecordingStates.get(recorder.id);
|
|
202
|
+
if (shortRecordingState) {
|
|
203
|
+
if (shortRecordingState.liveId === liveId) {
|
|
204
|
+
shortRecordingState.preferAlternativeStream = false;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
shortRecordingStates.delete(recorder.id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
173
210
|
recorder.appendTimeline?.({
|
|
174
211
|
startTime: recorder.liveInfo?.recordStartTime?.getTime() ?? Date.now(),
|
|
175
212
|
text: `开始录制:${recordHandle.stream}/${recordHandle.source}/${recordHandle.recorderType}/${recordHandle.url}`,
|
|
@@ -199,10 +236,36 @@ export function createRecorderManager(opts) {
|
|
|
199
236
|
recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder: recorder.toJSON(), keys }));
|
|
200
237
|
recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
|
|
201
238
|
recorder.on("RecordStop", ({ recordHandle, reason }) => {
|
|
239
|
+
const stoppedAt = Date.now();
|
|
202
240
|
recorder.appendTimeline?.({
|
|
203
|
-
startTime:
|
|
241
|
+
startTime: stoppedAt,
|
|
204
242
|
text: reason ? `停止录制:${reason}` : "停止录制",
|
|
205
243
|
});
|
|
244
|
+
const attempt = recordAttempts.get(recordHandle.id);
|
|
245
|
+
recordAttempts.delete(recordHandle.id);
|
|
246
|
+
if (attempt?.recorderId === recorder.id && attempt.liveId) {
|
|
247
|
+
const isReasonBlacklisted = reason != null && SHORT_RECORDING_REASON_BLACKLIST.has(reason);
|
|
248
|
+
const duration = stoppedAt - attempt.startedAt;
|
|
249
|
+
const previousState = shortRecordingStates.get(recorder.id);
|
|
250
|
+
if (isReasonBlacklisted || duration >= SHORT_RECORDING_THRESHOLD) {
|
|
251
|
+
shortRecordingStates.delete(recorder.id);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const count = previousState?.liveId === attempt.liveId ? previousState.count + 1 : 1;
|
|
255
|
+
const preferAlternativeStream = count >= SHORT_RECORDING_RETRY_COUNT;
|
|
256
|
+
shortRecordingStates.set(recorder.id, {
|
|
257
|
+
liveId: attempt.liveId,
|
|
258
|
+
count,
|
|
259
|
+
preferAlternativeStream,
|
|
260
|
+
});
|
|
261
|
+
if (preferAlternativeStream && !previousState?.preferAlternativeStream) {
|
|
262
|
+
recorder.appendTimeline?.({
|
|
263
|
+
startTime: stoppedAt,
|
|
264
|
+
text: `连续 ${SHORT_RECORDING_RETRY_COUNT} 次录制在 ${SHORT_RECORDING_THRESHOLD / 1000} 秒内结束,下次可能尝试切换流格式(仅抖音)`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
206
269
|
this.emit("RecordStop", { recorder: recorder.toJSON(), recordHandle, reason });
|
|
207
270
|
const maxRetryCount = 10;
|
|
208
271
|
// 默认策略下,如果录制被中断,那么会在下一个检查周期时重新检查直播状态并重新开始录制,这种策略的问题就是一部分时间会被漏掉。
|
|
@@ -241,6 +304,7 @@ export function createRecorderManager(opts) {
|
|
|
241
304
|
getSavePath(data) {
|
|
242
305
|
return genSavePathFromRule(manager, recorder, data);
|
|
243
306
|
},
|
|
307
|
+
streamRetryHint: getStreamRetryHint(recorder),
|
|
244
308
|
});
|
|
245
309
|
}, 1000);
|
|
246
310
|
}
|
|
@@ -296,6 +360,11 @@ export function createRecorderManager(opts) {
|
|
|
296
360
|
if (k.startsWith(keyPrefix))
|
|
297
361
|
delete chargeLiveObj[k];
|
|
298
362
|
});
|
|
363
|
+
shortRecordingStates.delete(recorder.id);
|
|
364
|
+
for (const [recordHandleId, attempt] of recordAttempts) {
|
|
365
|
+
if (attempt.recorderId === recorder.id)
|
|
366
|
+
recordAttempts.delete(recordHandleId);
|
|
367
|
+
}
|
|
299
368
|
this.emit("RecorderRemoved", recorder.toJSON());
|
|
300
369
|
},
|
|
301
370
|
getRecorder(id) {
|
|
@@ -317,6 +386,7 @@ export function createRecorderManager(opts) {
|
|
|
317
386
|
return genSavePathFromRule(manager, recorder, data);
|
|
318
387
|
},
|
|
319
388
|
isManualStart: true,
|
|
389
|
+
streamRetryHint: getStreamRetryHint(recorder),
|
|
320
390
|
});
|
|
321
391
|
delete tempBanObj[recorder.channelId];
|
|
322
392
|
recorder.tempStopIntervalCheck = false;
|
package/lib/recorder.d.ts
CHANGED
|
@@ -87,6 +87,10 @@ export interface AppendRecorderTimelineArgs {
|
|
|
87
87
|
startTime?: number;
|
|
88
88
|
text: string;
|
|
89
89
|
}
|
|
90
|
+
export interface StreamRetryHint {
|
|
91
|
+
liveId: string;
|
|
92
|
+
preferAlternativeStream: boolean;
|
|
93
|
+
}
|
|
90
94
|
export interface RecordHandle {
|
|
91
95
|
id: string;
|
|
92
96
|
stream: string;
|
|
@@ -172,6 +176,8 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
172
176
|
getSavePath: GetSavePath;
|
|
173
177
|
banLiveId?: string;
|
|
174
178
|
isManualStart?: boolean;
|
|
179
|
+
/** 上一次使用的流疑似不可用,录制器可在同一场直播中优先尝试备用流 */
|
|
180
|
+
streamRetryHint?: StreamRetryHint;
|
|
175
181
|
}) => Promise<RecordHandle | null>;
|
|
176
182
|
recordHandle?: RecordHandle;
|
|
177
183
|
toJSON: (this: Recorder<E>) => SerializedRecorder<E>;
|