@apocaliss92/nodelink-js 0.6.4 → 0.6.6
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 +26 -0
- package/dist/{chunk-UL34MR4L.js → chunk-JQ5NSEVD.js} +597 -42
- package/dist/chunk-JQ5NSEVD.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +612 -54
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +703 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +217 -51
- package/dist/index.d.ts +179 -5
- package/dist/index.js +61 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-UL34MR4L.js.map +0 -1
|
@@ -5977,9 +5977,354 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
5977
5977
|
}
|
|
5978
5978
|
}
|
|
5979
5979
|
|
|
5980
|
-
// src/baichuan/stream/
|
|
5981
|
-
|
|
5980
|
+
// src/baichuan/stream/alwaysOnTypes.ts
|
|
5981
|
+
var ALWAYS_ON_DEFAULTS = {
|
|
5982
|
+
triggers: ["motion", "doorbell"],
|
|
5983
|
+
windowMs: 15e3,
|
|
5984
|
+
idleFps: 1,
|
|
5985
|
+
primeOnStart: true,
|
|
5986
|
+
placeholder: { enabled: true, text: "Sleeping", opacity: 0.5 }
|
|
5987
|
+
};
|
|
5988
|
+
|
|
5989
|
+
// src/baichuan/stream/PlaceholderRenderer.ts
|
|
5982
5990
|
import { spawn } from "child_process";
|
|
5991
|
+
import { Jimp, JimpMime, loadFont, measureText, measureTextHeight } from "jimp";
|
|
5992
|
+
import { SANS_32_WHITE, SANS_64_WHITE, SANS_128_WHITE } from "jimp/fonts";
|
|
5993
|
+
function ffmpegCodec(videoType) {
|
|
5994
|
+
if (videoType === "H265") {
|
|
5995
|
+
return {
|
|
5996
|
+
inputFormat: "hevc",
|
|
5997
|
+
encoder: "libx265",
|
|
5998
|
+
outputFormat: "hevc"
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
return {
|
|
6002
|
+
inputFormat: "h264",
|
|
6003
|
+
encoder: "libx264",
|
|
6004
|
+
outputFormat: "h264"
|
|
6005
|
+
};
|
|
6006
|
+
}
|
|
6007
|
+
function runFfmpeg(args, input) {
|
|
6008
|
+
return new Promise((resolve, reject) => {
|
|
6009
|
+
const proc = spawn("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
6010
|
+
const stdoutChunks = [];
|
|
6011
|
+
const stderrChunks = [];
|
|
6012
|
+
proc.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
6013
|
+
proc.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
6014
|
+
proc.on("error", (error) => reject(error));
|
|
6015
|
+
proc.on("close", (code) => {
|
|
6016
|
+
if (code === 0) {
|
|
6017
|
+
resolve(Buffer.concat(stdoutChunks));
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
6021
|
+
reject(new Error(`ffmpeg exited with code ${code}: ${stderr}`));
|
|
6022
|
+
});
|
|
6023
|
+
const stdin = proc.stdin;
|
|
6024
|
+
if (!stdin) {
|
|
6025
|
+
reject(new Error("ffmpeg stdin not available"));
|
|
6026
|
+
return;
|
|
6027
|
+
}
|
|
6028
|
+
stdin.on("error", (error) => reject(error));
|
|
6029
|
+
stdin.end(input);
|
|
6030
|
+
});
|
|
6031
|
+
}
|
|
6032
|
+
var PlaceholderRenderer = class {
|
|
6033
|
+
opts;
|
|
6034
|
+
logger;
|
|
6035
|
+
constructor(args) {
|
|
6036
|
+
this.opts = { ...ALWAYS_ON_DEFAULTS.placeholder, ...args.placeholder ?? {} };
|
|
6037
|
+
this.logger = args.logger;
|
|
6038
|
+
}
|
|
6039
|
+
/** Returns the access unit bytes to emit as placeholder, or null if none available. */
|
|
6040
|
+
async render(keyframe) {
|
|
6041
|
+
if (!keyframe) return null;
|
|
6042
|
+
if (!this.opts.enabled) return keyframe.data;
|
|
6043
|
+
try {
|
|
6044
|
+
const jpeg = await this.decodeToJpeg(keyframe);
|
|
6045
|
+
const decorated = await this.decorate(jpeg);
|
|
6046
|
+
const idr = await this.encodeIdr(decorated, keyframe.videoType);
|
|
6047
|
+
if (!idr || idr.length === 0) {
|
|
6048
|
+
throw new Error("ffmpeg produced empty IDR output");
|
|
6049
|
+
}
|
|
6050
|
+
return idr;
|
|
6051
|
+
} catch (error) {
|
|
6052
|
+
this.logger?.warn?.(
|
|
6053
|
+
"PlaceholderRenderer: decoration failed, falling back to raw keyframe",
|
|
6054
|
+
error instanceof Error ? error.message : error
|
|
6055
|
+
);
|
|
6056
|
+
return keyframe.data;
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
/** Decodes the cached keyframe access unit into a single JPEG still via ffmpeg. */
|
|
6060
|
+
async decodeToJpeg(keyframe) {
|
|
6061
|
+
const { inputFormat } = ffmpegCodec(keyframe.videoType);
|
|
6062
|
+
return runFfmpeg(
|
|
6063
|
+
[
|
|
6064
|
+
"-hide_banner",
|
|
6065
|
+
"-loglevel",
|
|
6066
|
+
"error",
|
|
6067
|
+
"-f",
|
|
6068
|
+
inputFormat,
|
|
6069
|
+
"-i",
|
|
6070
|
+
"pipe:0",
|
|
6071
|
+
"-frames:v",
|
|
6072
|
+
"1",
|
|
6073
|
+
"-f",
|
|
6074
|
+
"mjpeg",
|
|
6075
|
+
"pipe:1"
|
|
6076
|
+
],
|
|
6077
|
+
keyframe.data
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
/** Dims the still and prints the overlay text using jimp, returning a JPEG buffer. */
|
|
6081
|
+
async decorate(jpeg) {
|
|
6082
|
+
const image = await Jimp.read(jpeg);
|
|
6083
|
+
const op = Math.max(0, Math.min(1, this.opts.opacity));
|
|
6084
|
+
if (op < 1) {
|
|
6085
|
+
const data = image.bitmap.data;
|
|
6086
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
6087
|
+
data[i] = data[i] * op;
|
|
6088
|
+
data[i + 1] = data[i + 1] * op;
|
|
6089
|
+
data[i + 2] = data[i + 2] * op;
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
const fontDef = image.width >= 1280 ? SANS_128_WHITE : image.width >= 640 ? SANS_64_WHITE : SANS_32_WHITE;
|
|
6093
|
+
const font = await loadFont(fontDef);
|
|
6094
|
+
const text = this.opts.text;
|
|
6095
|
+
const textWidth = measureText(font, text);
|
|
6096
|
+
const textHeight = measureTextHeight(font, text, image.width);
|
|
6097
|
+
const x = Math.max(0, Math.round((image.width - textWidth) / 2));
|
|
6098
|
+
const y = Math.max(0, Math.round((image.height - textHeight) / 2));
|
|
6099
|
+
image.print({ font, x, y, text });
|
|
6100
|
+
return image.getBuffer(JimpMime.jpeg);
|
|
6101
|
+
}
|
|
6102
|
+
/** Encodes the decorated JPEG into a single IDR access unit in the target codec. */
|
|
6103
|
+
async encodeIdr(jpeg, videoType) {
|
|
6104
|
+
const { encoder, outputFormat } = ffmpegCodec(videoType);
|
|
6105
|
+
return runFfmpeg(
|
|
6106
|
+
[
|
|
6107
|
+
"-hide_banner",
|
|
6108
|
+
"-loglevel",
|
|
6109
|
+
"error",
|
|
6110
|
+
"-f",
|
|
6111
|
+
"image2pipe",
|
|
6112
|
+
"-i",
|
|
6113
|
+
"pipe:0",
|
|
6114
|
+
"-frames:v",
|
|
6115
|
+
"1",
|
|
6116
|
+
"-c:v",
|
|
6117
|
+
encoder,
|
|
6118
|
+
"-pix_fmt",
|
|
6119
|
+
"yuv420p",
|
|
6120
|
+
"-f",
|
|
6121
|
+
outputFormat,
|
|
6122
|
+
"pipe:1"
|
|
6123
|
+
],
|
|
6124
|
+
jpeg
|
|
6125
|
+
);
|
|
6126
|
+
}
|
|
6127
|
+
};
|
|
6128
|
+
|
|
6129
|
+
// src/baichuan/stream/ContinuousVideoStream.ts
|
|
6130
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
6131
|
+
var ContinuousVideoStream = class extends EventEmitter3 {
|
|
6132
|
+
constructor(opts) {
|
|
6133
|
+
super();
|
|
6134
|
+
this.opts = opts;
|
|
6135
|
+
this.idleFps = Math.max(0.1, opts.idleFps ?? ALWAYS_ON_DEFAULTS.idleFps);
|
|
6136
|
+
this.logger = opts.logger;
|
|
6137
|
+
const rendererArgs = {};
|
|
6138
|
+
if (opts.placeholder !== void 0) rendererArgs.placeholder = opts.placeholder;
|
|
6139
|
+
if (opts.logger !== void 0) rendererArgs.logger = opts.logger;
|
|
6140
|
+
this.renderer = opts.renderer ?? new PlaceholderRenderer(rendererArgs);
|
|
6141
|
+
}
|
|
6142
|
+
live = null;
|
|
6143
|
+
lastKeyframe = null;
|
|
6144
|
+
lastMicroseconds = 0;
|
|
6145
|
+
idleFps;
|
|
6146
|
+
renderer;
|
|
6147
|
+
logger;
|
|
6148
|
+
stopped = false;
|
|
6149
|
+
starting = false;
|
|
6150
|
+
idleTimer = null;
|
|
6151
|
+
idlePlaceholder = null;
|
|
6152
|
+
hasCachedKeyframe() {
|
|
6153
|
+
return this.lastKeyframe !== null;
|
|
6154
|
+
}
|
|
6155
|
+
async goLive() {
|
|
6156
|
+
if (this.stopped || this.live || this.starting) return;
|
|
6157
|
+
this.starting = true;
|
|
6158
|
+
try {
|
|
6159
|
+
this.stopIdleLoop();
|
|
6160
|
+
const stream = await this.opts.createLiveStream();
|
|
6161
|
+
this.live = stream;
|
|
6162
|
+
stream.on("videoAccessUnit", this.onLiveAccessUnit);
|
|
6163
|
+
stream.on("additionalHeader", this.onAdditionalHeader);
|
|
6164
|
+
stream.on("audioFrame", this.onAudioFrame);
|
|
6165
|
+
stream.on("error", this.onLiveError);
|
|
6166
|
+
await stream.start().catch((e) => this.emit("error", e));
|
|
6167
|
+
} finally {
|
|
6168
|
+
this.starting = false;
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
async goIdle() {
|
|
6172
|
+
if (!this.live) return;
|
|
6173
|
+
const s = this.live;
|
|
6174
|
+
this.live = null;
|
|
6175
|
+
s.off("videoAccessUnit", this.onLiveAccessUnit);
|
|
6176
|
+
s.off("additionalHeader", this.onAdditionalHeader);
|
|
6177
|
+
s.off("audioFrame", this.onAudioFrame);
|
|
6178
|
+
s.off("error", this.onLiveError);
|
|
6179
|
+
await s.stop().catch(() => {
|
|
6180
|
+
});
|
|
6181
|
+
await this.startIdleLoop();
|
|
6182
|
+
}
|
|
6183
|
+
async stop() {
|
|
6184
|
+
this.stopped = true;
|
|
6185
|
+
await this.goIdle();
|
|
6186
|
+
this.stopIdleLoop();
|
|
6187
|
+
this.emit("close");
|
|
6188
|
+
}
|
|
6189
|
+
async startIdleLoop() {
|
|
6190
|
+
if (this.stopped) return;
|
|
6191
|
+
this.idlePlaceholder = await this.renderer.render(this.lastKeyframe);
|
|
6192
|
+
if (!this.idlePlaceholder || !this.lastKeyframe) {
|
|
6193
|
+
this.logger?.debug?.("[ContinuousVideoStream] no keyframe yet; idle loop deferred");
|
|
6194
|
+
return;
|
|
6195
|
+
}
|
|
6196
|
+
const stepUs = Math.round(1e6 / this.idleFps);
|
|
6197
|
+
const videoType = this.lastKeyframe.videoType;
|
|
6198
|
+
this.idleTimer = setInterval(() => {
|
|
6199
|
+
if (!this.idlePlaceholder) return;
|
|
6200
|
+
this.lastMicroseconds += stepUs;
|
|
6201
|
+
this.emit("videoAccessUnit", {
|
|
6202
|
+
data: this.idlePlaceholder,
|
|
6203
|
+
isKeyframe: true,
|
|
6204
|
+
videoType,
|
|
6205
|
+
microseconds: this.lastMicroseconds
|
|
6206
|
+
});
|
|
6207
|
+
}, Math.round(1e3 / this.idleFps));
|
|
6208
|
+
}
|
|
6209
|
+
stopIdleLoop() {
|
|
6210
|
+
if (this.idleTimer) {
|
|
6211
|
+
clearInterval(this.idleTimer);
|
|
6212
|
+
this.idleTimer = null;
|
|
6213
|
+
}
|
|
6214
|
+
this.idlePlaceholder = null;
|
|
6215
|
+
}
|
|
6216
|
+
onLiveAccessUnit = (au) => {
|
|
6217
|
+
if (au.isKeyframe) {
|
|
6218
|
+
this.lastKeyframe = { data: au.data, videoType: au.videoType };
|
|
6219
|
+
}
|
|
6220
|
+
this.lastMicroseconds = au.microseconds;
|
|
6221
|
+
this.emit("videoAccessUnit", au);
|
|
6222
|
+
};
|
|
6223
|
+
onAdditionalHeader = (h) => this.emit("additionalHeader", h);
|
|
6224
|
+
onAudioFrame = (a) => this.emit("audioFrame", a);
|
|
6225
|
+
onLiveError = (e) => this.emit("error", e);
|
|
6226
|
+
};
|
|
6227
|
+
|
|
6228
|
+
// src/baichuan/stream/AlwaysOnController.ts
|
|
6229
|
+
var AlwaysOnController = class {
|
|
6230
|
+
constructor(o) {
|
|
6231
|
+
this.o = o;
|
|
6232
|
+
this.triggers = new Set(o.options.triggers ?? ALWAYS_ON_DEFAULTS.triggers);
|
|
6233
|
+
this.windowMs = o.options.windowMs ?? ALWAYS_ON_DEFAULTS.windowMs;
|
|
6234
|
+
this.primeOnStart = o.options.primeOnStart ?? ALWAYS_ON_DEFAULTS.primeOnStart;
|
|
6235
|
+
this.logger = o.logger;
|
|
6236
|
+
}
|
|
6237
|
+
triggers;
|
|
6238
|
+
windowMs;
|
|
6239
|
+
primeOnStart;
|
|
6240
|
+
logger;
|
|
6241
|
+
windowTimer = null;
|
|
6242
|
+
live = false;
|
|
6243
|
+
started = false;
|
|
6244
|
+
handler = (e) => void this.onEvent(e);
|
|
6245
|
+
get windowSeconds() {
|
|
6246
|
+
return Math.round(this.windowMs / 1e3);
|
|
6247
|
+
}
|
|
6248
|
+
async start() {
|
|
6249
|
+
if (this.started) return;
|
|
6250
|
+
this.started = true;
|
|
6251
|
+
await this.o.api.onSimpleEvent(this.handler);
|
|
6252
|
+
this.logger?.info?.(
|
|
6253
|
+
`[AlwaysOnController] started ch${this.o.channel} \u2014 triggers=[${[...this.triggers].join(", ")}], window=${this.windowSeconds}s, primeOnStart=${this.primeOnStart}`
|
|
6254
|
+
);
|
|
6255
|
+
if (this.primeOnStart) {
|
|
6256
|
+
await this.openWindow("prime");
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
async stop() {
|
|
6260
|
+
if (!this.started) return;
|
|
6261
|
+
this.started = false;
|
|
6262
|
+
if (this.windowTimer) {
|
|
6263
|
+
clearTimeout(this.windowTimer);
|
|
6264
|
+
this.windowTimer = null;
|
|
6265
|
+
}
|
|
6266
|
+
await this.o.api.offSimpleEvent(this.handler).catch(() => {
|
|
6267
|
+
});
|
|
6268
|
+
if (this.live) {
|
|
6269
|
+
this.live = false;
|
|
6270
|
+
await this.o.goIdle().catch(() => {
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
this.logger?.info?.(`[AlwaysOnController] stopped ch${this.o.channel}`);
|
|
6274
|
+
}
|
|
6275
|
+
async onEvent(e) {
|
|
6276
|
+
if (e.channel !== this.o.channel) return;
|
|
6277
|
+
if (!this.triggers.has(e.type)) {
|
|
6278
|
+
this.logger?.debug?.(
|
|
6279
|
+
`[AlwaysOnController] event '${e.type}' ch${e.channel} ignored (not a configured trigger)`
|
|
6280
|
+
);
|
|
6281
|
+
return;
|
|
6282
|
+
}
|
|
6283
|
+
await this.openWindow(e.type);
|
|
6284
|
+
}
|
|
6285
|
+
async openWindow(reason) {
|
|
6286
|
+
if (this.windowTimer) clearTimeout(this.windowTimer);
|
|
6287
|
+
if (!this.live) {
|
|
6288
|
+
this.live = true;
|
|
6289
|
+
try {
|
|
6290
|
+
await this.o.api.wakeUp(this.o.channel).catch(() => {
|
|
6291
|
+
});
|
|
6292
|
+
await this.o.goLive();
|
|
6293
|
+
this.logger?.info?.(
|
|
6294
|
+
`[AlwaysOnController] live window OPENED (trigger=${reason}) \u2014 streaming real frames; will sleep in ${this.windowSeconds}s without new events`
|
|
6295
|
+
);
|
|
6296
|
+
} catch (err) {
|
|
6297
|
+
this.live = false;
|
|
6298
|
+
this.logger?.warn?.(
|
|
6299
|
+
`[AlwaysOnController] goLive failed: ${err?.message}`
|
|
6300
|
+
);
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
} else {
|
|
6304
|
+
this.logger?.info?.(
|
|
6305
|
+
`[AlwaysOnController] live window EXTENDED (trigger=${reason}) \u2014 sleep timer reset to ${this.windowSeconds}s`
|
|
6306
|
+
);
|
|
6307
|
+
}
|
|
6308
|
+
this.windowTimer = setTimeout(() => void this.closeWindow(), this.windowMs);
|
|
6309
|
+
}
|
|
6310
|
+
async closeWindow() {
|
|
6311
|
+
this.windowTimer = null;
|
|
6312
|
+
if (!this.live) return;
|
|
6313
|
+
this.live = false;
|
|
6314
|
+
this.logger?.info?.(
|
|
6315
|
+
`[AlwaysOnController] live window CLOSED \u2014 going idle (placeholder); camera can sleep`
|
|
6316
|
+
);
|
|
6317
|
+
await this.o.goIdle().catch(
|
|
6318
|
+
(err) => this.logger?.warn?.(
|
|
6319
|
+
`[AlwaysOnController] goIdle failed: ${err?.message}`
|
|
6320
|
+
)
|
|
6321
|
+
);
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
|
|
6325
|
+
// src/baichuan/stream/BaichuanRtspServer.ts
|
|
6326
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
6327
|
+
import { spawn as spawn2 } from "child_process";
|
|
5983
6328
|
import * as net2 from "net";
|
|
5984
6329
|
import * as dgram2 from "dgram";
|
|
5985
6330
|
import * as crypto from "crypto";
|
|
@@ -6206,7 +6551,7 @@ function envBool(value, defaultValue) {
|
|
|
6206
6551
|
if (v === "0" || v === "false" || v === "no" || v === "off") return false;
|
|
6207
6552
|
return defaultValue;
|
|
6208
6553
|
}
|
|
6209
|
-
var BaichuanRtspServer = class _BaichuanRtspServer extends
|
|
6554
|
+
var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter4 {
|
|
6210
6555
|
api;
|
|
6211
6556
|
channel;
|
|
6212
6557
|
profile;
|
|
@@ -6221,6 +6566,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6221
6566
|
deviceId;
|
|
6222
6567
|
dedicatedSessionRelease;
|
|
6223
6568
|
externalListener;
|
|
6569
|
+
// Always-on continuous stream (battery cameras). Populated only when
|
|
6570
|
+
// `options.alwaysOn?.enabled`; the default (non-alwaysOn) path leaves these
|
|
6571
|
+
// null/undefined and is byte-for-byte equivalent in behaviour.
|
|
6572
|
+
alwaysOnOptions;
|
|
6573
|
+
continuousStream = null;
|
|
6574
|
+
alwaysOnController = null;
|
|
6224
6575
|
// Authentication
|
|
6225
6576
|
authCredentials = [];
|
|
6226
6577
|
requireAuth;
|
|
@@ -6235,6 +6586,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6235
6586
|
// Set of client IDs (IP:port)
|
|
6236
6587
|
nativeStreamActive = false;
|
|
6237
6588
|
// Whether the native stream is currently active
|
|
6589
|
+
tearingDown = false;
|
|
6590
|
+
// True while stop() is running; suppresses onEnd-driven restarts
|
|
6238
6591
|
clientConnectionServer;
|
|
6239
6592
|
// TCP server to track connections
|
|
6240
6593
|
streamMetadata = null;
|
|
@@ -6422,6 +6775,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6422
6775
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
6423
6776
|
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
6424
6777
|
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
6778
|
+
this.alwaysOnOptions = options.alwaysOn;
|
|
6425
6779
|
const transport = this.api.client.getTransport();
|
|
6426
6780
|
this.flow = createRtspFlow(transport, "H264");
|
|
6427
6781
|
}
|
|
@@ -7545,7 +7899,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7545
7899
|
this.rtspDebugLog(
|
|
7546
7900
|
`Spawning ffmpeg for client ${clientId}: ffmpeg ${ffmpegArgs.join(" ")}`
|
|
7547
7901
|
);
|
|
7548
|
-
ffmpeg =
|
|
7902
|
+
ffmpeg = spawn2("ffmpeg", ffmpegArgs, {
|
|
7549
7903
|
stdio
|
|
7550
7904
|
});
|
|
7551
7905
|
try {
|
|
@@ -7909,6 +8263,141 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7909
8263
|
}
|
|
7910
8264
|
});
|
|
7911
8265
|
}
|
|
8266
|
+
/**
|
|
8267
|
+
* Always-on source: bridge a {@link ContinuousVideoStream} into the existing
|
|
8268
|
+
* fanout. Yields the same frame shape that `createNativeStream` produces, so
|
|
8269
|
+
* the rest of the pipeline (prebuffer, param-set extraction, per-client
|
|
8270
|
+
* subscribe, ffmpeg/direct-RTP) is unchanged.
|
|
8271
|
+
*
|
|
8272
|
+
* The CVS itself is long-lived (created once, reused across native-stream
|
|
8273
|
+
* restarts) and is driven by the {@link AlwaysOnController}, which opens/closes
|
|
8274
|
+
* live windows from camera events. Each fanout source generator only forwards
|
|
8275
|
+
* CVS events to the fanout pump for as long as `signal` is not aborted.
|
|
8276
|
+
*/
|
|
8277
|
+
async *createContinuousSource(dedicatedClient, signal) {
|
|
8278
|
+
const cvs = this.ensureContinuousStream(dedicatedClient);
|
|
8279
|
+
const queue = [];
|
|
8280
|
+
const MAX_QUEUE = 200;
|
|
8281
|
+
let wake = null;
|
|
8282
|
+
let done = false;
|
|
8283
|
+
const push = (frame) => {
|
|
8284
|
+
queue.push(frame);
|
|
8285
|
+
if (queue.length > MAX_QUEUE) {
|
|
8286
|
+
queue.splice(0, queue.length - MAX_QUEUE);
|
|
8287
|
+
}
|
|
8288
|
+
if (wake) {
|
|
8289
|
+
const w = wake;
|
|
8290
|
+
wake = null;
|
|
8291
|
+
w();
|
|
8292
|
+
}
|
|
8293
|
+
};
|
|
8294
|
+
const onVideo = (au) => {
|
|
8295
|
+
push({
|
|
8296
|
+
audio: false,
|
|
8297
|
+
data: au.data,
|
|
8298
|
+
codec: null,
|
|
8299
|
+
sampleRate: null,
|
|
8300
|
+
microseconds: au.microseconds,
|
|
8301
|
+
videoType: au.videoType,
|
|
8302
|
+
isKeyframe: au.isKeyframe
|
|
8303
|
+
});
|
|
8304
|
+
};
|
|
8305
|
+
const onAudio = (frame) => {
|
|
8306
|
+
push({
|
|
8307
|
+
audio: true,
|
|
8308
|
+
data: frame,
|
|
8309
|
+
codec: "aac",
|
|
8310
|
+
sampleRate: 8e3,
|
|
8311
|
+
microseconds: null
|
|
8312
|
+
});
|
|
8313
|
+
};
|
|
8314
|
+
const finish = () => {
|
|
8315
|
+
done = true;
|
|
8316
|
+
if (wake) {
|
|
8317
|
+
const w = wake;
|
|
8318
|
+
wake = null;
|
|
8319
|
+
w();
|
|
8320
|
+
}
|
|
8321
|
+
};
|
|
8322
|
+
const onAbort = () => finish();
|
|
8323
|
+
cvs.on("videoAccessUnit", onVideo);
|
|
8324
|
+
cvs.on("audioFrame", onAudio);
|
|
8325
|
+
cvs.on("close", finish);
|
|
8326
|
+
if (signal.aborted) {
|
|
8327
|
+
done = true;
|
|
8328
|
+
} else {
|
|
8329
|
+
signal.addEventListener("abort", onAbort);
|
|
8330
|
+
}
|
|
8331
|
+
try {
|
|
8332
|
+
while (!done && !signal.aborted) {
|
|
8333
|
+
if (queue.length > 0) {
|
|
8334
|
+
yield queue.shift();
|
|
8335
|
+
} else {
|
|
8336
|
+
await new Promise((resolve) => {
|
|
8337
|
+
wake = resolve;
|
|
8338
|
+
if (done || signal.aborted) {
|
|
8339
|
+
wake = null;
|
|
8340
|
+
resolve();
|
|
8341
|
+
}
|
|
8342
|
+
});
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
while (queue.length > 0 && !signal.aborted) {
|
|
8346
|
+
yield queue.shift();
|
|
8347
|
+
}
|
|
8348
|
+
} finally {
|
|
8349
|
+
cvs.off("videoAccessUnit", onVideo);
|
|
8350
|
+
cvs.off("audioFrame", onAudio);
|
|
8351
|
+
cvs.off("close", finish);
|
|
8352
|
+
signal.removeEventListener("abort", onAbort);
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
/**
|
|
8356
|
+
* Lazily build the long-lived {@link ContinuousVideoStream} +
|
|
8357
|
+
* {@link AlwaysOnController} for always-on mode. Both are created once and
|
|
8358
|
+
* reused for the lifetime of the server (across native-stream restarts).
|
|
8359
|
+
*/
|
|
8360
|
+
ensureContinuousStream(dedicatedClient) {
|
|
8361
|
+
if (this.continuousStream) return this.continuousStream;
|
|
8362
|
+
const createLiveStream = async () => {
|
|
8363
|
+
const client = dedicatedClient ?? this.api.client;
|
|
8364
|
+
return new BaichuanVideoStream({
|
|
8365
|
+
client,
|
|
8366
|
+
api: this.api,
|
|
8367
|
+
channel: this.channel,
|
|
8368
|
+
profile: this.profile,
|
|
8369
|
+
...this.variant !== "default" ? { variant: this.variant } : {},
|
|
8370
|
+
...this.logger ? { logger: this.logger } : {}
|
|
8371
|
+
});
|
|
8372
|
+
};
|
|
8373
|
+
const cvsOptions = {
|
|
8374
|
+
createLiveStream,
|
|
8375
|
+
...this.alwaysOnOptions?.idleFps !== void 0 ? { idleFps: this.alwaysOnOptions.idleFps } : {},
|
|
8376
|
+
...this.alwaysOnOptions?.placeholder !== void 0 ? { placeholder: this.alwaysOnOptions.placeholder } : {},
|
|
8377
|
+
...this.logger ? { logger: this.logger } : {}
|
|
8378
|
+
};
|
|
8379
|
+
const cvs = new ContinuousVideoStream(cvsOptions);
|
|
8380
|
+
cvs.on("error", (e) => {
|
|
8381
|
+
this.logger.warn(
|
|
8382
|
+
`[BaichuanRtspServer] ContinuousVideoStream error: ${e?.message ?? e}`
|
|
8383
|
+
);
|
|
8384
|
+
});
|
|
8385
|
+
this.continuousStream = cvs;
|
|
8386
|
+
this.alwaysOnController = new AlwaysOnController({
|
|
8387
|
+
api: this.api,
|
|
8388
|
+
channel: this.channel,
|
|
8389
|
+
options: this.alwaysOnOptions,
|
|
8390
|
+
goLive: () => cvs.goLive(),
|
|
8391
|
+
goIdle: () => cvs.goIdle(),
|
|
8392
|
+
...this.logger ? { logger: this.logger } : {}
|
|
8393
|
+
});
|
|
8394
|
+
void this.alwaysOnController.start().catch((e) => {
|
|
8395
|
+
this.logger.warn(
|
|
8396
|
+
`[BaichuanRtspServer] AlwaysOnController start failed: ${e?.message ?? e}`
|
|
8397
|
+
);
|
|
8398
|
+
});
|
|
8399
|
+
return cvs;
|
|
8400
|
+
}
|
|
7912
8401
|
/**
|
|
7913
8402
|
* Start native stream (mark as active).
|
|
7914
8403
|
* Each client will create its own generator, so we just track that the stream is active.
|
|
@@ -7970,7 +8459,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7970
8459
|
await this.flow.startKeepAlive(this.api);
|
|
7971
8460
|
this.nativeFanout = new NativeStreamFanout({
|
|
7972
8461
|
maxQueueItems: 200,
|
|
7973
|
-
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
8462
|
+
createSource: (signal) => this.alwaysOnOptions?.enabled ? this.createContinuousSource(dedicatedClient, signal) : createNativeStream(this.api, this.channel, this.profile, {
|
|
7974
8463
|
variant: this.variant,
|
|
7975
8464
|
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
7976
8465
|
signal
|
|
@@ -8053,6 +8542,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
8053
8542
|
} catch {
|
|
8054
8543
|
}
|
|
8055
8544
|
}
|
|
8545
|
+
if (this.tearingDown) return;
|
|
8056
8546
|
if (this.connectedClients.size > 0 && hadFrames) {
|
|
8057
8547
|
this.logger.info(
|
|
8058
8548
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
@@ -8066,7 +8556,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
8066
8556
|
});
|
|
8067
8557
|
this.nativeFanout.start();
|
|
8068
8558
|
this.clearNoFrameDeadlineTimer();
|
|
8069
|
-
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
8559
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0 && !this.alwaysOnOptions?.enabled) {
|
|
8070
8560
|
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
8071
8561
|
this.noFrameDeadlineTimer = void 0;
|
|
8072
8562
|
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
@@ -8079,7 +8569,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
8079
8569
|
this.noFrameDeadlineTimer?.unref?.();
|
|
8080
8570
|
}
|
|
8081
8571
|
this.clearNoClientAutoStopTimer();
|
|
8082
|
-
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
8572
|
+
if (this.nativeStreamPrimeIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
|
|
8083
8573
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
8084
8574
|
if (this.connectedClients.size === 0) {
|
|
8085
8575
|
this.rtspDebugLog(
|
|
@@ -8166,7 +8656,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
8166
8656
|
this.emit("clientDisconnected", clientId);
|
|
8167
8657
|
if (this.connectedClients.size === 0) {
|
|
8168
8658
|
this.clearNoClientAutoStopTimer();
|
|
8169
|
-
if (this.nativeStreamIdleStopMs > 0) {
|
|
8659
|
+
if (this.nativeStreamIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
|
|
8170
8660
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
8171
8661
|
if (this.connectedClients.size === 0) {
|
|
8172
8662
|
void this.stopNativeStream();
|
|
@@ -8237,9 +8727,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
8237
8727
|
if (!this.active) {
|
|
8238
8728
|
return;
|
|
8239
8729
|
}
|
|
8730
|
+
this.tearingDown = true;
|
|
8240
8731
|
this.logger.info(
|
|
8241
8732
|
`[BaichuanRtspServer] Stopping RTSP server on ${this.listenHost}:${this.listenPort}...`
|
|
8242
8733
|
);
|
|
8734
|
+
if (this.alwaysOnController) {
|
|
8735
|
+
const controller = this.alwaysOnController;
|
|
8736
|
+
this.alwaysOnController = null;
|
|
8737
|
+
await controller.stop().catch(() => {
|
|
8738
|
+
});
|
|
8739
|
+
}
|
|
8740
|
+
if (this.continuousStream) {
|
|
8741
|
+
const cvs = this.continuousStream;
|
|
8742
|
+
this.continuousStream = null;
|
|
8743
|
+
await cvs.stop().catch(() => {
|
|
8744
|
+
});
|
|
8745
|
+
}
|
|
8243
8746
|
await this.stopNativeStream();
|
|
8244
8747
|
const clientIds = Array.from(this.connectedClients);
|
|
8245
8748
|
for (const clientId of clientIds) {
|
|
@@ -9199,8 +9702,8 @@ function patchMotionSensitivityListXml(currentXml, bands) {
|
|
|
9199
9702
|
}
|
|
9200
9703
|
|
|
9201
9704
|
// src/emailPush/bus.ts
|
|
9202
|
-
import { EventEmitter as
|
|
9203
|
-
var emitter = new
|
|
9705
|
+
import { EventEmitter as EventEmitter5 } from "events";
|
|
9706
|
+
var emitter = new EventEmitter5();
|
|
9204
9707
|
var cameraResolver = () => void 0;
|
|
9205
9708
|
var lastEventByCamera = /* @__PURE__ */ new Map();
|
|
9206
9709
|
var MAX_GLOBAL_EVENTS = 300;
|
|
@@ -9253,7 +9756,7 @@ function _resetEmailPushBusForTests() {
|
|
|
9253
9756
|
}
|
|
9254
9757
|
|
|
9255
9758
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
9256
|
-
import { spawn as
|
|
9759
|
+
import { spawn as spawn3 } from "child_process";
|
|
9257
9760
|
import { mkdir } from "fs/promises";
|
|
9258
9761
|
import { dirname } from "path";
|
|
9259
9762
|
import { PassThrough } from "stream";
|
|
@@ -9402,6 +9905,30 @@ function buildSetNtpXml(current, patch) {
|
|
|
9402
9905
|
);
|
|
9403
9906
|
}
|
|
9404
9907
|
|
|
9908
|
+
// src/reolink/baichuan/utils/channelEnumeration.ts
|
|
9909
|
+
async function resolveBaichuanChannels(deps) {
|
|
9910
|
+
const fromPush = dedupeSorted(deps.pushChannels);
|
|
9911
|
+
if (fromPush.length > 0) return fromPush;
|
|
9912
|
+
const slots = dedupeSorted(deps.supportChnIds);
|
|
9913
|
+
const candidates = slots.length > 0 ? slots : [0];
|
|
9914
|
+
const probed = await Promise.all(
|
|
9915
|
+
candidates.map(
|
|
9916
|
+
async (channel) => await deps.probe(channel) ? channel : void 0
|
|
9917
|
+
)
|
|
9918
|
+
);
|
|
9919
|
+
return dedupeSorted(
|
|
9920
|
+
probed.filter((c) => c !== void 0)
|
|
9921
|
+
);
|
|
9922
|
+
}
|
|
9923
|
+
function dedupeSorted(values) {
|
|
9924
|
+
const set = /* @__PURE__ */ new Set();
|
|
9925
|
+
for (const v of values) {
|
|
9926
|
+
const n = Number(v);
|
|
9927
|
+
if (Number.isFinite(n) && n >= 0) set.add(n);
|
|
9928
|
+
}
|
|
9929
|
+
return [...set].sort((a, b) => a - b);
|
|
9930
|
+
}
|
|
9931
|
+
|
|
9405
9932
|
// src/reolink/baichuan/utils/dst.ts
|
|
9406
9933
|
var parseNumberSafe3 = (text) => {
|
|
9407
9934
|
if (text === void 0) return void 0;
|
|
@@ -9582,7 +10109,7 @@ function buildSetSystemGeneralXml(patch) {
|
|
|
9582
10109
|
}
|
|
9583
10110
|
|
|
9584
10111
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
9585
|
-
import { Jimp, JimpMime } from "jimp";
|
|
10112
|
+
import { Jimp as Jimp2, JimpMime as JimpMime2 } from "jimp";
|
|
9586
10113
|
|
|
9587
10114
|
// src/reolink/baichuan/utils/abilityInfo.ts
|
|
9588
10115
|
var parseAbilityInfoXml = (xml) => {
|
|
@@ -15095,13 +15622,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
15095
15622
|
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
15096
15623
|
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
15097
15624
|
* no dependency on async push messages. Recommended for first-call discovery.
|
|
15098
|
-
* - `"baichuan"`:
|
|
15099
|
-
*
|
|
15100
|
-
*
|
|
15101
|
-
*
|
|
15625
|
+
* - `"baichuan"`: HTTP-free discovery. Prefers the cmd_id 145 push cache when
|
|
15626
|
+
* populated; otherwise actively probes the channel slots advertised by Support
|
|
15627
|
+
* (`items[].chnID`) via `getInfo`. Use this for hubs with HTTP disabled.
|
|
15628
|
+
*
|
|
15629
|
+
* When the api was constructed with `nativeOnly`, the source is forced to
|
|
15630
|
+
* `"baichuan"` regardless of this option (no HTTP/CGI is ever attempted).
|
|
15102
15631
|
*/
|
|
15103
15632
|
async getNvrChannelsSummary(options) {
|
|
15104
|
-
const source = options?.source ?? "cgi";
|
|
15633
|
+
const source = this.nativeOnly ? "baichuan" : options?.source ?? "cgi";
|
|
15634
|
+
const support = await this.getSupportInfo().catch(() => {
|
|
15635
|
+
this.logger.error?.(
|
|
15636
|
+
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
15637
|
+
);
|
|
15638
|
+
return void 0;
|
|
15639
|
+
});
|
|
15105
15640
|
let channels;
|
|
15106
15641
|
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
15107
15642
|
if (options?.channels?.length) {
|
|
@@ -15131,15 +15666,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
15131
15666
|
channels = [];
|
|
15132
15667
|
}
|
|
15133
15668
|
} else {
|
|
15134
|
-
const
|
|
15135
|
-
|
|
15669
|
+
const pushChannels = Array.from(
|
|
15670
|
+
this.getChannelInfoFromPushCache().keys()
|
|
15671
|
+
).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
15672
|
+
const supportChnIds = (support?.items ?? []).map((i) => Number(i.chnID)).filter((n) => Number.isFinite(n));
|
|
15673
|
+
const probeTimeoutMs = options?.timeoutMs ?? 2500;
|
|
15674
|
+
channels = await resolveBaichuanChannels({
|
|
15675
|
+
pushChannels,
|
|
15676
|
+
supportChnIds,
|
|
15677
|
+
probe: async (channel) => {
|
|
15678
|
+
try {
|
|
15679
|
+
await this.getInfo(channel, {
|
|
15680
|
+
timeoutMs: probeTimeoutMs,
|
|
15681
|
+
tags: ["type", "name"]
|
|
15682
|
+
});
|
|
15683
|
+
return true;
|
|
15684
|
+
} catch {
|
|
15685
|
+
return false;
|
|
15686
|
+
}
|
|
15687
|
+
}
|
|
15688
|
+
});
|
|
15689
|
+
this.logger.debug?.(
|
|
15690
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: baichuan resolved ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
15691
|
+
);
|
|
15136
15692
|
}
|
|
15137
15693
|
channels = channels.sort((a, b) => a - b);
|
|
15138
|
-
const support = await this.getSupportInfo().catch(() => {
|
|
15139
|
-
this.logger.error?.(
|
|
15140
|
-
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
15141
|
-
);
|
|
15142
|
-
});
|
|
15143
15694
|
const truthyNumberLike = (v) => {
|
|
15144
15695
|
if (typeof v === "number") return v > 0;
|
|
15145
15696
|
if (typeof v === "string") {
|
|
@@ -15709,12 +16260,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
15709
16260
|
let wideImg;
|
|
15710
16261
|
let teleImg;
|
|
15711
16262
|
try {
|
|
15712
|
-
wideImg = await
|
|
16263
|
+
wideImg = await Jimp2.read(wide);
|
|
15713
16264
|
} catch {
|
|
15714
16265
|
return wide;
|
|
15715
16266
|
}
|
|
15716
16267
|
try {
|
|
15717
|
-
teleImg = await
|
|
16268
|
+
teleImg = await Jimp2.read(tele);
|
|
15718
16269
|
} catch {
|
|
15719
16270
|
return wide;
|
|
15720
16271
|
}
|
|
@@ -15748,7 +16299,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
15748
16299
|
});
|
|
15749
16300
|
teleImg.resize({ w: pipW, h: pipH });
|
|
15750
16301
|
wideImg.composite(teleImg, left, top);
|
|
15751
|
-
return await wideImg.getBuffer(
|
|
16302
|
+
return await wideImg.getBuffer(JimpMime2.jpeg, { quality: 80 });
|
|
15752
16303
|
}
|
|
15753
16304
|
const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
|
|
15754
16305
|
const variant = options?.variant ?? "default";
|
|
@@ -16706,7 +17257,7 @@ ${xml}`);
|
|
|
16706
17257
|
const chunks = [];
|
|
16707
17258
|
let stderr = "";
|
|
16708
17259
|
let timedOut = false;
|
|
16709
|
-
const ff =
|
|
17260
|
+
const ff = spawn3(params.ffmpegPath, [
|
|
16710
17261
|
"-hide_banner",
|
|
16711
17262
|
"-loglevel",
|
|
16712
17263
|
"error",
|
|
@@ -16791,7 +17342,7 @@ ${xml}`);
|
|
|
16791
17342
|
const chunks = [];
|
|
16792
17343
|
let stderr = "";
|
|
16793
17344
|
let timedOut = false;
|
|
16794
|
-
const ff =
|
|
17345
|
+
const ff = spawn3(ffmpegPath, [
|
|
16795
17346
|
"-hide_banner",
|
|
16796
17347
|
"-loglevel",
|
|
16797
17348
|
"error",
|
|
@@ -16907,7 +17458,7 @@ ${xml}`);
|
|
|
16907
17458
|
ensureEnabled: true
|
|
16908
17459
|
});
|
|
16909
17460
|
await new Promise((resolve, reject) => {
|
|
16910
|
-
const ff =
|
|
17461
|
+
const ff = spawn3(ffmpegPath, [
|
|
16911
17462
|
"-hide_banner",
|
|
16912
17463
|
"-loglevel",
|
|
16913
17464
|
"error",
|
|
@@ -16963,7 +17514,7 @@ ${stderr}`));
|
|
|
16963
17514
|
const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
|
|
16964
17515
|
await mkdir(dirname(params.outputPath), { recursive: true });
|
|
16965
17516
|
await new Promise((resolve, reject) => {
|
|
16966
|
-
const ff =
|
|
17517
|
+
const ff = spawn3(ffmpegPath, [
|
|
16967
17518
|
"-hide_banner",
|
|
16968
17519
|
"-loglevel",
|
|
16969
17520
|
"error",
|
|
@@ -17528,7 +18079,7 @@ ${stderr}`)
|
|
|
17528
18079
|
* Convert a raw video keyframe to JPEG using ffmpeg.
|
|
17529
18080
|
*/
|
|
17530
18081
|
async convertFrameToJpeg(params) {
|
|
17531
|
-
const { spawn:
|
|
18082
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
17532
18083
|
const ffmpeg = params.ffmpegPath ?? "ffmpeg";
|
|
17533
18084
|
const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
|
|
17534
18085
|
return new Promise((resolve, reject) => {
|
|
@@ -17550,7 +18101,7 @@ ${stderr}`)
|
|
|
17550
18101
|
"2",
|
|
17551
18102
|
"pipe:1"
|
|
17552
18103
|
];
|
|
17553
|
-
const proc =
|
|
18104
|
+
const proc = spawn4(ffmpeg, args, {
|
|
17554
18105
|
stdio: ["pipe", "pipe", "pipe"]
|
|
17555
18106
|
});
|
|
17556
18107
|
const chunks = [];
|
|
@@ -17693,7 +18244,7 @@ ${stderr}`)
|
|
|
17693
18244
|
* Internal helper to mux video+audio into MP4 using ffmpeg.
|
|
17694
18245
|
*/
|
|
17695
18246
|
async muxToMp4(params) {
|
|
17696
|
-
const { spawn:
|
|
18247
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
17697
18248
|
const { randomUUID: randomUUID3 } = await import("crypto");
|
|
17698
18249
|
const fs = await import("fs/promises");
|
|
17699
18250
|
const os = await import("os");
|
|
@@ -17745,7 +18296,7 @@ ${stderr}`)
|
|
|
17745
18296
|
outputPath
|
|
17746
18297
|
);
|
|
17747
18298
|
await new Promise((resolve, reject) => {
|
|
17748
|
-
const p =
|
|
18299
|
+
const p = spawn4(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
17749
18300
|
let stderr = "";
|
|
17750
18301
|
p.stderr.on("data", (d) => {
|
|
17751
18302
|
stderr += d.toString();
|
|
@@ -22732,7 +23283,7 @@ ${scheduleItems}
|
|
|
22732
23283
|
"mjpeg",
|
|
22733
23284
|
"pipe:1"
|
|
22734
23285
|
];
|
|
22735
|
-
const ff =
|
|
23286
|
+
const ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
22736
23287
|
const chunks = [];
|
|
22737
23288
|
let stderr = "";
|
|
22738
23289
|
ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
|
|
@@ -22856,7 +23407,7 @@ ${scheduleItems}
|
|
|
22856
23407
|
"pipe:1"
|
|
22857
23408
|
];
|
|
22858
23409
|
}
|
|
22859
|
-
ff =
|
|
23410
|
+
ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
22860
23411
|
if (!ff.stdin || !ff.stdout || !ff.stderr) {
|
|
22861
23412
|
throw new Error("ffmpeg stdio streams not available");
|
|
22862
23413
|
}
|
|
@@ -23103,7 +23654,7 @@ ${scheduleItems}
|
|
|
23103
23654
|
"mp4",
|
|
23104
23655
|
"pipe:1"
|
|
23105
23656
|
];
|
|
23106
|
-
ff =
|
|
23657
|
+
ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
23107
23658
|
if (!ff.stdin || !ff.stdout || !ff.stderr) {
|
|
23108
23659
|
throw new Error("ffmpeg stdio streams not available");
|
|
23109
23660
|
}
|
|
@@ -23312,7 +23863,7 @@ ${scheduleItems}
|
|
|
23312
23863
|
"independent_segments+temp_file",
|
|
23313
23864
|
playlistPath
|
|
23314
23865
|
];
|
|
23315
|
-
ff =
|
|
23866
|
+
ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
23316
23867
|
if (!ff.stdin || !ff.stderr) {
|
|
23317
23868
|
throw new Error("ffmpeg stdio streams not available");
|
|
23318
23869
|
}
|
|
@@ -24784,13 +25335,13 @@ async function pingHost(host, timeoutMs = 3e3) {
|
|
|
24784
25335
|
}
|
|
24785
25336
|
return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
|
|
24786
25337
|
};
|
|
24787
|
-
const { spawn:
|
|
25338
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
24788
25339
|
for (const bin of pingCandidates) {
|
|
24789
25340
|
const ranOk = await new Promise((resolve) => {
|
|
24790
25341
|
let settled = false;
|
|
24791
25342
|
let child;
|
|
24792
25343
|
try {
|
|
24793
|
-
child =
|
|
25344
|
+
child = spawn4(bin, pingArgs(bin), { stdio: "ignore" });
|
|
24794
25345
|
} catch {
|
|
24795
25346
|
resolve("spawn-failed");
|
|
24796
25347
|
return;
|
|
@@ -25444,6 +25995,10 @@ export {
|
|
|
25444
25995
|
Intercom,
|
|
25445
25996
|
BaichuanEventEmitter,
|
|
25446
25997
|
createNativeStream,
|
|
25998
|
+
ALWAYS_ON_DEFAULTS,
|
|
25999
|
+
PlaceholderRenderer,
|
|
26000
|
+
ContinuousVideoStream,
|
|
26001
|
+
AlwaysOnController,
|
|
25447
26002
|
BaichuanRtspServer,
|
|
25448
26003
|
MpegTsMuxer,
|
|
25449
26004
|
flattenAbilitiesForChannel,
|
|
@@ -25497,4 +26052,4 @@ export {
|
|
|
25497
26052
|
tcpReachabilityProbe,
|
|
25498
26053
|
autoDetectDeviceType
|
|
25499
26054
|
};
|
|
25500
|
-
//# sourceMappingURL=chunk-
|
|
26055
|
+
//# sourceMappingURL=chunk-JQ5NSEVD.js.map
|