@apocaliss92/nodelink-js 0.6.3 → 0.6.5

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.
@@ -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: spawn4 } = await import("child_process");
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 = spawn4(ffmpegPath, [
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, import_node_child_process2.spawn)("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
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, import_node_child_process2.spawn)("ffprobe", args, { stdio: ["ignore", "pipe", "pipe"] });
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, import_node_child_process2.spawn)("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
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, import_node_child_process2, import_node_path, REDACT_KEYS, MASK_KEYS, IPV4_RE, MAC_RE;
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
- import_node_child_process2 = require("child_process");
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 import_node_child_process3 = require("child_process");
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 import_node_events2 = require("events");
8181
- var import_node_child_process = require("child_process");
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 import_node_events2.EventEmitter {
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, import_node_child_process.spawn)("ffmpeg", ffmpegArgs, {
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 import_node_events4 = require("events");
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 import_node_events3 = require("events");
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
 
@@ -11340,6 +11850,7 @@ function readCache(uid, now) {
11340
11850
  }
11341
11851
  async function getServerBinding(uid, options = {}) {
11342
11852
  if (!uid || typeof uid !== "string") return void 0;
11853
+ uid = uid.toUpperCase();
11343
11854
  const now = Date.now();
11344
11855
  const cached = readCache(uid, now);
11345
11856
  if (cached?.kind === "ok") return cached.response;
@@ -11650,7 +12161,7 @@ var cachedP2pLookups = /* @__PURE__ */ new Map();
11650
12161
  var negCachedP2pLookups = /* @__PURE__ */ new Map();
11651
12162
  var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
11652
12163
  var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
11653
- var BcUdpStream = class extends import_node_events3.EventEmitter {
12164
+ var BcUdpStream = class extends import_node_events4.EventEmitter {
11654
12165
  opts;
11655
12166
  /**
11656
12167
  * Optional info-level logger for diagnostic milestones — set via
@@ -13113,7 +13624,7 @@ init_xml();
13113
13624
  function isTalkCmd(cmdId) {
13114
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;
13115
13626
  }
13116
- var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmitter {
13627
+ var BaichuanClient = class _BaichuanClient extends import_node_events5.EventEmitter {
13117
13628
  /**
13118
13629
  * Process-wide streaming activity registry.
13119
13630
  *
@@ -16699,7 +17210,7 @@ function buildSetSystemGeneralXml(patch) {
16699
17210
  }
16700
17211
 
16701
17212
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
16702
- var import_jimp = require("jimp");
17213
+ var import_jimp2 = require("jimp");
16703
17214
  init_ReolinkCgiApi();
16704
17215
  init_ReolinkHttpClient();
16705
17216
 
@@ -19659,8 +20170,8 @@ var parseSirenStatusListPushXml = (xml) => {
19659
20170
  };
19660
20171
 
19661
20172
  // src/emailPush/bus.ts
19662
- var import_node_events5 = require("events");
19663
- var emitter = new import_node_events5.EventEmitter();
20173
+ var import_node_events6 = require("events");
20174
+ var emitter = new import_node_events6.EventEmitter();
19664
20175
  function onEmailPushEvent(handler) {
19665
20176
  emitter.on("event", handler);
19666
20177
  return () => emitter.off("event", handler);
@@ -23508,12 +24019,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
23508
24019
  let wideImg;
23509
24020
  let teleImg;
23510
24021
  try {
23511
- wideImg = await import_jimp.Jimp.read(wide);
24022
+ wideImg = await import_jimp2.Jimp.read(wide);
23512
24023
  } catch {
23513
24024
  return wide;
23514
24025
  }
23515
24026
  try {
23516
- teleImg = await import_jimp.Jimp.read(tele);
24027
+ teleImg = await import_jimp2.Jimp.read(tele);
23517
24028
  } catch {
23518
24029
  return wide;
23519
24030
  }
@@ -23547,7 +24058,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
23547
24058
  });
23548
24059
  teleImg.resize({ w: pipW, h: pipH });
23549
24060
  wideImg.composite(teleImg, left, top);
23550
- return await wideImg.getBuffer(import_jimp.JimpMime.jpeg, { quality: 80 });
24061
+ return await wideImg.getBuffer(import_jimp2.JimpMime.jpeg, { quality: 80 });
23551
24062
  }
23552
24063
  const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
23553
24064
  const variant = options?.variant ?? "default";
@@ -24505,7 +25016,7 @@ ${xml}`);
24505
25016
  const chunks = [];
24506
25017
  let stderr = "";
24507
25018
  let timedOut = false;
24508
- const ff = (0, import_node_child_process3.spawn)(params.ffmpegPath, [
25019
+ const ff = (0, import_node_child_process4.spawn)(params.ffmpegPath, [
24509
25020
  "-hide_banner",
24510
25021
  "-loglevel",
24511
25022
  "error",
@@ -24590,7 +25101,7 @@ ${xml}`);
24590
25101
  const chunks = [];
24591
25102
  let stderr = "";
24592
25103
  let timedOut = false;
24593
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25104
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
24594
25105
  "-hide_banner",
24595
25106
  "-loglevel",
24596
25107
  "error",
@@ -24706,7 +25217,7 @@ ${xml}`);
24706
25217
  ensureEnabled: true
24707
25218
  });
24708
25219
  await new Promise((resolve, reject) => {
24709
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25220
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
24710
25221
  "-hide_banner",
24711
25222
  "-loglevel",
24712
25223
  "error",
@@ -24762,7 +25273,7 @@ ${stderr}`));
24762
25273
  const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
24763
25274
  await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(params.outputPath), { recursive: true });
24764
25275
  await new Promise((resolve, reject) => {
24765
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25276
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
24766
25277
  "-hide_banner",
24767
25278
  "-loglevel",
24768
25279
  "error",
@@ -25327,7 +25838,7 @@ ${stderr}`)
25327
25838
  * Convert a raw video keyframe to JPEG using ffmpeg.
25328
25839
  */
25329
25840
  async convertFrameToJpeg(params) {
25330
- const { spawn: spawn4 } = await import("child_process");
25841
+ const { spawn: spawn5 } = await import("child_process");
25331
25842
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
25332
25843
  const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
25333
25844
  return new Promise((resolve, reject) => {
@@ -25349,7 +25860,7 @@ ${stderr}`)
25349
25860
  "2",
25350
25861
  "pipe:1"
25351
25862
  ];
25352
- const proc = spawn4(ffmpeg, args, {
25863
+ const proc = spawn5(ffmpeg, args, {
25353
25864
  stdio: ["pipe", "pipe", "pipe"]
25354
25865
  });
25355
25866
  const chunks = [];
@@ -25492,7 +26003,7 @@ ${stderr}`)
25492
26003
  * Internal helper to mux video+audio into MP4 using ffmpeg.
25493
26004
  */
25494
26005
  async muxToMp4(params) {
25495
- const { spawn: spawn4 } = await import("child_process");
26006
+ const { spawn: spawn5 } = await import("child_process");
25496
26007
  const { randomUUID: randomUUID3 } = await import("crypto");
25497
26008
  const fs5 = await import("fs/promises");
25498
26009
  const os = await import("os");
@@ -25544,7 +26055,7 @@ ${stderr}`)
25544
26055
  outputPath
25545
26056
  );
25546
26057
  await new Promise((resolve, reject) => {
25547
- const p = spawn4(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26058
+ const p = spawn5(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
25548
26059
  let stderr = "";
25549
26060
  p.stderr.on("data", (d) => {
25550
26061
  stderr += d.toString();
@@ -30531,7 +31042,7 @@ ${scheduleItems}
30531
31042
  "mjpeg",
30532
31043
  "pipe:1"
30533
31044
  ];
30534
- const ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31045
+ const ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
30535
31046
  const chunks = [];
30536
31047
  let stderr = "";
30537
31048
  ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
@@ -30655,7 +31166,7 @@ ${scheduleItems}
30655
31166
  "pipe:1"
30656
31167
  ];
30657
31168
  }
30658
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31169
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
30659
31170
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
30660
31171
  throw new Error("ffmpeg stdio streams not available");
30661
31172
  }
@@ -30902,7 +31413,7 @@ ${scheduleItems}
30902
31413
  "mp4",
30903
31414
  "pipe:1"
30904
31415
  ];
30905
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31416
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
30906
31417
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
30907
31418
  throw new Error("ffmpeg stdio streams not available");
30908
31419
  }
@@ -31111,7 +31622,7 @@ ${scheduleItems}
31111
31622
  "independent_segments+temp_file",
31112
31623
  playlistPath
31113
31624
  ];
31114
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31625
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31115
31626
  if (!ff.stdin || !ff.stderr) {
31116
31627
  throw new Error("ffmpeg stdio streams not available");
31117
31628
  }
@@ -31724,13 +32235,13 @@ ${scheduleItems}
31724
32235
  init_constants();
31725
32236
 
31726
32237
  // src/reolink/discovery.ts
31727
- var import_node_child_process4 = require("child_process");
32238
+ var import_node_child_process5 = require("child_process");
31728
32239
  var import_node_crypto4 = require("crypto");
31729
32240
  var import_node_dgram2 = __toESM(require("dgram"), 1);
31730
32241
  var net3 = __toESM(require("net"), 1);
31731
32242
  var import_node_os2 = require("os");
31732
32243
  var import_node_util = require("util");
31733
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process4.execFile);
32244
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process5.execFile);
31734
32245
  async function discoverViaUdpDirect(host, options) {
31735
32246
  if (!options.enableUdpDiscovery) return [];
31736
32247
  const logger = options.logger;
@@ -31927,7 +32438,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
31927
32438
  return methods.filter((m) => m === "local-direct");
31928
32439
  }
31929
32440
  function normalizeUid(uid) {
31930
- const v = uid?.trim();
32441
+ const v = uid?.trim().toUpperCase();
31931
32442
  return v ? v : void 0;
31932
32443
  }
31933
32444
  function maskUid(uid) {
@@ -32006,13 +32517,13 @@ async function pingHost(host, timeoutMs = 3e3) {
32006
32517
  }
32007
32518
  return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
32008
32519
  };
32009
- const { spawn: spawn4 } = await import("child_process");
32520
+ const { spawn: spawn5 } = await import("child_process");
32010
32521
  for (const bin of pingCandidates) {
32011
32522
  const ranOk = await new Promise((resolve) => {
32012
32523
  let settled = false;
32013
32524
  let child;
32014
32525
  try {
32015
- child = spawn4(bin, pingArgs(bin), { stdio: "ignore" });
32526
+ child = spawn5(bin, pingArgs(bin), { stdio: "ignore" });
32016
32527
  } catch {
32017
32528
  resolve("spawn-failed");
32018
32529
  return;