@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
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -4635,12 +4635,12 @@ var init_ReolinkCgiApi = __esm({
|
|
|
4635
4635
|
"getVideoclipThumbnailJpeg",
|
|
4636
4636
|
`Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`
|
|
4637
4637
|
);
|
|
4638
|
-
const { spawn:
|
|
4638
|
+
const { spawn: spawn5 } = await import("child_process");
|
|
4639
4639
|
return new Promise((resolve, reject) => {
|
|
4640
4640
|
const chunks = [];
|
|
4641
4641
|
let stderr = "";
|
|
4642
4642
|
let timedOut = false;
|
|
4643
|
-
const ffmpeg =
|
|
4643
|
+
const ffmpeg = spawn5(ffmpegPath, [
|
|
4644
4644
|
"-y",
|
|
4645
4645
|
"-analyzeduration",
|
|
4646
4646
|
"10000000",
|
|
@@ -5443,7 +5443,7 @@ function spawnFfmpeg(args, logPath) {
|
|
|
5443
5443
|
return new Promise((resolve) => {
|
|
5444
5444
|
mkdirp(path4.dirname(logPath));
|
|
5445
5445
|
const logStream = fs4.createWriteStream(logPath, { flags: "a" });
|
|
5446
|
-
const p = (0,
|
|
5446
|
+
const p = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
5447
5447
|
p.on("error", (e) => {
|
|
5448
5448
|
logStream.write(
|
|
5449
5449
|
`ffmpeg spawn error: ${e instanceof Error ? e.message : String(e)}
|
|
@@ -5474,7 +5474,7 @@ function spawnFfprobeJson(args, logPath) {
|
|
|
5474
5474
|
return new Promise((resolve) => {
|
|
5475
5475
|
mkdirp(path4.dirname(logPath));
|
|
5476
5476
|
const logStream = fs4.createWriteStream(logPath, { flags: "a" });
|
|
5477
|
-
const p = (0,
|
|
5477
|
+
const p = (0, import_node_child_process3.spawn)("ffprobe", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5478
5478
|
p.on("error", (e) => {
|
|
5479
5479
|
const msg = e instanceof Error ? e.message : String(e);
|
|
5480
5480
|
logStream.write(`ffprobe spawn error: ${msg}
|
|
@@ -5582,7 +5582,7 @@ async function testStreamWithFfmpeg(params) {
|
|
|
5582
5582
|
// Output to null (we just want to test connection)
|
|
5583
5583
|
];
|
|
5584
5584
|
return new Promise((resolve) => {
|
|
5585
|
-
const p = (0,
|
|
5585
|
+
const p = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
5586
5586
|
let stderr = "";
|
|
5587
5587
|
let hasData = false;
|
|
5588
5588
|
p.stderr.on("data", (d) => {
|
|
@@ -8129,14 +8129,14 @@ async function captureModelFixtures(params) {
|
|
|
8129
8129
|
}
|
|
8130
8130
|
return { calls, outDir, summary };
|
|
8131
8131
|
}
|
|
8132
|
-
var fs4, path4, import_node_crypto3,
|
|
8132
|
+
var fs4, path4, import_node_crypto3, import_node_child_process3, import_node_path, REDACT_KEYS, MASK_KEYS, IPV4_RE, MAC_RE;
|
|
8133
8133
|
var init_DiagnosticsTools = __esm({
|
|
8134
8134
|
"src/debug/DiagnosticsTools.ts"() {
|
|
8135
8135
|
"use strict";
|
|
8136
8136
|
fs4 = __toESM(require("fs"), 1);
|
|
8137
8137
|
path4 = __toESM(require("path"), 1);
|
|
8138
8138
|
import_node_crypto3 = require("crypto");
|
|
8139
|
-
|
|
8139
|
+
import_node_child_process3 = require("child_process");
|
|
8140
8140
|
init_ReolinkCgiApi();
|
|
8141
8141
|
import_node_path = require("path");
|
|
8142
8142
|
init_zip();
|
|
@@ -8171,14 +8171,14 @@ var init_DiagnosticsTools = __esm({
|
|
|
8171
8171
|
});
|
|
8172
8172
|
|
|
8173
8173
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
8174
|
-
var
|
|
8174
|
+
var import_node_child_process4 = require("child_process");
|
|
8175
8175
|
var import_promises2 = require("fs/promises");
|
|
8176
8176
|
var import_node_path2 = require("path");
|
|
8177
8177
|
var import_node_stream = require("stream");
|
|
8178
8178
|
|
|
8179
8179
|
// src/baichuan/stream/BaichuanRtspServer.ts
|
|
8180
|
-
var
|
|
8181
|
-
var
|
|
8180
|
+
var import_node_events3 = require("events");
|
|
8181
|
+
var import_node_child_process2 = require("child_process");
|
|
8182
8182
|
var net = __toESM(require("net"), 1);
|
|
8183
8183
|
var dgram = __toESM(require("dgram"), 1);
|
|
8184
8184
|
var crypto = __toESM(require("crypto"), 1);
|
|
@@ -8378,6 +8378,358 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
8378
8378
|
}
|
|
8379
8379
|
}
|
|
8380
8380
|
|
|
8381
|
+
// src/baichuan/stream/BaichuanRtspServer.ts
|
|
8382
|
+
init_BaichuanVideoStream();
|
|
8383
|
+
|
|
8384
|
+
// src/baichuan/stream/ContinuousVideoStream.ts
|
|
8385
|
+
var import_node_events2 = require("events");
|
|
8386
|
+
|
|
8387
|
+
// src/baichuan/stream/PlaceholderRenderer.ts
|
|
8388
|
+
var import_node_child_process = require("child_process");
|
|
8389
|
+
var import_jimp = require("jimp");
|
|
8390
|
+
var import_fonts = require("jimp/fonts");
|
|
8391
|
+
|
|
8392
|
+
// src/baichuan/stream/alwaysOnTypes.ts
|
|
8393
|
+
var ALWAYS_ON_DEFAULTS = {
|
|
8394
|
+
triggers: ["motion", "doorbell"],
|
|
8395
|
+
windowMs: 15e3,
|
|
8396
|
+
idleFps: 1,
|
|
8397
|
+
primeOnStart: true,
|
|
8398
|
+
placeholder: { enabled: true, text: "Sleeping", opacity: 0.5 }
|
|
8399
|
+
};
|
|
8400
|
+
|
|
8401
|
+
// src/baichuan/stream/PlaceholderRenderer.ts
|
|
8402
|
+
function ffmpegCodec(videoType) {
|
|
8403
|
+
if (videoType === "H265") {
|
|
8404
|
+
return {
|
|
8405
|
+
inputFormat: "hevc",
|
|
8406
|
+
encoder: "libx265",
|
|
8407
|
+
outputFormat: "hevc"
|
|
8408
|
+
};
|
|
8409
|
+
}
|
|
8410
|
+
return {
|
|
8411
|
+
inputFormat: "h264",
|
|
8412
|
+
encoder: "libx264",
|
|
8413
|
+
outputFormat: "h264"
|
|
8414
|
+
};
|
|
8415
|
+
}
|
|
8416
|
+
function runFfmpeg(args, input) {
|
|
8417
|
+
return new Promise((resolve, reject) => {
|
|
8418
|
+
const proc = (0, import_node_child_process.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
8419
|
+
const stdoutChunks = [];
|
|
8420
|
+
const stderrChunks = [];
|
|
8421
|
+
proc.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
8422
|
+
proc.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
8423
|
+
proc.on("error", (error) => reject(error));
|
|
8424
|
+
proc.on("close", (code) => {
|
|
8425
|
+
if (code === 0) {
|
|
8426
|
+
resolve(Buffer.concat(stdoutChunks));
|
|
8427
|
+
return;
|
|
8428
|
+
}
|
|
8429
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
8430
|
+
reject(new Error(`ffmpeg exited with code ${code}: ${stderr}`));
|
|
8431
|
+
});
|
|
8432
|
+
const stdin = proc.stdin;
|
|
8433
|
+
if (!stdin) {
|
|
8434
|
+
reject(new Error("ffmpeg stdin not available"));
|
|
8435
|
+
return;
|
|
8436
|
+
}
|
|
8437
|
+
stdin.on("error", (error) => reject(error));
|
|
8438
|
+
stdin.end(input);
|
|
8439
|
+
});
|
|
8440
|
+
}
|
|
8441
|
+
var PlaceholderRenderer = class {
|
|
8442
|
+
opts;
|
|
8443
|
+
logger;
|
|
8444
|
+
constructor(args) {
|
|
8445
|
+
this.opts = { ...ALWAYS_ON_DEFAULTS.placeholder, ...args.placeholder ?? {} };
|
|
8446
|
+
this.logger = args.logger;
|
|
8447
|
+
}
|
|
8448
|
+
/** Returns the access unit bytes to emit as placeholder, or null if none available. */
|
|
8449
|
+
async render(keyframe) {
|
|
8450
|
+
if (!keyframe) return null;
|
|
8451
|
+
if (!this.opts.enabled) return keyframe.data;
|
|
8452
|
+
try {
|
|
8453
|
+
const jpeg = await this.decodeToJpeg(keyframe);
|
|
8454
|
+
const decorated = await this.decorate(jpeg);
|
|
8455
|
+
const idr = await this.encodeIdr(decorated, keyframe.videoType);
|
|
8456
|
+
if (!idr || idr.length === 0) {
|
|
8457
|
+
throw new Error("ffmpeg produced empty IDR output");
|
|
8458
|
+
}
|
|
8459
|
+
return idr;
|
|
8460
|
+
} catch (error) {
|
|
8461
|
+
this.logger?.warn?.(
|
|
8462
|
+
"PlaceholderRenderer: decoration failed, falling back to raw keyframe",
|
|
8463
|
+
error instanceof Error ? error.message : error
|
|
8464
|
+
);
|
|
8465
|
+
return keyframe.data;
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8468
|
+
/** Decodes the cached keyframe access unit into a single JPEG still via ffmpeg. */
|
|
8469
|
+
async decodeToJpeg(keyframe) {
|
|
8470
|
+
const { inputFormat } = ffmpegCodec(keyframe.videoType);
|
|
8471
|
+
return runFfmpeg(
|
|
8472
|
+
[
|
|
8473
|
+
"-hide_banner",
|
|
8474
|
+
"-loglevel",
|
|
8475
|
+
"error",
|
|
8476
|
+
"-f",
|
|
8477
|
+
inputFormat,
|
|
8478
|
+
"-i",
|
|
8479
|
+
"pipe:0",
|
|
8480
|
+
"-frames:v",
|
|
8481
|
+
"1",
|
|
8482
|
+
"-f",
|
|
8483
|
+
"mjpeg",
|
|
8484
|
+
"pipe:1"
|
|
8485
|
+
],
|
|
8486
|
+
keyframe.data
|
|
8487
|
+
);
|
|
8488
|
+
}
|
|
8489
|
+
/** Dims the still and prints the overlay text using jimp, returning a JPEG buffer. */
|
|
8490
|
+
async decorate(jpeg) {
|
|
8491
|
+
const image = await import_jimp.Jimp.read(jpeg);
|
|
8492
|
+
const op = Math.max(0, Math.min(1, this.opts.opacity));
|
|
8493
|
+
if (op < 1) {
|
|
8494
|
+
const data = image.bitmap.data;
|
|
8495
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
8496
|
+
data[i] = data[i] * op;
|
|
8497
|
+
data[i + 1] = data[i + 1] * op;
|
|
8498
|
+
data[i + 2] = data[i + 2] * op;
|
|
8499
|
+
}
|
|
8500
|
+
}
|
|
8501
|
+
const fontDef = image.width >= 1280 ? import_fonts.SANS_128_WHITE : image.width >= 640 ? import_fonts.SANS_64_WHITE : import_fonts.SANS_32_WHITE;
|
|
8502
|
+
const font = await (0, import_jimp.loadFont)(fontDef);
|
|
8503
|
+
const text = this.opts.text;
|
|
8504
|
+
const textWidth = (0, import_jimp.measureText)(font, text);
|
|
8505
|
+
const textHeight = (0, import_jimp.measureTextHeight)(font, text, image.width);
|
|
8506
|
+
const x = Math.max(0, Math.round((image.width - textWidth) / 2));
|
|
8507
|
+
const y = Math.max(0, Math.round((image.height - textHeight) / 2));
|
|
8508
|
+
image.print({ font, x, y, text });
|
|
8509
|
+
return image.getBuffer(import_jimp.JimpMime.jpeg);
|
|
8510
|
+
}
|
|
8511
|
+
/** Encodes the decorated JPEG into a single IDR access unit in the target codec. */
|
|
8512
|
+
async encodeIdr(jpeg, videoType) {
|
|
8513
|
+
const { encoder, outputFormat } = ffmpegCodec(videoType);
|
|
8514
|
+
return runFfmpeg(
|
|
8515
|
+
[
|
|
8516
|
+
"-hide_banner",
|
|
8517
|
+
"-loglevel",
|
|
8518
|
+
"error",
|
|
8519
|
+
"-f",
|
|
8520
|
+
"image2pipe",
|
|
8521
|
+
"-i",
|
|
8522
|
+
"pipe:0",
|
|
8523
|
+
"-frames:v",
|
|
8524
|
+
"1",
|
|
8525
|
+
"-c:v",
|
|
8526
|
+
encoder,
|
|
8527
|
+
"-pix_fmt",
|
|
8528
|
+
"yuv420p",
|
|
8529
|
+
"-f",
|
|
8530
|
+
outputFormat,
|
|
8531
|
+
"pipe:1"
|
|
8532
|
+
],
|
|
8533
|
+
jpeg
|
|
8534
|
+
);
|
|
8535
|
+
}
|
|
8536
|
+
};
|
|
8537
|
+
|
|
8538
|
+
// src/baichuan/stream/ContinuousVideoStream.ts
|
|
8539
|
+
var ContinuousVideoStream = class extends import_node_events2.EventEmitter {
|
|
8540
|
+
constructor(opts) {
|
|
8541
|
+
super();
|
|
8542
|
+
this.opts = opts;
|
|
8543
|
+
this.idleFps = Math.max(0.1, opts.idleFps ?? ALWAYS_ON_DEFAULTS.idleFps);
|
|
8544
|
+
this.logger = opts.logger;
|
|
8545
|
+
const rendererArgs = {};
|
|
8546
|
+
if (opts.placeholder !== void 0) rendererArgs.placeholder = opts.placeholder;
|
|
8547
|
+
if (opts.logger !== void 0) rendererArgs.logger = opts.logger;
|
|
8548
|
+
this.renderer = opts.renderer ?? new PlaceholderRenderer(rendererArgs);
|
|
8549
|
+
}
|
|
8550
|
+
live = null;
|
|
8551
|
+
lastKeyframe = null;
|
|
8552
|
+
lastMicroseconds = 0;
|
|
8553
|
+
idleFps;
|
|
8554
|
+
renderer;
|
|
8555
|
+
logger;
|
|
8556
|
+
stopped = false;
|
|
8557
|
+
starting = false;
|
|
8558
|
+
idleTimer = null;
|
|
8559
|
+
idlePlaceholder = null;
|
|
8560
|
+
hasCachedKeyframe() {
|
|
8561
|
+
return this.lastKeyframe !== null;
|
|
8562
|
+
}
|
|
8563
|
+
async goLive() {
|
|
8564
|
+
if (this.stopped || this.live || this.starting) return;
|
|
8565
|
+
this.starting = true;
|
|
8566
|
+
try {
|
|
8567
|
+
this.stopIdleLoop();
|
|
8568
|
+
const stream = await this.opts.createLiveStream();
|
|
8569
|
+
this.live = stream;
|
|
8570
|
+
stream.on("videoAccessUnit", this.onLiveAccessUnit);
|
|
8571
|
+
stream.on("additionalHeader", this.onAdditionalHeader);
|
|
8572
|
+
stream.on("audioFrame", this.onAudioFrame);
|
|
8573
|
+
stream.on("error", this.onLiveError);
|
|
8574
|
+
await stream.start().catch((e) => this.emit("error", e));
|
|
8575
|
+
} finally {
|
|
8576
|
+
this.starting = false;
|
|
8577
|
+
}
|
|
8578
|
+
}
|
|
8579
|
+
async goIdle() {
|
|
8580
|
+
if (!this.live) return;
|
|
8581
|
+
const s = this.live;
|
|
8582
|
+
this.live = null;
|
|
8583
|
+
s.off("videoAccessUnit", this.onLiveAccessUnit);
|
|
8584
|
+
s.off("additionalHeader", this.onAdditionalHeader);
|
|
8585
|
+
s.off("audioFrame", this.onAudioFrame);
|
|
8586
|
+
s.off("error", this.onLiveError);
|
|
8587
|
+
await s.stop().catch(() => {
|
|
8588
|
+
});
|
|
8589
|
+
await this.startIdleLoop();
|
|
8590
|
+
}
|
|
8591
|
+
async stop() {
|
|
8592
|
+
this.stopped = true;
|
|
8593
|
+
await this.goIdle();
|
|
8594
|
+
this.stopIdleLoop();
|
|
8595
|
+
this.emit("close");
|
|
8596
|
+
}
|
|
8597
|
+
async startIdleLoop() {
|
|
8598
|
+
if (this.stopped) return;
|
|
8599
|
+
this.idlePlaceholder = await this.renderer.render(this.lastKeyframe);
|
|
8600
|
+
if (!this.idlePlaceholder || !this.lastKeyframe) {
|
|
8601
|
+
this.logger?.debug?.("[ContinuousVideoStream] no keyframe yet; idle loop deferred");
|
|
8602
|
+
return;
|
|
8603
|
+
}
|
|
8604
|
+
const stepUs = Math.round(1e6 / this.idleFps);
|
|
8605
|
+
const videoType = this.lastKeyframe.videoType;
|
|
8606
|
+
this.idleTimer = setInterval(() => {
|
|
8607
|
+
if (!this.idlePlaceholder) return;
|
|
8608
|
+
this.lastMicroseconds += stepUs;
|
|
8609
|
+
this.emit("videoAccessUnit", {
|
|
8610
|
+
data: this.idlePlaceholder,
|
|
8611
|
+
isKeyframe: true,
|
|
8612
|
+
videoType,
|
|
8613
|
+
microseconds: this.lastMicroseconds
|
|
8614
|
+
});
|
|
8615
|
+
}, Math.round(1e3 / this.idleFps));
|
|
8616
|
+
}
|
|
8617
|
+
stopIdleLoop() {
|
|
8618
|
+
if (this.idleTimer) {
|
|
8619
|
+
clearInterval(this.idleTimer);
|
|
8620
|
+
this.idleTimer = null;
|
|
8621
|
+
}
|
|
8622
|
+
this.idlePlaceholder = null;
|
|
8623
|
+
}
|
|
8624
|
+
onLiveAccessUnit = (au) => {
|
|
8625
|
+
if (au.isKeyframe) {
|
|
8626
|
+
this.lastKeyframe = { data: au.data, videoType: au.videoType };
|
|
8627
|
+
}
|
|
8628
|
+
this.lastMicroseconds = au.microseconds;
|
|
8629
|
+
this.emit("videoAccessUnit", au);
|
|
8630
|
+
};
|
|
8631
|
+
onAdditionalHeader = (h) => this.emit("additionalHeader", h);
|
|
8632
|
+
onAudioFrame = (a) => this.emit("audioFrame", a);
|
|
8633
|
+
onLiveError = (e) => this.emit("error", e);
|
|
8634
|
+
};
|
|
8635
|
+
|
|
8636
|
+
// src/baichuan/stream/AlwaysOnController.ts
|
|
8637
|
+
var AlwaysOnController = class {
|
|
8638
|
+
constructor(o) {
|
|
8639
|
+
this.o = o;
|
|
8640
|
+
this.triggers = new Set(o.options.triggers ?? ALWAYS_ON_DEFAULTS.triggers);
|
|
8641
|
+
this.windowMs = o.options.windowMs ?? ALWAYS_ON_DEFAULTS.windowMs;
|
|
8642
|
+
this.primeOnStart = o.options.primeOnStart ?? ALWAYS_ON_DEFAULTS.primeOnStart;
|
|
8643
|
+
this.logger = o.logger;
|
|
8644
|
+
}
|
|
8645
|
+
triggers;
|
|
8646
|
+
windowMs;
|
|
8647
|
+
primeOnStart;
|
|
8648
|
+
logger;
|
|
8649
|
+
windowTimer = null;
|
|
8650
|
+
live = false;
|
|
8651
|
+
started = false;
|
|
8652
|
+
handler = (e) => void this.onEvent(e);
|
|
8653
|
+
get windowSeconds() {
|
|
8654
|
+
return Math.round(this.windowMs / 1e3);
|
|
8655
|
+
}
|
|
8656
|
+
async start() {
|
|
8657
|
+
if (this.started) return;
|
|
8658
|
+
this.started = true;
|
|
8659
|
+
await this.o.api.onSimpleEvent(this.handler);
|
|
8660
|
+
this.logger?.info?.(
|
|
8661
|
+
`[AlwaysOnController] started ch${this.o.channel} \u2014 triggers=[${[...this.triggers].join(", ")}], window=${this.windowSeconds}s, primeOnStart=${this.primeOnStart}`
|
|
8662
|
+
);
|
|
8663
|
+
if (this.primeOnStart) {
|
|
8664
|
+
await this.openWindow("prime");
|
|
8665
|
+
}
|
|
8666
|
+
}
|
|
8667
|
+
async stop() {
|
|
8668
|
+
if (!this.started) return;
|
|
8669
|
+
this.started = false;
|
|
8670
|
+
if (this.windowTimer) {
|
|
8671
|
+
clearTimeout(this.windowTimer);
|
|
8672
|
+
this.windowTimer = null;
|
|
8673
|
+
}
|
|
8674
|
+
await this.o.api.offSimpleEvent(this.handler).catch(() => {
|
|
8675
|
+
});
|
|
8676
|
+
if (this.live) {
|
|
8677
|
+
this.live = false;
|
|
8678
|
+
await this.o.goIdle().catch(() => {
|
|
8679
|
+
});
|
|
8680
|
+
}
|
|
8681
|
+
this.logger?.info?.(`[AlwaysOnController] stopped ch${this.o.channel}`);
|
|
8682
|
+
}
|
|
8683
|
+
async onEvent(e) {
|
|
8684
|
+
if (e.channel !== this.o.channel) return;
|
|
8685
|
+
if (!this.triggers.has(e.type)) {
|
|
8686
|
+
this.logger?.debug?.(
|
|
8687
|
+
`[AlwaysOnController] event '${e.type}' ch${e.channel} ignored (not a configured trigger)`
|
|
8688
|
+
);
|
|
8689
|
+
return;
|
|
8690
|
+
}
|
|
8691
|
+
await this.openWindow(e.type);
|
|
8692
|
+
}
|
|
8693
|
+
async openWindow(reason) {
|
|
8694
|
+
if (this.windowTimer) clearTimeout(this.windowTimer);
|
|
8695
|
+
if (!this.live) {
|
|
8696
|
+
this.live = true;
|
|
8697
|
+
try {
|
|
8698
|
+
await this.o.api.wakeUp(this.o.channel).catch(() => {
|
|
8699
|
+
});
|
|
8700
|
+
await this.o.goLive();
|
|
8701
|
+
this.logger?.info?.(
|
|
8702
|
+
`[AlwaysOnController] live window OPENED (trigger=${reason}) \u2014 streaming real frames; will sleep in ${this.windowSeconds}s without new events`
|
|
8703
|
+
);
|
|
8704
|
+
} catch (err) {
|
|
8705
|
+
this.live = false;
|
|
8706
|
+
this.logger?.warn?.(
|
|
8707
|
+
`[AlwaysOnController] goLive failed: ${err?.message}`
|
|
8708
|
+
);
|
|
8709
|
+
return;
|
|
8710
|
+
}
|
|
8711
|
+
} else {
|
|
8712
|
+
this.logger?.info?.(
|
|
8713
|
+
`[AlwaysOnController] live window EXTENDED (trigger=${reason}) \u2014 sleep timer reset to ${this.windowSeconds}s`
|
|
8714
|
+
);
|
|
8715
|
+
}
|
|
8716
|
+
this.windowTimer = setTimeout(() => void this.closeWindow(), this.windowMs);
|
|
8717
|
+
}
|
|
8718
|
+
async closeWindow() {
|
|
8719
|
+
this.windowTimer = null;
|
|
8720
|
+
if (!this.live) return;
|
|
8721
|
+
this.live = false;
|
|
8722
|
+
this.logger?.info?.(
|
|
8723
|
+
`[AlwaysOnController] live window CLOSED \u2014 going idle (placeholder); camera can sleep`
|
|
8724
|
+
);
|
|
8725
|
+
await this.o.goIdle().catch(
|
|
8726
|
+
(err) => this.logger?.warn?.(
|
|
8727
|
+
`[AlwaysOnController] goIdle failed: ${err?.message}`
|
|
8728
|
+
)
|
|
8729
|
+
);
|
|
8730
|
+
}
|
|
8731
|
+
};
|
|
8732
|
+
|
|
8381
8733
|
// src/baichuan/stream/rtspFlow.ts
|
|
8382
8734
|
init_H264Converter();
|
|
8383
8735
|
init_H265Converter();
|
|
@@ -8604,7 +8956,7 @@ function envBool(value, defaultValue) {
|
|
|
8604
8956
|
if (v === "0" || v === "false" || v === "no" || v === "off") return false;
|
|
8605
8957
|
return defaultValue;
|
|
8606
8958
|
}
|
|
8607
|
-
var BaichuanRtspServer = class _BaichuanRtspServer extends
|
|
8959
|
+
var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events3.EventEmitter {
|
|
8608
8960
|
api;
|
|
8609
8961
|
channel;
|
|
8610
8962
|
profile;
|
|
@@ -8619,6 +8971,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8619
8971
|
deviceId;
|
|
8620
8972
|
dedicatedSessionRelease;
|
|
8621
8973
|
externalListener;
|
|
8974
|
+
// Always-on continuous stream (battery cameras). Populated only when
|
|
8975
|
+
// `options.alwaysOn?.enabled`; the default (non-alwaysOn) path leaves these
|
|
8976
|
+
// null/undefined and is byte-for-byte equivalent in behaviour.
|
|
8977
|
+
alwaysOnOptions;
|
|
8978
|
+
continuousStream = null;
|
|
8979
|
+
alwaysOnController = null;
|
|
8622
8980
|
// Authentication
|
|
8623
8981
|
authCredentials = [];
|
|
8624
8982
|
requireAuth;
|
|
@@ -8633,6 +8991,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8633
8991
|
// Set of client IDs (IP:port)
|
|
8634
8992
|
nativeStreamActive = false;
|
|
8635
8993
|
// Whether the native stream is currently active
|
|
8994
|
+
tearingDown = false;
|
|
8995
|
+
// True while stop() is running; suppresses onEnd-driven restarts
|
|
8636
8996
|
clientConnectionServer;
|
|
8637
8997
|
// TCP server to track connections
|
|
8638
8998
|
streamMetadata = null;
|
|
@@ -8820,6 +9180,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8820
9180
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
8821
9181
|
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
8822
9182
|
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
9183
|
+
this.alwaysOnOptions = options.alwaysOn;
|
|
8823
9184
|
const transport = this.api.client.getTransport();
|
|
8824
9185
|
this.flow = createRtspFlow(transport, "H264");
|
|
8825
9186
|
}
|
|
@@ -9943,7 +10304,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9943
10304
|
this.rtspDebugLog(
|
|
9944
10305
|
`Spawning ffmpeg for client ${clientId}: ffmpeg ${ffmpegArgs.join(" ")}`
|
|
9945
10306
|
);
|
|
9946
|
-
ffmpeg = (0,
|
|
10307
|
+
ffmpeg = (0, import_node_child_process2.spawn)("ffmpeg", ffmpegArgs, {
|
|
9947
10308
|
stdio
|
|
9948
10309
|
});
|
|
9949
10310
|
try {
|
|
@@ -10307,6 +10668,141 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10307
10668
|
}
|
|
10308
10669
|
});
|
|
10309
10670
|
}
|
|
10671
|
+
/**
|
|
10672
|
+
* Always-on source: bridge a {@link ContinuousVideoStream} into the existing
|
|
10673
|
+
* fanout. Yields the same frame shape that `createNativeStream` produces, so
|
|
10674
|
+
* the rest of the pipeline (prebuffer, param-set extraction, per-client
|
|
10675
|
+
* subscribe, ffmpeg/direct-RTP) is unchanged.
|
|
10676
|
+
*
|
|
10677
|
+
* The CVS itself is long-lived (created once, reused across native-stream
|
|
10678
|
+
* restarts) and is driven by the {@link AlwaysOnController}, which opens/closes
|
|
10679
|
+
* live windows from camera events. Each fanout source generator only forwards
|
|
10680
|
+
* CVS events to the fanout pump for as long as `signal` is not aborted.
|
|
10681
|
+
*/
|
|
10682
|
+
async *createContinuousSource(dedicatedClient, signal) {
|
|
10683
|
+
const cvs = this.ensureContinuousStream(dedicatedClient);
|
|
10684
|
+
const queue = [];
|
|
10685
|
+
const MAX_QUEUE = 200;
|
|
10686
|
+
let wake = null;
|
|
10687
|
+
let done = false;
|
|
10688
|
+
const push = (frame) => {
|
|
10689
|
+
queue.push(frame);
|
|
10690
|
+
if (queue.length > MAX_QUEUE) {
|
|
10691
|
+
queue.splice(0, queue.length - MAX_QUEUE);
|
|
10692
|
+
}
|
|
10693
|
+
if (wake) {
|
|
10694
|
+
const w = wake;
|
|
10695
|
+
wake = null;
|
|
10696
|
+
w();
|
|
10697
|
+
}
|
|
10698
|
+
};
|
|
10699
|
+
const onVideo = (au) => {
|
|
10700
|
+
push({
|
|
10701
|
+
audio: false,
|
|
10702
|
+
data: au.data,
|
|
10703
|
+
codec: null,
|
|
10704
|
+
sampleRate: null,
|
|
10705
|
+
microseconds: au.microseconds,
|
|
10706
|
+
videoType: au.videoType,
|
|
10707
|
+
isKeyframe: au.isKeyframe
|
|
10708
|
+
});
|
|
10709
|
+
};
|
|
10710
|
+
const onAudio = (frame) => {
|
|
10711
|
+
push({
|
|
10712
|
+
audio: true,
|
|
10713
|
+
data: frame,
|
|
10714
|
+
codec: "aac",
|
|
10715
|
+
sampleRate: 8e3,
|
|
10716
|
+
microseconds: null
|
|
10717
|
+
});
|
|
10718
|
+
};
|
|
10719
|
+
const finish = () => {
|
|
10720
|
+
done = true;
|
|
10721
|
+
if (wake) {
|
|
10722
|
+
const w = wake;
|
|
10723
|
+
wake = null;
|
|
10724
|
+
w();
|
|
10725
|
+
}
|
|
10726
|
+
};
|
|
10727
|
+
const onAbort = () => finish();
|
|
10728
|
+
cvs.on("videoAccessUnit", onVideo);
|
|
10729
|
+
cvs.on("audioFrame", onAudio);
|
|
10730
|
+
cvs.on("close", finish);
|
|
10731
|
+
if (signal.aborted) {
|
|
10732
|
+
done = true;
|
|
10733
|
+
} else {
|
|
10734
|
+
signal.addEventListener("abort", onAbort);
|
|
10735
|
+
}
|
|
10736
|
+
try {
|
|
10737
|
+
while (!done && !signal.aborted) {
|
|
10738
|
+
if (queue.length > 0) {
|
|
10739
|
+
yield queue.shift();
|
|
10740
|
+
} else {
|
|
10741
|
+
await new Promise((resolve) => {
|
|
10742
|
+
wake = resolve;
|
|
10743
|
+
if (done || signal.aborted) {
|
|
10744
|
+
wake = null;
|
|
10745
|
+
resolve();
|
|
10746
|
+
}
|
|
10747
|
+
});
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
while (queue.length > 0 && !signal.aborted) {
|
|
10751
|
+
yield queue.shift();
|
|
10752
|
+
}
|
|
10753
|
+
} finally {
|
|
10754
|
+
cvs.off("videoAccessUnit", onVideo);
|
|
10755
|
+
cvs.off("audioFrame", onAudio);
|
|
10756
|
+
cvs.off("close", finish);
|
|
10757
|
+
signal.removeEventListener("abort", onAbort);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
/**
|
|
10761
|
+
* Lazily build the long-lived {@link ContinuousVideoStream} +
|
|
10762
|
+
* {@link AlwaysOnController} for always-on mode. Both are created once and
|
|
10763
|
+
* reused for the lifetime of the server (across native-stream restarts).
|
|
10764
|
+
*/
|
|
10765
|
+
ensureContinuousStream(dedicatedClient) {
|
|
10766
|
+
if (this.continuousStream) return this.continuousStream;
|
|
10767
|
+
const createLiveStream = async () => {
|
|
10768
|
+
const client = dedicatedClient ?? this.api.client;
|
|
10769
|
+
return new BaichuanVideoStream({
|
|
10770
|
+
client,
|
|
10771
|
+
api: this.api,
|
|
10772
|
+
channel: this.channel,
|
|
10773
|
+
profile: this.profile,
|
|
10774
|
+
...this.variant !== "default" ? { variant: this.variant } : {},
|
|
10775
|
+
...this.logger ? { logger: this.logger } : {}
|
|
10776
|
+
});
|
|
10777
|
+
};
|
|
10778
|
+
const cvsOptions = {
|
|
10779
|
+
createLiveStream,
|
|
10780
|
+
...this.alwaysOnOptions?.idleFps !== void 0 ? { idleFps: this.alwaysOnOptions.idleFps } : {},
|
|
10781
|
+
...this.alwaysOnOptions?.placeholder !== void 0 ? { placeholder: this.alwaysOnOptions.placeholder } : {},
|
|
10782
|
+
...this.logger ? { logger: this.logger } : {}
|
|
10783
|
+
};
|
|
10784
|
+
const cvs = new ContinuousVideoStream(cvsOptions);
|
|
10785
|
+
cvs.on("error", (e) => {
|
|
10786
|
+
this.logger.warn(
|
|
10787
|
+
`[BaichuanRtspServer] ContinuousVideoStream error: ${e?.message ?? e}`
|
|
10788
|
+
);
|
|
10789
|
+
});
|
|
10790
|
+
this.continuousStream = cvs;
|
|
10791
|
+
this.alwaysOnController = new AlwaysOnController({
|
|
10792
|
+
api: this.api,
|
|
10793
|
+
channel: this.channel,
|
|
10794
|
+
options: this.alwaysOnOptions,
|
|
10795
|
+
goLive: () => cvs.goLive(),
|
|
10796
|
+
goIdle: () => cvs.goIdle(),
|
|
10797
|
+
...this.logger ? { logger: this.logger } : {}
|
|
10798
|
+
});
|
|
10799
|
+
void this.alwaysOnController.start().catch((e) => {
|
|
10800
|
+
this.logger.warn(
|
|
10801
|
+
`[BaichuanRtspServer] AlwaysOnController start failed: ${e?.message ?? e}`
|
|
10802
|
+
);
|
|
10803
|
+
});
|
|
10804
|
+
return cvs;
|
|
10805
|
+
}
|
|
10310
10806
|
/**
|
|
10311
10807
|
* Start native stream (mark as active).
|
|
10312
10808
|
* Each client will create its own generator, so we just track that the stream is active.
|
|
@@ -10368,7 +10864,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10368
10864
|
await this.flow.startKeepAlive(this.api);
|
|
10369
10865
|
this.nativeFanout = new NativeStreamFanout({
|
|
10370
10866
|
maxQueueItems: 200,
|
|
10371
|
-
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
10867
|
+
createSource: (signal) => this.alwaysOnOptions?.enabled ? this.createContinuousSource(dedicatedClient, signal) : createNativeStream(this.api, this.channel, this.profile, {
|
|
10372
10868
|
variant: this.variant,
|
|
10373
10869
|
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
10374
10870
|
signal
|
|
@@ -10451,6 +10947,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10451
10947
|
} catch {
|
|
10452
10948
|
}
|
|
10453
10949
|
}
|
|
10950
|
+
if (this.tearingDown) return;
|
|
10454
10951
|
if (this.connectedClients.size > 0 && hadFrames) {
|
|
10455
10952
|
this.logger.info(
|
|
10456
10953
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
@@ -10464,7 +10961,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10464
10961
|
});
|
|
10465
10962
|
this.nativeFanout.start();
|
|
10466
10963
|
this.clearNoFrameDeadlineTimer();
|
|
10467
|
-
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
10964
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0 && !this.alwaysOnOptions?.enabled) {
|
|
10468
10965
|
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
10469
10966
|
this.noFrameDeadlineTimer = void 0;
|
|
10470
10967
|
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
@@ -10477,7 +10974,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10477
10974
|
this.noFrameDeadlineTimer?.unref?.();
|
|
10478
10975
|
}
|
|
10479
10976
|
this.clearNoClientAutoStopTimer();
|
|
10480
|
-
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
10977
|
+
if (this.nativeStreamPrimeIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
|
|
10481
10978
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
10482
10979
|
if (this.connectedClients.size === 0) {
|
|
10483
10980
|
this.rtspDebugLog(
|
|
@@ -10564,7 +11061,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10564
11061
|
this.emit("clientDisconnected", clientId);
|
|
10565
11062
|
if (this.connectedClients.size === 0) {
|
|
10566
11063
|
this.clearNoClientAutoStopTimer();
|
|
10567
|
-
if (this.nativeStreamIdleStopMs > 0) {
|
|
11064
|
+
if (this.nativeStreamIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
|
|
10568
11065
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
10569
11066
|
if (this.connectedClients.size === 0) {
|
|
10570
11067
|
void this.stopNativeStream();
|
|
@@ -10635,9 +11132,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
10635
11132
|
if (!this.active) {
|
|
10636
11133
|
return;
|
|
10637
11134
|
}
|
|
11135
|
+
this.tearingDown = true;
|
|
10638
11136
|
this.logger.info(
|
|
10639
11137
|
`[BaichuanRtspServer] Stopping RTSP server on ${this.listenHost}:${this.listenPort}...`
|
|
10640
11138
|
);
|
|
11139
|
+
if (this.alwaysOnController) {
|
|
11140
|
+
const controller = this.alwaysOnController;
|
|
11141
|
+
this.alwaysOnController = null;
|
|
11142
|
+
await controller.stop().catch(() => {
|
|
11143
|
+
});
|
|
11144
|
+
}
|
|
11145
|
+
if (this.continuousStream) {
|
|
11146
|
+
const cvs = this.continuousStream;
|
|
11147
|
+
this.continuousStream = null;
|
|
11148
|
+
await cvs.stop().catch(() => {
|
|
11149
|
+
});
|
|
11150
|
+
}
|
|
10641
11151
|
await this.stopNativeStream();
|
|
10642
11152
|
const clientIds = Array.from(this.connectedClients);
|
|
10643
11153
|
for (const clientId of clientIds) {
|
|
@@ -10977,14 +11487,14 @@ var MpegTsMuxer = class {
|
|
|
10977
11487
|
};
|
|
10978
11488
|
|
|
10979
11489
|
// src/client/BaichuanClient.ts
|
|
10980
|
-
var
|
|
11490
|
+
var import_node_events5 = require("events");
|
|
10981
11491
|
var import_node_crypto2 = require("crypto");
|
|
10982
11492
|
var import_node_net = __toESM(require("net"), 1);
|
|
10983
11493
|
|
|
10984
11494
|
// src/bcudp/BcUdpStream.ts
|
|
10985
11495
|
var import_node_dgram = __toESM(require("dgram"), 1);
|
|
10986
11496
|
var import_promises = __toESM(require("dns/promises"), 1);
|
|
10987
|
-
var
|
|
11497
|
+
var import_node_events4 = require("events");
|
|
10988
11498
|
var import_node_os = require("os");
|
|
10989
11499
|
var import_node_timers = require("timers");
|
|
10990
11500
|
|
|
@@ -11651,7 +12161,7 @@ var cachedP2pLookups = /* @__PURE__ */ new Map();
|
|
|
11651
12161
|
var negCachedP2pLookups = /* @__PURE__ */ new Map();
|
|
11652
12162
|
var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
|
|
11653
12163
|
var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
|
|
11654
|
-
var BcUdpStream = class extends
|
|
12164
|
+
var BcUdpStream = class extends import_node_events4.EventEmitter {
|
|
11655
12165
|
opts;
|
|
11656
12166
|
/**
|
|
11657
12167
|
* Optional info-level logger for diagnostic milestones — set via
|
|
@@ -13114,7 +13624,7 @@ init_xml();
|
|
|
13114
13624
|
function isTalkCmd(cmdId) {
|
|
13115
13625
|
return cmdId === BC_CMD_ID_TALK_ABILITY || cmdId === BC_CMD_ID_TALK_RESET || cmdId === BC_CMD_ID_TALK_CONFIG || cmdId === BC_CMD_ID_TALK;
|
|
13116
13626
|
}
|
|
13117
|
-
var BaichuanClient = class _BaichuanClient extends
|
|
13627
|
+
var BaichuanClient = class _BaichuanClient extends import_node_events5.EventEmitter {
|
|
13118
13628
|
/**
|
|
13119
13629
|
* Process-wide streaming activity registry.
|
|
13120
13630
|
*
|
|
@@ -16517,6 +17027,30 @@ function buildSetNtpXml(current, patch) {
|
|
|
16517
17027
|
);
|
|
16518
17028
|
}
|
|
16519
17029
|
|
|
17030
|
+
// src/reolink/baichuan/utils/channelEnumeration.ts
|
|
17031
|
+
async function resolveBaichuanChannels(deps) {
|
|
17032
|
+
const fromPush = dedupeSorted(deps.pushChannels);
|
|
17033
|
+
if (fromPush.length > 0) return fromPush;
|
|
17034
|
+
const slots = dedupeSorted(deps.supportChnIds);
|
|
17035
|
+
const candidates = slots.length > 0 ? slots : [0];
|
|
17036
|
+
const probed = await Promise.all(
|
|
17037
|
+
candidates.map(
|
|
17038
|
+
async (channel) => await deps.probe(channel) ? channel : void 0
|
|
17039
|
+
)
|
|
17040
|
+
);
|
|
17041
|
+
return dedupeSorted(
|
|
17042
|
+
probed.filter((c) => c !== void 0)
|
|
17043
|
+
);
|
|
17044
|
+
}
|
|
17045
|
+
function dedupeSorted(values) {
|
|
17046
|
+
const set = /* @__PURE__ */ new Set();
|
|
17047
|
+
for (const v of values) {
|
|
17048
|
+
const n = Number(v);
|
|
17049
|
+
if (Number.isFinite(n) && n >= 0) set.add(n);
|
|
17050
|
+
}
|
|
17051
|
+
return [...set].sort((a, b) => a - b);
|
|
17052
|
+
}
|
|
17053
|
+
|
|
16520
17054
|
// src/reolink/baichuan/utils/dst.ts
|
|
16521
17055
|
init_xml();
|
|
16522
17056
|
var parseNumberSafe3 = (text) => {
|
|
@@ -16700,7 +17234,7 @@ function buildSetSystemGeneralXml(patch) {
|
|
|
16700
17234
|
}
|
|
16701
17235
|
|
|
16702
17236
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
16703
|
-
var
|
|
17237
|
+
var import_jimp2 = require("jimp");
|
|
16704
17238
|
init_ReolinkCgiApi();
|
|
16705
17239
|
init_ReolinkHttpClient();
|
|
16706
17240
|
|
|
@@ -19660,8 +20194,8 @@ var parseSirenStatusListPushXml = (xml) => {
|
|
|
19660
20194
|
};
|
|
19661
20195
|
|
|
19662
20196
|
// src/emailPush/bus.ts
|
|
19663
|
-
var
|
|
19664
|
-
var emitter = new
|
|
20197
|
+
var import_node_events6 = require("events");
|
|
20198
|
+
var emitter = new import_node_events6.EventEmitter();
|
|
19665
20199
|
function onEmailPushEvent(handler) {
|
|
19666
20200
|
emitter.on("event", handler);
|
|
19667
20201
|
return () => emitter.off("event", handler);
|
|
@@ -22895,13 +23429,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
22895
23429
|
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
22896
23430
|
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
22897
23431
|
* no dependency on async push messages. Recommended for first-call discovery.
|
|
22898
|
-
* - `"baichuan"`:
|
|
22899
|
-
*
|
|
22900
|
-
*
|
|
22901
|
-
*
|
|
23432
|
+
* - `"baichuan"`: HTTP-free discovery. Prefers the cmd_id 145 push cache when
|
|
23433
|
+
* populated; otherwise actively probes the channel slots advertised by Support
|
|
23434
|
+
* (`items[].chnID`) via `getInfo`. Use this for hubs with HTTP disabled.
|
|
23435
|
+
*
|
|
23436
|
+
* When the api was constructed with `nativeOnly`, the source is forced to
|
|
23437
|
+
* `"baichuan"` regardless of this option (no HTTP/CGI is ever attempted).
|
|
22902
23438
|
*/
|
|
22903
23439
|
async getNvrChannelsSummary(options) {
|
|
22904
|
-
const source = options?.source ?? "cgi";
|
|
23440
|
+
const source = this.nativeOnly ? "baichuan" : options?.source ?? "cgi";
|
|
23441
|
+
const support = await this.getSupportInfo().catch(() => {
|
|
23442
|
+
this.logger.error?.(
|
|
23443
|
+
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
23444
|
+
);
|
|
23445
|
+
return void 0;
|
|
23446
|
+
});
|
|
22905
23447
|
let channels;
|
|
22906
23448
|
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
22907
23449
|
if (options?.channels?.length) {
|
|
@@ -22931,15 +23473,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
22931
23473
|
channels = [];
|
|
22932
23474
|
}
|
|
22933
23475
|
} else {
|
|
22934
|
-
const
|
|
22935
|
-
|
|
23476
|
+
const pushChannels = Array.from(
|
|
23477
|
+
this.getChannelInfoFromPushCache().keys()
|
|
23478
|
+
).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
23479
|
+
const supportChnIds = (support?.items ?? []).map((i) => Number(i.chnID)).filter((n) => Number.isFinite(n));
|
|
23480
|
+
const probeTimeoutMs = options?.timeoutMs ?? 2500;
|
|
23481
|
+
channels = await resolveBaichuanChannels({
|
|
23482
|
+
pushChannels,
|
|
23483
|
+
supportChnIds,
|
|
23484
|
+
probe: async (channel) => {
|
|
23485
|
+
try {
|
|
23486
|
+
await this.getInfo(channel, {
|
|
23487
|
+
timeoutMs: probeTimeoutMs,
|
|
23488
|
+
tags: ["type", "name"]
|
|
23489
|
+
});
|
|
23490
|
+
return true;
|
|
23491
|
+
} catch {
|
|
23492
|
+
return false;
|
|
23493
|
+
}
|
|
23494
|
+
}
|
|
23495
|
+
});
|
|
23496
|
+
this.logger.debug?.(
|
|
23497
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: baichuan resolved ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
23498
|
+
);
|
|
22936
23499
|
}
|
|
22937
23500
|
channels = channels.sort((a, b) => a - b);
|
|
22938
|
-
const support = await this.getSupportInfo().catch(() => {
|
|
22939
|
-
this.logger.error?.(
|
|
22940
|
-
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
22941
|
-
);
|
|
22942
|
-
});
|
|
22943
23501
|
const truthyNumberLike = (v) => {
|
|
22944
23502
|
if (typeof v === "number") return v > 0;
|
|
22945
23503
|
if (typeof v === "string") {
|
|
@@ -23509,12 +24067,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
23509
24067
|
let wideImg;
|
|
23510
24068
|
let teleImg;
|
|
23511
24069
|
try {
|
|
23512
|
-
wideImg = await
|
|
24070
|
+
wideImg = await import_jimp2.Jimp.read(wide);
|
|
23513
24071
|
} catch {
|
|
23514
24072
|
return wide;
|
|
23515
24073
|
}
|
|
23516
24074
|
try {
|
|
23517
|
-
teleImg = await
|
|
24075
|
+
teleImg = await import_jimp2.Jimp.read(tele);
|
|
23518
24076
|
} catch {
|
|
23519
24077
|
return wide;
|
|
23520
24078
|
}
|
|
@@ -23548,7 +24106,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
23548
24106
|
});
|
|
23549
24107
|
teleImg.resize({ w: pipW, h: pipH });
|
|
23550
24108
|
wideImg.composite(teleImg, left, top);
|
|
23551
|
-
return await wideImg.getBuffer(
|
|
24109
|
+
return await wideImg.getBuffer(import_jimp2.JimpMime.jpeg, { quality: 80 });
|
|
23552
24110
|
}
|
|
23553
24111
|
const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
|
|
23554
24112
|
const variant = options?.variant ?? "default";
|
|
@@ -24506,7 +25064,7 @@ ${xml}`);
|
|
|
24506
25064
|
const chunks = [];
|
|
24507
25065
|
let stderr = "";
|
|
24508
25066
|
let timedOut = false;
|
|
24509
|
-
const ff = (0,
|
|
25067
|
+
const ff = (0, import_node_child_process4.spawn)(params.ffmpegPath, [
|
|
24510
25068
|
"-hide_banner",
|
|
24511
25069
|
"-loglevel",
|
|
24512
25070
|
"error",
|
|
@@ -24591,7 +25149,7 @@ ${xml}`);
|
|
|
24591
25149
|
const chunks = [];
|
|
24592
25150
|
let stderr = "";
|
|
24593
25151
|
let timedOut = false;
|
|
24594
|
-
const ff = (0,
|
|
25152
|
+
const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
|
|
24595
25153
|
"-hide_banner",
|
|
24596
25154
|
"-loglevel",
|
|
24597
25155
|
"error",
|
|
@@ -24707,7 +25265,7 @@ ${xml}`);
|
|
|
24707
25265
|
ensureEnabled: true
|
|
24708
25266
|
});
|
|
24709
25267
|
await new Promise((resolve, reject) => {
|
|
24710
|
-
const ff = (0,
|
|
25268
|
+
const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
|
|
24711
25269
|
"-hide_banner",
|
|
24712
25270
|
"-loglevel",
|
|
24713
25271
|
"error",
|
|
@@ -24763,7 +25321,7 @@ ${stderr}`));
|
|
|
24763
25321
|
const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
|
|
24764
25322
|
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(params.outputPath), { recursive: true });
|
|
24765
25323
|
await new Promise((resolve, reject) => {
|
|
24766
|
-
const ff = (0,
|
|
25324
|
+
const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
|
|
24767
25325
|
"-hide_banner",
|
|
24768
25326
|
"-loglevel",
|
|
24769
25327
|
"error",
|
|
@@ -25328,7 +25886,7 @@ ${stderr}`)
|
|
|
25328
25886
|
* Convert a raw video keyframe to JPEG using ffmpeg.
|
|
25329
25887
|
*/
|
|
25330
25888
|
async convertFrameToJpeg(params) {
|
|
25331
|
-
const { spawn:
|
|
25889
|
+
const { spawn: spawn5 } = await import("child_process");
|
|
25332
25890
|
const ffmpeg = params.ffmpegPath ?? "ffmpeg";
|
|
25333
25891
|
const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
|
|
25334
25892
|
return new Promise((resolve, reject) => {
|
|
@@ -25350,7 +25908,7 @@ ${stderr}`)
|
|
|
25350
25908
|
"2",
|
|
25351
25909
|
"pipe:1"
|
|
25352
25910
|
];
|
|
25353
|
-
const proc =
|
|
25911
|
+
const proc = spawn5(ffmpeg, args, {
|
|
25354
25912
|
stdio: ["pipe", "pipe", "pipe"]
|
|
25355
25913
|
});
|
|
25356
25914
|
const chunks = [];
|
|
@@ -25493,7 +26051,7 @@ ${stderr}`)
|
|
|
25493
26051
|
* Internal helper to mux video+audio into MP4 using ffmpeg.
|
|
25494
26052
|
*/
|
|
25495
26053
|
async muxToMp4(params) {
|
|
25496
|
-
const { spawn:
|
|
26054
|
+
const { spawn: spawn5 } = await import("child_process");
|
|
25497
26055
|
const { randomUUID: randomUUID3 } = await import("crypto");
|
|
25498
26056
|
const fs5 = await import("fs/promises");
|
|
25499
26057
|
const os = await import("os");
|
|
@@ -25545,7 +26103,7 @@ ${stderr}`)
|
|
|
25545
26103
|
outputPath
|
|
25546
26104
|
);
|
|
25547
26105
|
await new Promise((resolve, reject) => {
|
|
25548
|
-
const p =
|
|
26106
|
+
const p = spawn5(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
25549
26107
|
let stderr = "";
|
|
25550
26108
|
p.stderr.on("data", (d) => {
|
|
25551
26109
|
stderr += d.toString();
|
|
@@ -30532,7 +31090,7 @@ ${scheduleItems}
|
|
|
30532
31090
|
"mjpeg",
|
|
30533
31091
|
"pipe:1"
|
|
30534
31092
|
];
|
|
30535
|
-
const ff = (0,
|
|
31093
|
+
const ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
30536
31094
|
const chunks = [];
|
|
30537
31095
|
let stderr = "";
|
|
30538
31096
|
ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
|
|
@@ -30656,7 +31214,7 @@ ${scheduleItems}
|
|
|
30656
31214
|
"pipe:1"
|
|
30657
31215
|
];
|
|
30658
31216
|
}
|
|
30659
|
-
ff = (0,
|
|
31217
|
+
ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
30660
31218
|
if (!ff.stdin || !ff.stdout || !ff.stderr) {
|
|
30661
31219
|
throw new Error("ffmpeg stdio streams not available");
|
|
30662
31220
|
}
|
|
@@ -30903,7 +31461,7 @@ ${scheduleItems}
|
|
|
30903
31461
|
"mp4",
|
|
30904
31462
|
"pipe:1"
|
|
30905
31463
|
];
|
|
30906
|
-
ff = (0,
|
|
31464
|
+
ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
30907
31465
|
if (!ff.stdin || !ff.stdout || !ff.stderr) {
|
|
30908
31466
|
throw new Error("ffmpeg stdio streams not available");
|
|
30909
31467
|
}
|
|
@@ -31112,7 +31670,7 @@ ${scheduleItems}
|
|
|
31112
31670
|
"independent_segments+temp_file",
|
|
31113
31671
|
playlistPath
|
|
31114
31672
|
];
|
|
31115
|
-
ff = (0,
|
|
31673
|
+
ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
31116
31674
|
if (!ff.stdin || !ff.stderr) {
|
|
31117
31675
|
throw new Error("ffmpeg stdio streams not available");
|
|
31118
31676
|
}
|
|
@@ -31725,13 +32283,13 @@ ${scheduleItems}
|
|
|
31725
32283
|
init_constants();
|
|
31726
32284
|
|
|
31727
32285
|
// src/reolink/discovery.ts
|
|
31728
|
-
var
|
|
32286
|
+
var import_node_child_process5 = require("child_process");
|
|
31729
32287
|
var import_node_crypto4 = require("crypto");
|
|
31730
32288
|
var import_node_dgram2 = __toESM(require("dgram"), 1);
|
|
31731
32289
|
var net3 = __toESM(require("net"), 1);
|
|
31732
32290
|
var import_node_os2 = require("os");
|
|
31733
32291
|
var import_node_util = require("util");
|
|
31734
|
-
var execFileAsync = (0, import_node_util.promisify)(
|
|
32292
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process5.execFile);
|
|
31735
32293
|
async function discoverViaUdpDirect(host, options) {
|
|
31736
32294
|
if (!options.enableUdpDiscovery) return [];
|
|
31737
32295
|
const logger = options.logger;
|
|
@@ -32007,13 +32565,13 @@ async function pingHost(host, timeoutMs = 3e3) {
|
|
|
32007
32565
|
}
|
|
32008
32566
|
return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
|
|
32009
32567
|
};
|
|
32010
|
-
const { spawn:
|
|
32568
|
+
const { spawn: spawn5 } = await import("child_process");
|
|
32011
32569
|
for (const bin of pingCandidates) {
|
|
32012
32570
|
const ranOk = await new Promise((resolve) => {
|
|
32013
32571
|
let settled = false;
|
|
32014
32572
|
let child;
|
|
32015
32573
|
try {
|
|
32016
|
-
child =
|
|
32574
|
+
child = spawn5(bin, pingArgs(bin), { stdio: "ignore" });
|
|
32017
32575
|
} catch {
|
|
32018
32576
|
resolve("spawn-failed");
|
|
32019
32577
|
return;
|