@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/dist/index.cjs CHANGED
@@ -7678,12 +7678,12 @@ var init_ReolinkCgiApi = __esm({
7678
7678
  "getVideoclipThumbnailJpeg",
7679
7679
  `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`
7680
7680
  );
7681
- const { spawn: spawn13 } = await import("child_process");
7681
+ const { spawn: spawn14 } = await import("child_process");
7682
7682
  return new Promise((resolve, reject) => {
7683
7683
  const chunks = [];
7684
7684
  let stderr = "";
7685
7685
  let timedOut = false;
7686
- const ffmpeg = spawn13(ffmpegPath, [
7686
+ const ffmpeg = spawn14(ffmpegPath, [
7687
7687
  "-y",
7688
7688
  "-analyzeduration",
7689
7689
  "10000000",
@@ -8267,7 +8267,9 @@ var init_ReolinkCgiApi = __esm({
8267
8267
  var index_exports = {};
8268
8268
  __export(index_exports, {
8269
8269
  ALL_UDP_DISCOVERY_METHODS: () => ALL_UDP_DISCOVERY_METHODS,
8270
+ ALWAYS_ON_DEFAULTS: () => ALWAYS_ON_DEFAULTS,
8270
8271
  AesStreamDecryptor: () => AesStreamDecryptor,
8272
+ AlwaysOnController: () => AlwaysOnController,
8271
8273
  AutodiscoveryClient: () => AutodiscoveryClient,
8272
8274
  BC_AES_IV: () => BC_AES_IV,
8273
8275
  BC_CLASS_FILE_DOWNLOAD: () => BC_CLASS_FILE_DOWNLOAD,
@@ -8424,6 +8426,7 @@ __export(index_exports, {
8424
8426
  BcUdpStream: () => BcUdpStream,
8425
8427
  CompositeRtspServer: () => CompositeRtspServer,
8426
8428
  CompositeStream: () => CompositeStream,
8429
+ ContinuousVideoStream: () => ContinuousVideoStream,
8427
8430
  DEFAULT_SHELTER_CANVAS: () => DEFAULT_SHELTER_CANVAS,
8428
8431
  DUAL_LENS_DUAL_MOTION_MODELS: () => DUAL_LENS_DUAL_MOTION_MODELS,
8429
8432
  DUAL_LENS_MODELS: () => DUAL_LENS_MODELS,
@@ -8437,6 +8440,7 @@ __export(index_exports, {
8437
8440
  MpegTsMuxer: () => MpegTsMuxer,
8438
8441
  NVR_HUB_EXACT_TYPES: () => NVR_HUB_EXACT_TYPES,
8439
8442
  NVR_HUB_MODEL_PATTERNS: () => NVR_HUB_MODEL_PATTERNS,
8443
+ PlaceholderRenderer: () => PlaceholderRenderer,
8440
8444
  ReolinkBaichuanApi: () => ReolinkBaichuanApi,
8441
8445
  ReolinkCgiApi: () => ReolinkCgiApi,
8442
8446
  ReolinkHttpClient: () => ReolinkHttpClient,
@@ -14068,14 +14072,14 @@ init_ReolinkHttpClient();
14068
14072
  init_ReolinkCgiApi();
14069
14073
 
14070
14074
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
14071
- var import_node_child_process3 = require("child_process");
14075
+ var import_node_child_process4 = require("child_process");
14072
14076
  var import_promises2 = require("fs/promises");
14073
14077
  var import_node_path2 = require("path");
14074
14078
  var import_node_stream = require("stream");
14075
14079
 
14076
14080
  // src/baichuan/stream/BaichuanRtspServer.ts
14077
- var import_node_events4 = require("events");
14078
- var import_node_child_process2 = require("child_process");
14081
+ var import_node_events5 = require("events");
14082
+ var import_node_child_process3 = require("child_process");
14079
14083
  var net2 = __toESM(require("net"), 1);
14080
14084
  var dgram2 = __toESM(require("dgram"), 1);
14081
14085
  var crypto = __toESM(require("crypto"), 1);
@@ -14436,6 +14440,358 @@ async function* createNativeStream(api, channel, profile, options) {
14436
14440
  }
14437
14441
  }
14438
14442
 
14443
+ // src/baichuan/stream/BaichuanRtspServer.ts
14444
+ init_BaichuanVideoStream();
14445
+
14446
+ // src/baichuan/stream/ContinuousVideoStream.ts
14447
+ var import_node_events4 = require("events");
14448
+
14449
+ // src/baichuan/stream/PlaceholderRenderer.ts
14450
+ var import_node_child_process2 = require("child_process");
14451
+ var import_jimp = require("jimp");
14452
+ var import_fonts = require("jimp/fonts");
14453
+
14454
+ // src/baichuan/stream/alwaysOnTypes.ts
14455
+ var ALWAYS_ON_DEFAULTS = {
14456
+ triggers: ["motion", "doorbell"],
14457
+ windowMs: 15e3,
14458
+ idleFps: 1,
14459
+ primeOnStart: true,
14460
+ placeholder: { enabled: true, text: "Sleeping", opacity: 0.5 }
14461
+ };
14462
+
14463
+ // src/baichuan/stream/PlaceholderRenderer.ts
14464
+ function ffmpegCodec(videoType) {
14465
+ if (videoType === "H265") {
14466
+ return {
14467
+ inputFormat: "hevc",
14468
+ encoder: "libx265",
14469
+ outputFormat: "hevc"
14470
+ };
14471
+ }
14472
+ return {
14473
+ inputFormat: "h264",
14474
+ encoder: "libx264",
14475
+ outputFormat: "h264"
14476
+ };
14477
+ }
14478
+ function runFfmpeg(args, input) {
14479
+ return new Promise((resolve, reject) => {
14480
+ const proc = (0, import_node_child_process2.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
14481
+ const stdoutChunks = [];
14482
+ const stderrChunks = [];
14483
+ proc.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
14484
+ proc.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
14485
+ proc.on("error", (error) => reject(error));
14486
+ proc.on("close", (code) => {
14487
+ if (code === 0) {
14488
+ resolve(Buffer.concat(stdoutChunks));
14489
+ return;
14490
+ }
14491
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
14492
+ reject(new Error(`ffmpeg exited with code ${code}: ${stderr}`));
14493
+ });
14494
+ const stdin = proc.stdin;
14495
+ if (!stdin) {
14496
+ reject(new Error("ffmpeg stdin not available"));
14497
+ return;
14498
+ }
14499
+ stdin.on("error", (error) => reject(error));
14500
+ stdin.end(input);
14501
+ });
14502
+ }
14503
+ var PlaceholderRenderer = class {
14504
+ opts;
14505
+ logger;
14506
+ constructor(args) {
14507
+ this.opts = { ...ALWAYS_ON_DEFAULTS.placeholder, ...args.placeholder ?? {} };
14508
+ this.logger = args.logger;
14509
+ }
14510
+ /** Returns the access unit bytes to emit as placeholder, or null if none available. */
14511
+ async render(keyframe) {
14512
+ if (!keyframe) return null;
14513
+ if (!this.opts.enabled) return keyframe.data;
14514
+ try {
14515
+ const jpeg = await this.decodeToJpeg(keyframe);
14516
+ const decorated = await this.decorate(jpeg);
14517
+ const idr = await this.encodeIdr(decorated, keyframe.videoType);
14518
+ if (!idr || idr.length === 0) {
14519
+ throw new Error("ffmpeg produced empty IDR output");
14520
+ }
14521
+ return idr;
14522
+ } catch (error) {
14523
+ this.logger?.warn?.(
14524
+ "PlaceholderRenderer: decoration failed, falling back to raw keyframe",
14525
+ error instanceof Error ? error.message : error
14526
+ );
14527
+ return keyframe.data;
14528
+ }
14529
+ }
14530
+ /** Decodes the cached keyframe access unit into a single JPEG still via ffmpeg. */
14531
+ async decodeToJpeg(keyframe) {
14532
+ const { inputFormat } = ffmpegCodec(keyframe.videoType);
14533
+ return runFfmpeg(
14534
+ [
14535
+ "-hide_banner",
14536
+ "-loglevel",
14537
+ "error",
14538
+ "-f",
14539
+ inputFormat,
14540
+ "-i",
14541
+ "pipe:0",
14542
+ "-frames:v",
14543
+ "1",
14544
+ "-f",
14545
+ "mjpeg",
14546
+ "pipe:1"
14547
+ ],
14548
+ keyframe.data
14549
+ );
14550
+ }
14551
+ /** Dims the still and prints the overlay text using jimp, returning a JPEG buffer. */
14552
+ async decorate(jpeg) {
14553
+ const image = await import_jimp.Jimp.read(jpeg);
14554
+ const op = Math.max(0, Math.min(1, this.opts.opacity));
14555
+ if (op < 1) {
14556
+ const data = image.bitmap.data;
14557
+ for (let i = 0; i < data.length; i += 4) {
14558
+ data[i] = data[i] * op;
14559
+ data[i + 1] = data[i + 1] * op;
14560
+ data[i + 2] = data[i + 2] * op;
14561
+ }
14562
+ }
14563
+ const fontDef = image.width >= 1280 ? import_fonts.SANS_128_WHITE : image.width >= 640 ? import_fonts.SANS_64_WHITE : import_fonts.SANS_32_WHITE;
14564
+ const font = await (0, import_jimp.loadFont)(fontDef);
14565
+ const text = this.opts.text;
14566
+ const textWidth = (0, import_jimp.measureText)(font, text);
14567
+ const textHeight = (0, import_jimp.measureTextHeight)(font, text, image.width);
14568
+ const x = Math.max(0, Math.round((image.width - textWidth) / 2));
14569
+ const y = Math.max(0, Math.round((image.height - textHeight) / 2));
14570
+ image.print({ font, x, y, text });
14571
+ return image.getBuffer(import_jimp.JimpMime.jpeg);
14572
+ }
14573
+ /** Encodes the decorated JPEG into a single IDR access unit in the target codec. */
14574
+ async encodeIdr(jpeg, videoType) {
14575
+ const { encoder, outputFormat } = ffmpegCodec(videoType);
14576
+ return runFfmpeg(
14577
+ [
14578
+ "-hide_banner",
14579
+ "-loglevel",
14580
+ "error",
14581
+ "-f",
14582
+ "image2pipe",
14583
+ "-i",
14584
+ "pipe:0",
14585
+ "-frames:v",
14586
+ "1",
14587
+ "-c:v",
14588
+ encoder,
14589
+ "-pix_fmt",
14590
+ "yuv420p",
14591
+ "-f",
14592
+ outputFormat,
14593
+ "pipe:1"
14594
+ ],
14595
+ jpeg
14596
+ );
14597
+ }
14598
+ };
14599
+
14600
+ // src/baichuan/stream/ContinuousVideoStream.ts
14601
+ var ContinuousVideoStream = class extends import_node_events4.EventEmitter {
14602
+ constructor(opts) {
14603
+ super();
14604
+ this.opts = opts;
14605
+ this.idleFps = Math.max(0.1, opts.idleFps ?? ALWAYS_ON_DEFAULTS.idleFps);
14606
+ this.logger = opts.logger;
14607
+ const rendererArgs = {};
14608
+ if (opts.placeholder !== void 0) rendererArgs.placeholder = opts.placeholder;
14609
+ if (opts.logger !== void 0) rendererArgs.logger = opts.logger;
14610
+ this.renderer = opts.renderer ?? new PlaceholderRenderer(rendererArgs);
14611
+ }
14612
+ live = null;
14613
+ lastKeyframe = null;
14614
+ lastMicroseconds = 0;
14615
+ idleFps;
14616
+ renderer;
14617
+ logger;
14618
+ stopped = false;
14619
+ starting = false;
14620
+ idleTimer = null;
14621
+ idlePlaceholder = null;
14622
+ hasCachedKeyframe() {
14623
+ return this.lastKeyframe !== null;
14624
+ }
14625
+ async goLive() {
14626
+ if (this.stopped || this.live || this.starting) return;
14627
+ this.starting = true;
14628
+ try {
14629
+ this.stopIdleLoop();
14630
+ const stream = await this.opts.createLiveStream();
14631
+ this.live = stream;
14632
+ stream.on("videoAccessUnit", this.onLiveAccessUnit);
14633
+ stream.on("additionalHeader", this.onAdditionalHeader);
14634
+ stream.on("audioFrame", this.onAudioFrame);
14635
+ stream.on("error", this.onLiveError);
14636
+ await stream.start().catch((e) => this.emit("error", e));
14637
+ } finally {
14638
+ this.starting = false;
14639
+ }
14640
+ }
14641
+ async goIdle() {
14642
+ if (!this.live) return;
14643
+ const s = this.live;
14644
+ this.live = null;
14645
+ s.off("videoAccessUnit", this.onLiveAccessUnit);
14646
+ s.off("additionalHeader", this.onAdditionalHeader);
14647
+ s.off("audioFrame", this.onAudioFrame);
14648
+ s.off("error", this.onLiveError);
14649
+ await s.stop().catch(() => {
14650
+ });
14651
+ await this.startIdleLoop();
14652
+ }
14653
+ async stop() {
14654
+ this.stopped = true;
14655
+ await this.goIdle();
14656
+ this.stopIdleLoop();
14657
+ this.emit("close");
14658
+ }
14659
+ async startIdleLoop() {
14660
+ if (this.stopped) return;
14661
+ this.idlePlaceholder = await this.renderer.render(this.lastKeyframe);
14662
+ if (!this.idlePlaceholder || !this.lastKeyframe) {
14663
+ this.logger?.debug?.("[ContinuousVideoStream] no keyframe yet; idle loop deferred");
14664
+ return;
14665
+ }
14666
+ const stepUs = Math.round(1e6 / this.idleFps);
14667
+ const videoType = this.lastKeyframe.videoType;
14668
+ this.idleTimer = setInterval(() => {
14669
+ if (!this.idlePlaceholder) return;
14670
+ this.lastMicroseconds += stepUs;
14671
+ this.emit("videoAccessUnit", {
14672
+ data: this.idlePlaceholder,
14673
+ isKeyframe: true,
14674
+ videoType,
14675
+ microseconds: this.lastMicroseconds
14676
+ });
14677
+ }, Math.round(1e3 / this.idleFps));
14678
+ }
14679
+ stopIdleLoop() {
14680
+ if (this.idleTimer) {
14681
+ clearInterval(this.idleTimer);
14682
+ this.idleTimer = null;
14683
+ }
14684
+ this.idlePlaceholder = null;
14685
+ }
14686
+ onLiveAccessUnit = (au) => {
14687
+ if (au.isKeyframe) {
14688
+ this.lastKeyframe = { data: au.data, videoType: au.videoType };
14689
+ }
14690
+ this.lastMicroseconds = au.microseconds;
14691
+ this.emit("videoAccessUnit", au);
14692
+ };
14693
+ onAdditionalHeader = (h) => this.emit("additionalHeader", h);
14694
+ onAudioFrame = (a) => this.emit("audioFrame", a);
14695
+ onLiveError = (e) => this.emit("error", e);
14696
+ };
14697
+
14698
+ // src/baichuan/stream/AlwaysOnController.ts
14699
+ var AlwaysOnController = class {
14700
+ constructor(o) {
14701
+ this.o = o;
14702
+ this.triggers = new Set(o.options.triggers ?? ALWAYS_ON_DEFAULTS.triggers);
14703
+ this.windowMs = o.options.windowMs ?? ALWAYS_ON_DEFAULTS.windowMs;
14704
+ this.primeOnStart = o.options.primeOnStart ?? ALWAYS_ON_DEFAULTS.primeOnStart;
14705
+ this.logger = o.logger;
14706
+ }
14707
+ triggers;
14708
+ windowMs;
14709
+ primeOnStart;
14710
+ logger;
14711
+ windowTimer = null;
14712
+ live = false;
14713
+ started = false;
14714
+ handler = (e) => void this.onEvent(e);
14715
+ get windowSeconds() {
14716
+ return Math.round(this.windowMs / 1e3);
14717
+ }
14718
+ async start() {
14719
+ if (this.started) return;
14720
+ this.started = true;
14721
+ await this.o.api.onSimpleEvent(this.handler);
14722
+ this.logger?.info?.(
14723
+ `[AlwaysOnController] started ch${this.o.channel} \u2014 triggers=[${[...this.triggers].join(", ")}], window=${this.windowSeconds}s, primeOnStart=${this.primeOnStart}`
14724
+ );
14725
+ if (this.primeOnStart) {
14726
+ await this.openWindow("prime");
14727
+ }
14728
+ }
14729
+ async stop() {
14730
+ if (!this.started) return;
14731
+ this.started = false;
14732
+ if (this.windowTimer) {
14733
+ clearTimeout(this.windowTimer);
14734
+ this.windowTimer = null;
14735
+ }
14736
+ await this.o.api.offSimpleEvent(this.handler).catch(() => {
14737
+ });
14738
+ if (this.live) {
14739
+ this.live = false;
14740
+ await this.o.goIdle().catch(() => {
14741
+ });
14742
+ }
14743
+ this.logger?.info?.(`[AlwaysOnController] stopped ch${this.o.channel}`);
14744
+ }
14745
+ async onEvent(e) {
14746
+ if (e.channel !== this.o.channel) return;
14747
+ if (!this.triggers.has(e.type)) {
14748
+ this.logger?.debug?.(
14749
+ `[AlwaysOnController] event '${e.type}' ch${e.channel} ignored (not a configured trigger)`
14750
+ );
14751
+ return;
14752
+ }
14753
+ await this.openWindow(e.type);
14754
+ }
14755
+ async openWindow(reason) {
14756
+ if (this.windowTimer) clearTimeout(this.windowTimer);
14757
+ if (!this.live) {
14758
+ this.live = true;
14759
+ try {
14760
+ await this.o.api.wakeUp(this.o.channel).catch(() => {
14761
+ });
14762
+ await this.o.goLive();
14763
+ this.logger?.info?.(
14764
+ `[AlwaysOnController] live window OPENED (trigger=${reason}) \u2014 streaming real frames; will sleep in ${this.windowSeconds}s without new events`
14765
+ );
14766
+ } catch (err) {
14767
+ this.live = false;
14768
+ this.logger?.warn?.(
14769
+ `[AlwaysOnController] goLive failed: ${err?.message}`
14770
+ );
14771
+ return;
14772
+ }
14773
+ } else {
14774
+ this.logger?.info?.(
14775
+ `[AlwaysOnController] live window EXTENDED (trigger=${reason}) \u2014 sleep timer reset to ${this.windowSeconds}s`
14776
+ );
14777
+ }
14778
+ this.windowTimer = setTimeout(() => void this.closeWindow(), this.windowMs);
14779
+ }
14780
+ async closeWindow() {
14781
+ this.windowTimer = null;
14782
+ if (!this.live) return;
14783
+ this.live = false;
14784
+ this.logger?.info?.(
14785
+ `[AlwaysOnController] live window CLOSED \u2014 going idle (placeholder); camera can sleep`
14786
+ );
14787
+ await this.o.goIdle().catch(
14788
+ (err) => this.logger?.warn?.(
14789
+ `[AlwaysOnController] goIdle failed: ${err?.message}`
14790
+ )
14791
+ );
14792
+ }
14793
+ };
14794
+
14439
14795
  // src/baichuan/stream/rtspFlow.ts
14440
14796
  init_H264Converter();
14441
14797
  init_H265Converter();
@@ -14662,7 +15018,7 @@ function envBool(value, defaultValue) {
14662
15018
  if (v === "0" || v === "false" || v === "no" || v === "off") return false;
14663
15019
  return defaultValue;
14664
15020
  }
14665
- var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.EventEmitter {
15021
+ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.EventEmitter {
14666
15022
  api;
14667
15023
  channel;
14668
15024
  profile;
@@ -14677,6 +15033,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14677
15033
  deviceId;
14678
15034
  dedicatedSessionRelease;
14679
15035
  externalListener;
15036
+ // Always-on continuous stream (battery cameras). Populated only when
15037
+ // `options.alwaysOn?.enabled`; the default (non-alwaysOn) path leaves these
15038
+ // null/undefined and is byte-for-byte equivalent in behaviour.
15039
+ alwaysOnOptions;
15040
+ continuousStream = null;
15041
+ alwaysOnController = null;
14680
15042
  // Authentication
14681
15043
  authCredentials = [];
14682
15044
  requireAuth;
@@ -14691,6 +15053,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14691
15053
  // Set of client IDs (IP:port)
14692
15054
  nativeStreamActive = false;
14693
15055
  // Whether the native stream is currently active
15056
+ tearingDown = false;
15057
+ // True while stop() is running; suppresses onEnd-driven restarts
14694
15058
  clientConnectionServer;
14695
15059
  // TCP server to track connections
14696
15060
  streamMetadata = null;
@@ -14878,6 +15242,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14878
15242
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
14879
15243
  this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
14880
15244
  this.lazyMetadata = options.lazyMetadata ?? false;
15245
+ this.alwaysOnOptions = options.alwaysOn;
14881
15246
  const transport = this.api.client.getTransport();
14882
15247
  this.flow = createRtspFlow(transport, "H264");
14883
15248
  }
@@ -16001,7 +16366,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16001
16366
  this.rtspDebugLog(
16002
16367
  `Spawning ffmpeg for client ${clientId}: ffmpeg ${ffmpegArgs.join(" ")}`
16003
16368
  );
16004
- ffmpeg = (0, import_node_child_process2.spawn)("ffmpeg", ffmpegArgs, {
16369
+ ffmpeg = (0, import_node_child_process3.spawn)("ffmpeg", ffmpegArgs, {
16005
16370
  stdio
16006
16371
  });
16007
16372
  try {
@@ -16365,6 +16730,141 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16365
16730
  }
16366
16731
  });
16367
16732
  }
16733
+ /**
16734
+ * Always-on source: bridge a {@link ContinuousVideoStream} into the existing
16735
+ * fanout. Yields the same frame shape that `createNativeStream` produces, so
16736
+ * the rest of the pipeline (prebuffer, param-set extraction, per-client
16737
+ * subscribe, ffmpeg/direct-RTP) is unchanged.
16738
+ *
16739
+ * The CVS itself is long-lived (created once, reused across native-stream
16740
+ * restarts) and is driven by the {@link AlwaysOnController}, which opens/closes
16741
+ * live windows from camera events. Each fanout source generator only forwards
16742
+ * CVS events to the fanout pump for as long as `signal` is not aborted.
16743
+ */
16744
+ async *createContinuousSource(dedicatedClient, signal) {
16745
+ const cvs = this.ensureContinuousStream(dedicatedClient);
16746
+ const queue = [];
16747
+ const MAX_QUEUE = 200;
16748
+ let wake = null;
16749
+ let done = false;
16750
+ const push = (frame) => {
16751
+ queue.push(frame);
16752
+ if (queue.length > MAX_QUEUE) {
16753
+ queue.splice(0, queue.length - MAX_QUEUE);
16754
+ }
16755
+ if (wake) {
16756
+ const w = wake;
16757
+ wake = null;
16758
+ w();
16759
+ }
16760
+ };
16761
+ const onVideo = (au) => {
16762
+ push({
16763
+ audio: false,
16764
+ data: au.data,
16765
+ codec: null,
16766
+ sampleRate: null,
16767
+ microseconds: au.microseconds,
16768
+ videoType: au.videoType,
16769
+ isKeyframe: au.isKeyframe
16770
+ });
16771
+ };
16772
+ const onAudio = (frame) => {
16773
+ push({
16774
+ audio: true,
16775
+ data: frame,
16776
+ codec: "aac",
16777
+ sampleRate: 8e3,
16778
+ microseconds: null
16779
+ });
16780
+ };
16781
+ const finish = () => {
16782
+ done = true;
16783
+ if (wake) {
16784
+ const w = wake;
16785
+ wake = null;
16786
+ w();
16787
+ }
16788
+ };
16789
+ const onAbort = () => finish();
16790
+ cvs.on("videoAccessUnit", onVideo);
16791
+ cvs.on("audioFrame", onAudio);
16792
+ cvs.on("close", finish);
16793
+ if (signal.aborted) {
16794
+ done = true;
16795
+ } else {
16796
+ signal.addEventListener("abort", onAbort);
16797
+ }
16798
+ try {
16799
+ while (!done && !signal.aborted) {
16800
+ if (queue.length > 0) {
16801
+ yield queue.shift();
16802
+ } else {
16803
+ await new Promise((resolve) => {
16804
+ wake = resolve;
16805
+ if (done || signal.aborted) {
16806
+ wake = null;
16807
+ resolve();
16808
+ }
16809
+ });
16810
+ }
16811
+ }
16812
+ while (queue.length > 0 && !signal.aborted) {
16813
+ yield queue.shift();
16814
+ }
16815
+ } finally {
16816
+ cvs.off("videoAccessUnit", onVideo);
16817
+ cvs.off("audioFrame", onAudio);
16818
+ cvs.off("close", finish);
16819
+ signal.removeEventListener("abort", onAbort);
16820
+ }
16821
+ }
16822
+ /**
16823
+ * Lazily build the long-lived {@link ContinuousVideoStream} +
16824
+ * {@link AlwaysOnController} for always-on mode. Both are created once and
16825
+ * reused for the lifetime of the server (across native-stream restarts).
16826
+ */
16827
+ ensureContinuousStream(dedicatedClient) {
16828
+ if (this.continuousStream) return this.continuousStream;
16829
+ const createLiveStream = async () => {
16830
+ const client = dedicatedClient ?? this.api.client;
16831
+ return new BaichuanVideoStream({
16832
+ client,
16833
+ api: this.api,
16834
+ channel: this.channel,
16835
+ profile: this.profile,
16836
+ ...this.variant !== "default" ? { variant: this.variant } : {},
16837
+ ...this.logger ? { logger: this.logger } : {}
16838
+ });
16839
+ };
16840
+ const cvsOptions = {
16841
+ createLiveStream,
16842
+ ...this.alwaysOnOptions?.idleFps !== void 0 ? { idleFps: this.alwaysOnOptions.idleFps } : {},
16843
+ ...this.alwaysOnOptions?.placeholder !== void 0 ? { placeholder: this.alwaysOnOptions.placeholder } : {},
16844
+ ...this.logger ? { logger: this.logger } : {}
16845
+ };
16846
+ const cvs = new ContinuousVideoStream(cvsOptions);
16847
+ cvs.on("error", (e) => {
16848
+ this.logger.warn(
16849
+ `[BaichuanRtspServer] ContinuousVideoStream error: ${e?.message ?? e}`
16850
+ );
16851
+ });
16852
+ this.continuousStream = cvs;
16853
+ this.alwaysOnController = new AlwaysOnController({
16854
+ api: this.api,
16855
+ channel: this.channel,
16856
+ options: this.alwaysOnOptions,
16857
+ goLive: () => cvs.goLive(),
16858
+ goIdle: () => cvs.goIdle(),
16859
+ ...this.logger ? { logger: this.logger } : {}
16860
+ });
16861
+ void this.alwaysOnController.start().catch((e) => {
16862
+ this.logger.warn(
16863
+ `[BaichuanRtspServer] AlwaysOnController start failed: ${e?.message ?? e}`
16864
+ );
16865
+ });
16866
+ return cvs;
16867
+ }
16368
16868
  /**
16369
16869
  * Start native stream (mark as active).
16370
16870
  * Each client will create its own generator, so we just track that the stream is active.
@@ -16426,7 +16926,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16426
16926
  await this.flow.startKeepAlive(this.api);
16427
16927
  this.nativeFanout = new NativeStreamFanout({
16428
16928
  maxQueueItems: 200,
16429
- createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
16929
+ createSource: (signal) => this.alwaysOnOptions?.enabled ? this.createContinuousSource(dedicatedClient, signal) : createNativeStream(this.api, this.channel, this.profile, {
16430
16930
  variant: this.variant,
16431
16931
  ...dedicatedClient ? { client: dedicatedClient } : {},
16432
16932
  signal
@@ -16509,6 +17009,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16509
17009
  } catch {
16510
17010
  }
16511
17011
  }
17012
+ if (this.tearingDown) return;
16512
17013
  if (this.connectedClients.size > 0 && hadFrames) {
16513
17014
  this.logger.info(
16514
17015
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
@@ -16522,7 +17023,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16522
17023
  });
16523
17024
  this.nativeFanout.start();
16524
17025
  this.clearNoFrameDeadlineTimer();
16525
- if (this.nativeStreamNoFrameDeadlineMs > 0) {
17026
+ if (this.nativeStreamNoFrameDeadlineMs > 0 && !this.alwaysOnOptions?.enabled) {
16526
17027
  this.noFrameDeadlineTimer = setTimeout(() => {
16527
17028
  this.noFrameDeadlineTimer = void 0;
16528
17029
  if (!this.firstFrameReceived && this.nativeStreamActive) {
@@ -16535,7 +17036,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16535
17036
  this.noFrameDeadlineTimer?.unref?.();
16536
17037
  }
16537
17038
  this.clearNoClientAutoStopTimer();
16538
- if (this.nativeStreamPrimeIdleStopMs > 0) {
17039
+ if (this.nativeStreamPrimeIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
16539
17040
  this.noClientAutoStopTimer = setTimeout(() => {
16540
17041
  if (this.connectedClients.size === 0) {
16541
17042
  this.rtspDebugLog(
@@ -16622,7 +17123,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16622
17123
  this.emit("clientDisconnected", clientId);
16623
17124
  if (this.connectedClients.size === 0) {
16624
17125
  this.clearNoClientAutoStopTimer();
16625
- if (this.nativeStreamIdleStopMs > 0) {
17126
+ if (this.nativeStreamIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
16626
17127
  this.noClientAutoStopTimer = setTimeout(() => {
16627
17128
  if (this.connectedClients.size === 0) {
16628
17129
  void this.stopNativeStream();
@@ -16693,9 +17194,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16693
17194
  if (!this.active) {
16694
17195
  return;
16695
17196
  }
17197
+ this.tearingDown = true;
16696
17198
  this.logger.info(
16697
17199
  `[BaichuanRtspServer] Stopping RTSP server on ${this.listenHost}:${this.listenPort}...`
16698
17200
  );
17201
+ if (this.alwaysOnController) {
17202
+ const controller = this.alwaysOnController;
17203
+ this.alwaysOnController = null;
17204
+ await controller.stop().catch(() => {
17205
+ });
17206
+ }
17207
+ if (this.continuousStream) {
17208
+ const cvs = this.continuousStream;
17209
+ this.continuousStream = null;
17210
+ await cvs.stop().catch(() => {
17211
+ });
17212
+ }
16699
17213
  await this.stopNativeStream();
16700
17214
  const clientIds = Array.from(this.connectedClients);
16701
17215
  for (const clientId of clientIds) {
@@ -17186,6 +17700,30 @@ function buildSetNtpXml(current, patch) {
17186
17700
  );
17187
17701
  }
17188
17702
 
17703
+ // src/reolink/baichuan/utils/channelEnumeration.ts
17704
+ async function resolveBaichuanChannels(deps) {
17705
+ const fromPush = dedupeSorted(deps.pushChannels);
17706
+ if (fromPush.length > 0) return fromPush;
17707
+ const slots = dedupeSorted(deps.supportChnIds);
17708
+ const candidates = slots.length > 0 ? slots : [0];
17709
+ const probed = await Promise.all(
17710
+ candidates.map(
17711
+ async (channel) => await deps.probe(channel) ? channel : void 0
17712
+ )
17713
+ );
17714
+ return dedupeSorted(
17715
+ probed.filter((c) => c !== void 0)
17716
+ );
17717
+ }
17718
+ function dedupeSorted(values) {
17719
+ const set = /* @__PURE__ */ new Set();
17720
+ for (const v of values) {
17721
+ const n = Number(v);
17722
+ if (Number.isFinite(n) && n >= 0) set.add(n);
17723
+ }
17724
+ return [...set].sort((a, b) => a - b);
17725
+ }
17726
+
17189
17727
  // src/reolink/baichuan/utils/dst.ts
17190
17728
  init_xml();
17191
17729
  var parseNumberSafe3 = (text) => {
@@ -17369,7 +17907,7 @@ function buildSetSystemGeneralXml(patch) {
17369
17907
  }
17370
17908
 
17371
17909
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
17372
- var import_jimp = require("jimp");
17910
+ var import_jimp2 = require("jimp");
17373
17911
  init_ReolinkCgiApi();
17374
17912
  init_ReolinkHttpClient();
17375
17913
 
@@ -20329,8 +20867,8 @@ var parseSirenStatusListPushXml = (xml) => {
20329
20867
  };
20330
20868
 
20331
20869
  // src/emailPush/bus.ts
20332
- var import_node_events5 = require("events");
20333
- var emitter = new import_node_events5.EventEmitter();
20870
+ var import_node_events6 = require("events");
20871
+ var emitter = new import_node_events6.EventEmitter();
20334
20872
  var cameraResolver = () => void 0;
20335
20873
  var lastEventByCamera = /* @__PURE__ */ new Map();
20336
20874
  var MAX_GLOBAL_EVENTS = 300;
@@ -23595,13 +24133,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
23595
24133
  * @param options.source - Data source for the channel list (default: `"cgi"`):
23596
24134
  * - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
23597
24135
  * no dependency on async push messages. Recommended for first-call discovery.
23598
- * - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
23599
- * info after login + event subscription. This push is *asynchronous*: if it has not
23600
- * arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
23601
- * with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
24136
+ * - `"baichuan"`: HTTP-free discovery. Prefers the cmd_id 145 push cache when
24137
+ * populated; otherwise actively probes the channel slots advertised by Support
24138
+ * (`items[].chnID`) via `getInfo`. Use this for hubs with HTTP disabled.
24139
+ *
24140
+ * When the api was constructed with `nativeOnly`, the source is forced to
24141
+ * `"baichuan"` regardless of this option (no HTTP/CGI is ever attempted).
23602
24142
  */
23603
24143
  async getNvrChannelsSummary(options) {
23604
- const source = options?.source ?? "cgi";
24144
+ const source = this.nativeOnly ? "baichuan" : options?.source ?? "cgi";
24145
+ const support = await this.getSupportInfo().catch(() => {
24146
+ this.logger.error?.(
24147
+ "[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
24148
+ );
24149
+ return void 0;
24150
+ });
23605
24151
  let channels;
23606
24152
  const cgiStatusByChannel = /* @__PURE__ */ new Map();
23607
24153
  if (options?.channels?.length) {
@@ -23631,15 +24177,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
23631
24177
  channels = [];
23632
24178
  }
23633
24179
  } else {
23634
- const pushInfo2 = this.getChannelInfoFromPushCache();
23635
- channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
24180
+ const pushChannels = Array.from(
24181
+ this.getChannelInfoFromPushCache().keys()
24182
+ ).map((c) => Number(c)).filter((n) => Number.isFinite(n));
24183
+ const supportChnIds = (support?.items ?? []).map((i) => Number(i.chnID)).filter((n) => Number.isFinite(n));
24184
+ const probeTimeoutMs = options?.timeoutMs ?? 2500;
24185
+ channels = await resolveBaichuanChannels({
24186
+ pushChannels,
24187
+ supportChnIds,
24188
+ probe: async (channel) => {
24189
+ try {
24190
+ await this.getInfo(channel, {
24191
+ timeoutMs: probeTimeoutMs,
24192
+ tags: ["type", "name"]
24193
+ });
24194
+ return true;
24195
+ } catch {
24196
+ return false;
24197
+ }
24198
+ }
24199
+ });
24200
+ this.logger.debug?.(
24201
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: baichuan resolved ${channels.length} channel(s): [${channels.join(", ")}]`
24202
+ );
23636
24203
  }
23637
24204
  channels = channels.sort((a, b) => a - b);
23638
- const support = await this.getSupportInfo().catch(() => {
23639
- this.logger.error?.(
23640
- "[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
23641
- );
23642
- });
23643
24205
  const truthyNumberLike = (v) => {
23644
24206
  if (typeof v === "number") return v > 0;
23645
24207
  if (typeof v === "string") {
@@ -24209,12 +24771,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
24209
24771
  let wideImg;
24210
24772
  let teleImg;
24211
24773
  try {
24212
- wideImg = await import_jimp.Jimp.read(wide);
24774
+ wideImg = await import_jimp2.Jimp.read(wide);
24213
24775
  } catch {
24214
24776
  return wide;
24215
24777
  }
24216
24778
  try {
24217
- teleImg = await import_jimp.Jimp.read(tele);
24779
+ teleImg = await import_jimp2.Jimp.read(tele);
24218
24780
  } catch {
24219
24781
  return wide;
24220
24782
  }
@@ -24248,7 +24810,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
24248
24810
  });
24249
24811
  teleImg.resize({ w: pipW, h: pipH });
24250
24812
  wideImg.composite(teleImg, left, top);
24251
- return await wideImg.getBuffer(import_jimp.JimpMime.jpeg, { quality: 80 });
24813
+ return await wideImg.getBuffer(import_jimp2.JimpMime.jpeg, { quality: 80 });
24252
24814
  }
24253
24815
  const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
24254
24816
  const variant = options?.variant ?? "default";
@@ -25206,7 +25768,7 @@ ${xml}`);
25206
25768
  const chunks = [];
25207
25769
  let stderr = "";
25208
25770
  let timedOut = false;
25209
- const ff = (0, import_node_child_process3.spawn)(params.ffmpegPath, [
25771
+ const ff = (0, import_node_child_process4.spawn)(params.ffmpegPath, [
25210
25772
  "-hide_banner",
25211
25773
  "-loglevel",
25212
25774
  "error",
@@ -25291,7 +25853,7 @@ ${xml}`);
25291
25853
  const chunks = [];
25292
25854
  let stderr = "";
25293
25855
  let timedOut = false;
25294
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25856
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25295
25857
  "-hide_banner",
25296
25858
  "-loglevel",
25297
25859
  "error",
@@ -25407,7 +25969,7 @@ ${xml}`);
25407
25969
  ensureEnabled: true
25408
25970
  });
25409
25971
  await new Promise((resolve, reject) => {
25410
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25972
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25411
25973
  "-hide_banner",
25412
25974
  "-loglevel",
25413
25975
  "error",
@@ -25463,7 +26025,7 @@ ${stderr}`));
25463
26025
  const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
25464
26026
  await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(params.outputPath), { recursive: true });
25465
26027
  await new Promise((resolve, reject) => {
25466
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
26028
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25467
26029
  "-hide_banner",
25468
26030
  "-loglevel",
25469
26031
  "error",
@@ -26028,7 +26590,7 @@ ${stderr}`)
26028
26590
  * Convert a raw video keyframe to JPEG using ffmpeg.
26029
26591
  */
26030
26592
  async convertFrameToJpeg(params) {
26031
- const { spawn: spawn13 } = await import("child_process");
26593
+ const { spawn: spawn14 } = await import("child_process");
26032
26594
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
26033
26595
  const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
26034
26596
  return new Promise((resolve, reject) => {
@@ -26050,7 +26612,7 @@ ${stderr}`)
26050
26612
  "2",
26051
26613
  "pipe:1"
26052
26614
  ];
26053
- const proc = spawn13(ffmpeg, args, {
26615
+ const proc = spawn14(ffmpeg, args, {
26054
26616
  stdio: ["pipe", "pipe", "pipe"]
26055
26617
  });
26056
26618
  const chunks = [];
@@ -26193,7 +26755,7 @@ ${stderr}`)
26193
26755
  * Internal helper to mux video+audio into MP4 using ffmpeg.
26194
26756
  */
26195
26757
  async muxToMp4(params) {
26196
- const { spawn: spawn13 } = await import("child_process");
26758
+ const { spawn: spawn14 } = await import("child_process");
26197
26759
  const { randomUUID: randomUUID3 } = await import("crypto");
26198
26760
  const fs7 = await import("fs/promises");
26199
26761
  const os2 = await import("os");
@@ -26245,7 +26807,7 @@ ${stderr}`)
26245
26807
  outputPath
26246
26808
  );
26247
26809
  await new Promise((resolve, reject) => {
26248
- const p = spawn13(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26810
+ const p = spawn14(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26249
26811
  let stderr = "";
26250
26812
  p.stderr.on("data", (d) => {
26251
26813
  stderr += d.toString();
@@ -31232,7 +31794,7 @@ ${scheduleItems}
31232
31794
  "mjpeg",
31233
31795
  "pipe:1"
31234
31796
  ];
31235
- const ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31797
+ const ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31236
31798
  const chunks = [];
31237
31799
  let stderr = "";
31238
31800
  ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
@@ -31356,7 +31918,7 @@ ${scheduleItems}
31356
31918
  "pipe:1"
31357
31919
  ];
31358
31920
  }
31359
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31921
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31360
31922
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
31361
31923
  throw new Error("ffmpeg stdio streams not available");
31362
31924
  }
@@ -31603,7 +32165,7 @@ ${scheduleItems}
31603
32165
  "mp4",
31604
32166
  "pipe:1"
31605
32167
  ];
31606
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
32168
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31607
32169
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
31608
32170
  throw new Error("ffmpeg stdio streams not available");
31609
32171
  }
@@ -31812,7 +32374,7 @@ ${scheduleItems}
31812
32374
  "independent_segments+temp_file",
31813
32375
  playlistPath
31814
32376
  ];
31815
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
32377
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31816
32378
  if (!ff.stdin || !ff.stderr) {
31817
32379
  throw new Error("ffmpeg stdio streams not available");
31818
32380
  }
@@ -32784,14 +33346,14 @@ function buildHlsRedirectUrl(originalUrl) {
32784
33346
  }
32785
33347
 
32786
33348
  // src/reolink/discovery.ts
32787
- var import_node_child_process4 = require("child_process");
33349
+ var import_node_child_process5 = require("child_process");
32788
33350
  var import_node_crypto4 = require("crypto");
32789
33351
  var import_node_dgram2 = __toESM(require("dgram"), 1);
32790
33352
  var net3 = __toESM(require("net"), 1);
32791
33353
  var import_node_os2 = require("os");
32792
33354
  var import_node_util = require("util");
32793
33355
  init_ReolinkCgiApi();
32794
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process4.execFile);
33356
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process5.execFile);
32795
33357
  async function discoverViaUdpDirect(host, options) {
32796
33358
  if (!options.enableUdpDiscovery) return [];
32797
33359
  const logger = options.logger;
@@ -33833,7 +34395,7 @@ init_recordingFileName();
33833
34395
 
33834
34396
  // src/reolink/baichuan/endpoints-server.ts
33835
34397
  var import_node_http = __toESM(require("http"), 1);
33836
- var import_node_child_process5 = require("child_process");
34398
+ var import_node_child_process6 = require("child_process");
33837
34399
  function parseIntParam(v, def) {
33838
34400
  if (v == null) return def;
33839
34401
  const n = Number.parseInt(v, 10);
@@ -34072,7 +34634,7 @@ function createBaichuanEndpointsServer(opts) {
34072
34634
  "Cache-Control": "no-cache",
34073
34635
  Connection: "close"
34074
34636
  });
34075
- const ff2 = (0, import_node_child_process5.spawn)("ffmpeg", [
34637
+ const ff2 = (0, import_node_child_process6.spawn)("ffmpeg", [
34076
34638
  "-hide_banner",
34077
34639
  "-loglevel",
34078
34640
  "error",
@@ -34105,7 +34667,7 @@ function createBaichuanEndpointsServer(opts) {
34105
34667
  );
34106
34668
  res.setHeader("Cache-Control", "no-cache");
34107
34669
  res.setHeader("Connection", "close");
34108
- const ff = (0, import_node_child_process5.spawn)("ffmpeg", [
34670
+ const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
34109
34671
  "-hide_banner",
34110
34672
  "-loglevel",
34111
34673
  "error",
@@ -34216,7 +34778,7 @@ init_urls();
34216
34778
 
34217
34779
  // src/rtsp/server.ts
34218
34780
  var import_node_http2 = __toESM(require("http"), 1);
34219
- var import_node_child_process6 = require("child_process");
34781
+ var import_node_child_process7 = require("child_process");
34220
34782
  init_urls();
34221
34783
  function createRtspProxyServer(opts) {
34222
34784
  return import_node_http2.default.createServer((req, res) => {
@@ -34257,7 +34819,7 @@ function createRtspProxyServer(opts) {
34257
34819
  Connection: "close"
34258
34820
  });
34259
34821
  const rtspTransport = opts.rtspTransport ?? "tcp";
34260
- const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
34822
+ const ff = (0, import_node_child_process7.spawn)("ffmpeg", [
34261
34823
  "-hide_banner",
34262
34824
  "-loglevel",
34263
34825
  "error",
@@ -35129,9 +35691,9 @@ var import_node_net2 = __toESM(require("net"), 1);
35129
35691
  init_BaichuanVideoStream();
35130
35692
 
35131
35693
  // src/multifocal/compositeStream.ts
35132
- var import_node_child_process7 = require("child_process");
35694
+ var import_node_child_process8 = require("child_process");
35133
35695
  var import_node_crypto6 = require("crypto");
35134
- var import_node_events6 = require("events");
35696
+ var import_node_events7 = require("events");
35135
35697
  function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pipHeight, margin) {
35136
35698
  const pipW = Math.floor(pipWidth);
35137
35699
  const pipH = Math.floor(pipHeight);
@@ -35159,7 +35721,7 @@ function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pip
35159
35721
  return { x: m, y: m };
35160
35722
  }
35161
35723
  }
35162
- var CompositeStream = class extends import_node_events6.EventEmitter {
35724
+ var CompositeStream = class extends import_node_events7.EventEmitter {
35163
35725
  options;
35164
35726
  widerStream = null;
35165
35727
  teleStream = null;
@@ -35484,7 +36046,7 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35484
36046
  this.logger.log?.(
35485
36047
  `[CompositeStream] Starting ffmpeg (rtsp inputs): bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35486
36048
  );
35487
- this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
36049
+ this.ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegBin, ffmpegArgs, {
35488
36050
  stdio: ["ignore", "pipe", "pipe"]
35489
36051
  });
35490
36052
  this.ffmpegProcess.on("error", (error) => {
@@ -35614,7 +36176,7 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35614
36176
  this.logger.log?.(
35615
36177
  `[CompositeStream] Starting ffmpeg: bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35616
36178
  );
35617
- this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
36179
+ this.ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegBin, ffmpegArgs, {
35618
36180
  stdio: ["pipe", "pipe", "pipe", "pipe"]
35619
36181
  });
35620
36182
  this.ffmpegProcess.on("error", (error) => {
@@ -36192,7 +36754,8 @@ async function createRfc4571TcpServerInternal(options) {
36192
36754
  apisToClose.add(resolvedCompositeApis.widerApi);
36193
36755
  if (resolvedCompositeApis?.teleApi)
36194
36756
  apisToClose.add(resolvedCompositeApis.teleApi);
36195
- const uptimeRestartMs = uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
36757
+ const alwaysOnEnabled = Boolean(options.alwaysOn?.enabled) && !isComposite;
36758
+ const uptimeRestartMs = alwaysOnEnabled ? 0 : uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
36196
36759
  const variantSuffix = variant && variant !== "default" ? ` variant=${variant}` : "";
36197
36760
  const logPrefix = isComposite ? `[native-rfc4571 composite profile=${profile}${variantSuffix}${requestedId ? ` id=${requestedId}` : ""}]` : `[native-rfc4571 ch=${channel} profile=${profile}${variantSuffix}]`;
36198
36761
  const log = (message) => {
@@ -36216,6 +36779,7 @@ async function createRfc4571TcpServerInternal(options) {
36216
36779
  );
36217
36780
  let videoStream;
36218
36781
  let isCompositeStream = false;
36782
+ let alwaysOnController;
36219
36783
  if (isComposite) {
36220
36784
  const widerChannel = compositeOptions?.widerChannel ?? 0;
36221
36785
  const teleChannel = compositeOptions?.teleChannel ?? 1;
@@ -36358,7 +36922,7 @@ async function createRfc4571TcpServerInternal(options) {
36358
36922
  } else {
36359
36923
  streamClient = baseApi.client;
36360
36924
  }
36361
- videoStream = new BaichuanVideoStream({
36925
+ const createLiveStream = async () => new BaichuanVideoStream({
36362
36926
  client: streamClient,
36363
36927
  api: baseApi,
36364
36928
  channel: ch,
@@ -36366,10 +36930,39 @@ async function createRfc4571TcpServerInternal(options) {
36366
36930
  variant,
36367
36931
  logger
36368
36932
  });
36369
- await videoStream.start();
36370
- log(
36371
- `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36372
- );
36933
+ if (options.alwaysOn?.enabled) {
36934
+ const cvsOpts = {
36935
+ // ContinuousVideoStream owns the lifecycle: it calls createLiveStream
36936
+ // (which returns a started stream) and re-starts it internally on goLive.
36937
+ createLiveStream,
36938
+ logger
36939
+ };
36940
+ if (options.alwaysOn.idleFps !== void 0)
36941
+ cvsOpts.idleFps = options.alwaysOn.idleFps;
36942
+ if (options.alwaysOn.placeholder !== void 0)
36943
+ cvsOpts.placeholder = options.alwaysOn.placeholder;
36944
+ const cvs = new ContinuousVideoStream(cvsOpts);
36945
+ alwaysOnController = new AlwaysOnController({
36946
+ api: baseApi,
36947
+ channel: ch,
36948
+ options: options.alwaysOn,
36949
+ goLive: () => cvs.goLive(),
36950
+ goIdle: () => cvs.goIdle(),
36951
+ logger
36952
+ });
36953
+ await alwaysOnController.start();
36954
+ videoStream = cvs;
36955
+ log(
36956
+ `always-on stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36957
+ );
36958
+ } else {
36959
+ const live = await createLiveStream();
36960
+ await live.start();
36961
+ videoStream = live;
36962
+ log(
36963
+ `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36964
+ );
36965
+ }
36373
36966
  }
36374
36967
  const waitForKeyframe = async () => {
36375
36968
  if (isCompositeStream) {
@@ -36497,6 +37090,12 @@ async function createRfc4571TcpServerInternal(options) {
36497
37090
  try {
36498
37091
  keyframe = await waitForKeyframe();
36499
37092
  } catch (e) {
37093
+ if (alwaysOnController) {
37094
+ try {
37095
+ await alwaysOnController.stop();
37096
+ } catch {
37097
+ }
37098
+ }
36500
37099
  try {
36501
37100
  await videoStream.stop();
36502
37101
  } catch {
@@ -36745,12 +37344,13 @@ async function createRfc4571TcpServerInternal(options) {
36745
37344
  } catch {
36746
37345
  }
36747
37346
  muxer = makeMuxer();
37347
+ const restartable = videoStream;
36748
37348
  try {
36749
- await videoStream.stop();
37349
+ await restartable.stop();
36750
37350
  } catch {
36751
37351
  }
36752
37352
  try {
36753
- await videoStream.start();
37353
+ await restartable.start();
36754
37354
  } catch (e) {
36755
37355
  restarting = false;
36756
37356
  close(e).catch(() => {
@@ -36771,6 +37371,12 @@ async function createRfc4571TcpServerInternal(options) {
36771
37371
  cancelIdleTeardown();
36772
37372
  const reasonStr = reason?.message || reason?.toString?.() || reason || "requested";
36773
37373
  muxer.close();
37374
+ if (alwaysOnController) {
37375
+ try {
37376
+ await alwaysOnController.stop();
37377
+ } catch {
37378
+ }
37379
+ }
36774
37380
  try {
36775
37381
  await videoStream.stop();
36776
37382
  } catch {
@@ -37370,7 +37976,7 @@ async function createRfc4571TcpServerForReplay(options) {
37370
37976
 
37371
37977
  // src/rfc/replay-http-server.ts
37372
37978
  var import_node_http3 = __toESM(require("http"), 1);
37373
- var import_node_child_process8 = require("child_process");
37979
+ var import_node_child_process9 = require("child_process");
37374
37980
  var import_node_stream2 = require("stream");
37375
37981
  async function createReplayHttpServer(options) {
37376
37982
  const {
@@ -37524,7 +38130,7 @@ async function createReplayHttpServer(options) {
37524
38130
  "pipe:1"
37525
38131
  ];
37526
38132
  log(`spawning ffmpeg: ${ffmpegPath} ${ffmpegArgs.join(" ")}`);
37527
- ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegPath, ffmpegArgs, {
38133
+ ffmpegProcess = (0, import_node_child_process9.spawn)(ffmpegPath, ffmpegArgs, {
37528
38134
  stdio: ["pipe", "pipe", "pipe"]
37529
38135
  });
37530
38136
  ffmpegProcess.stdout?.pipe(outputStream).pipe(res);
@@ -37625,7 +38231,7 @@ async function createReplayHttpServer(options) {
37625
38231
  init_BaichuanVideoStream();
37626
38232
 
37627
38233
  // src/baichuan/stream/Go2rtcTcpServer.ts
37628
- var import_node_events7 = require("events");
38234
+ var import_node_events8 = require("events");
37629
38235
  var net4 = __toESM(require("net"), 1);
37630
38236
  init_H264Converter();
37631
38237
  init_H265Converter();
@@ -37737,7 +38343,7 @@ var NativeStreamFanout2 = class {
37737
38343
  this.pumpPromise = null;
37738
38344
  }
37739
38345
  };
37740
- var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEmitter {
38346
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events8.EventEmitter {
37741
38347
  api;
37742
38348
  channel;
37743
38349
  profile;
@@ -38428,8 +39034,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEm
38428
39034
  };
38429
39035
 
38430
39036
  // src/baichuan/stream/BaichuanHttpStreamServer.ts
38431
- var import_node_events8 = require("events");
38432
- var import_node_child_process9 = require("child_process");
39037
+ var import_node_events9 = require("events");
39038
+ var import_node_child_process10 = require("child_process");
38433
39039
  var http4 = __toESM(require("http"), 1);
38434
39040
  var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
38435
39041
  var NAL_START_CODE_3B3 = Buffer.from([0, 0, 1]);
@@ -38475,7 +39081,7 @@ function isH264KeyframeFromAnnexB(annexB) {
38475
39081
  }
38476
39082
  return false;
38477
39083
  }
38478
- var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
39084
+ var BaichuanHttpStreamServer = class extends import_node_events9.EventEmitter {
38479
39085
  videoStream;
38480
39086
  listenPort;
38481
39087
  path;
@@ -38539,7 +39145,7 @@ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
38539
39145
  this.httpServer.on("error", reject);
38540
39146
  });
38541
39147
  this.logger.info(`[BaichuanHttpStreamServer] Starting ffmpeg for H.264 -> MPEG-TS conversion...`);
38542
- const ffmpeg = (0, import_node_child_process9.spawn)("ffmpeg", [
39148
+ const ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", [
38543
39149
  "-hide_banner",
38544
39150
  // ffmpeg warnings often include non-fatal decode messages (e.g. decode_slice_header),
38545
39151
  // which we don't want to treat as application errors.
@@ -38747,15 +39353,15 @@ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
38747
39353
  };
38748
39354
 
38749
39355
  // src/baichuan/stream/BaichuanMjpegServer.ts
38750
- var import_node_events10 = require("events");
39356
+ var import_node_events11 = require("events");
38751
39357
  var http5 = __toESM(require("http"), 1);
38752
39358
 
38753
39359
  // src/baichuan/stream/MjpegTransformer.ts
38754
- var import_node_events9 = require("events");
38755
- var import_node_child_process10 = require("child_process");
39360
+ var import_node_events10 = require("events");
39361
+ var import_node_child_process11 = require("child_process");
38756
39362
  var JPEG_SOI = Buffer.from([255, 216]);
38757
39363
  var JPEG_EOI = Buffer.from([255, 217]);
38758
- var MjpegTransformer = class extends import_node_events9.EventEmitter {
39364
+ var MjpegTransformer = class extends import_node_events10.EventEmitter {
38759
39365
  options;
38760
39366
  ffmpeg = null;
38761
39367
  started = false;
@@ -38813,7 +39419,7 @@ var MjpegTransformer = class extends import_node_events9.EventEmitter {
38813
39419
  "pipe:1"
38814
39420
  );
38815
39421
  this.log("debug", `Starting FFmpeg with args: ${args.join(" ")}`);
38816
- this.ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", args, {
39422
+ this.ffmpeg = (0, import_node_child_process11.spawn)("ffmpeg", args, {
38817
39423
  stdio: ["pipe", "pipe", "pipe"]
38818
39424
  });
38819
39425
  this.ffmpeg.stdout.on("data", (data) => {
@@ -38954,7 +39560,7 @@ Content-Length: ${frame.length}\r
38954
39560
  // src/baichuan/stream/BaichuanMjpegServer.ts
38955
39561
  init_H264Converter();
38956
39562
  init_H265Converter();
38957
- var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
39563
+ var BaichuanMjpegServer = class extends import_node_events11.EventEmitter {
38958
39564
  options;
38959
39565
  clients = /* @__PURE__ */ new Map();
38960
39566
  httpServer = null;
@@ -39235,14 +39841,14 @@ var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
39235
39841
  };
39236
39842
 
39237
39843
  // src/baichuan/stream/BaichuanWebRTCServer.ts
39238
- var import_node_events12 = require("events");
39844
+ var import_node_events13 = require("events");
39239
39845
  init_BcMediaAnnexBDecoder();
39240
39846
 
39241
39847
  // src/baichuan/stream/AacToOpusTranscoder.ts
39242
- var import_node_child_process11 = require("child_process");
39848
+ var import_node_child_process12 = require("child_process");
39243
39849
  var import_node_dgram3 = require("dgram");
39244
- var import_node_events11 = require("events");
39245
- var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
39850
+ var import_node_events12 = require("events");
39851
+ var AacToOpusTranscoder = class extends import_node_events12.EventEmitter {
39246
39852
  opts;
39247
39853
  socket = null;
39248
39854
  ffmpeg = null;
@@ -39319,7 +39925,7 @@ var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
39319
39925
  `rtp://127.0.0.1:${this.port}`
39320
39926
  ];
39321
39927
  this.log("info", `spawning ffmpeg with: ${this.opts.ffmpegPath} ${args.join(" ")}`);
39322
- this.ffmpeg = (0, import_node_child_process11.spawn)(this.opts.ffmpegPath, args, {
39928
+ this.ffmpeg = (0, import_node_child_process12.spawn)(this.opts.ffmpegPath, args, {
39323
39929
  stdio: ["pipe", "ignore", "pipe"]
39324
39930
  });
39325
39931
  this.ffmpeg.on("error", (err) => {
@@ -39459,7 +40065,7 @@ function getH264NalType(nalUnit) {
39459
40065
  function getH265NalType2(nalUnit) {
39460
40066
  return nalUnit[0] >> 1 & 63;
39461
40067
  }
39462
- var BaichuanWebRTCServer = class extends import_node_events12.EventEmitter {
40068
+ var BaichuanWebRTCServer = class extends import_node_events13.EventEmitter {
39463
40069
  options;
39464
40070
  sessions = /* @__PURE__ */ new Map();
39465
40071
  sessionIdCounter = 0;
@@ -40451,12 +41057,12 @@ Error: ${err}`
40451
41057
  };
40452
41058
 
40453
41059
  // src/baichuan/stream/BaichuanHlsServer.ts
40454
- var import_node_events13 = require("events");
41060
+ var import_node_events14 = require("events");
40455
41061
  var import_node_fs = __toESM(require("fs"), 1);
40456
41062
  var import_promises3 = __toESM(require("fs/promises"), 1);
40457
41063
  var import_node_os3 = __toESM(require("os"), 1);
40458
41064
  var import_node_path3 = __toESM(require("path"), 1);
40459
- var import_node_child_process12 = require("child_process");
41065
+ var import_node_child_process13 = require("child_process");
40460
41066
  init_BcMediaAnnexBDecoder();
40461
41067
  init_H264Converter();
40462
41068
  init_H265Converter();
@@ -40531,7 +41137,7 @@ function getNalTypes(codec, annexB) {
40531
41137
  }
40532
41138
  });
40533
41139
  }
40534
- var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
41140
+ var BaichuanHlsServer = class extends import_node_events14.EventEmitter {
40535
41141
  api;
40536
41142
  channel;
40537
41143
  profile;
@@ -40933,7 +41539,7 @@ var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
40933
41539
  this.segmentPattern,
40934
41540
  this.playlistPath
40935
41541
  );
40936
- const p = (0, import_node_child_process12.spawn)(this.ffmpegPath, args, {
41542
+ const p = (0, import_node_child_process13.spawn)(this.ffmpegPath, args, {
40937
41543
  stdio: ["pipe", "ignore", "pipe"]
40938
41544
  });
40939
41545
  p.on("error", (err) => {
@@ -41057,13 +41663,13 @@ async function pingHost(host, timeoutMs = 3e3) {
41057
41663
  }
41058
41664
  return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
41059
41665
  };
41060
- const { spawn: spawn13 } = await import("child_process");
41666
+ const { spawn: spawn14 } = await import("child_process");
41061
41667
  for (const bin of pingCandidates) {
41062
41668
  const ranOk = await new Promise((resolve) => {
41063
41669
  let settled = false;
41064
41670
  let child;
41065
41671
  try {
41066
- child = spawn13(bin, pingArgs(bin), { stdio: "ignore" });
41672
+ child = spawn14(bin, pingArgs(bin), { stdio: "ignore" });
41067
41673
  } catch {
41068
41674
  resolve("spawn-failed");
41069
41675
  return;
@@ -41696,10 +42302,10 @@ async function autoDetectDeviceType(inputs) {
41696
42302
  }
41697
42303
 
41698
42304
  // src/multifocal/compositeRtspServer.ts
41699
- var import_node_events14 = require("events");
41700
- var import_node_child_process13 = require("child_process");
42305
+ var import_node_events15 = require("events");
42306
+ var import_node_child_process14 = require("child_process");
41701
42307
  var net5 = __toESM(require("net"), 1);
41702
- var CompositeRtspServer = class extends import_node_events14.EventEmitter {
42308
+ var CompositeRtspServer = class extends import_node_events15.EventEmitter {
41703
42309
  options;
41704
42310
  compositeStream = null;
41705
42311
  rtspServer = null;
@@ -41804,7 +42410,7 @@ var CompositeRtspServer = class extends import_node_events14.EventEmitter {
41804
42410
  this.logger.log?.(
41805
42411
  `[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
41806
42412
  );
41807
- this.ffmpegProcess = (0, import_node_child_process13.spawn)("ffmpeg", ffmpegArgs, {
42413
+ this.ffmpegProcess = (0, import_node_child_process14.spawn)("ffmpeg", ffmpegArgs, {
41808
42414
  stdio: ["pipe", "pipe", "pipe"]
41809
42415
  });
41810
42416
  this.ffmpegProcess.on("error", (error) => {
@@ -42475,7 +43081,7 @@ var RtspBackchannel = class _RtspBackchannel {
42475
43081
  };
42476
43082
 
42477
43083
  // src/baichuan/stream/BaichuanRtspBackchannelServer.ts
42478
- var import_node_events15 = require("events");
43084
+ var import_node_events16 = require("events");
42479
43085
  var net6 = __toESM(require("net"), 1);
42480
43086
  var crypto3 = __toESM(require("crypto"), 1);
42481
43087
  var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
@@ -42532,7 +43138,7 @@ function extractPublicEndpoint(url, requestText) {
42532
43138
  if (hostHeader) return hostHeader;
42533
43139
  return null;
42534
43140
  }
42535
- var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
43141
+ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events16.EventEmitter {
42536
43142
  listenHost;
42537
43143
  listenPort;
42538
43144
  logger;
@@ -43513,7 +44119,9 @@ function buildInitialStatus(config) {
43513
44119
  // Annotate the CommonJS export names for ESM import in node:
43514
44120
  0 && (module.exports = {
43515
44121
  ALL_UDP_DISCOVERY_METHODS,
44122
+ ALWAYS_ON_DEFAULTS,
43516
44123
  AesStreamDecryptor,
44124
+ AlwaysOnController,
43517
44125
  AutodiscoveryClient,
43518
44126
  BC_AES_IV,
43519
44127
  BC_CLASS_FILE_DOWNLOAD,
@@ -43670,6 +44278,7 @@ function buildInitialStatus(config) {
43670
44278
  BcUdpStream,
43671
44279
  CompositeRtspServer,
43672
44280
  CompositeStream,
44281
+ ContinuousVideoStream,
43673
44282
  DEFAULT_SHELTER_CANVAS,
43674
44283
  DUAL_LENS_DUAL_MOTION_MODELS,
43675
44284
  DUAL_LENS_MODELS,
@@ -43683,6 +44292,7 @@ function buildInitialStatus(config) {
43683
44292
  MpegTsMuxer,
43684
44293
  NVR_HUB_EXACT_TYPES,
43685
44294
  NVR_HUB_MODEL_PATTERNS,
44295
+ PlaceholderRenderer,
43686
44296
  ReolinkBaichuanApi,
43687
44297
  ReolinkCgiApi,
43688
44298
  ReolinkHttpClient,