@apocaliss92/nodelink-js 0.6.4 → 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.
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) {
@@ -17369,7 +17883,7 @@ function buildSetSystemGeneralXml(patch) {
17369
17883
  }
17370
17884
 
17371
17885
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
17372
- var import_jimp = require("jimp");
17886
+ var import_jimp2 = require("jimp");
17373
17887
  init_ReolinkCgiApi();
17374
17888
  init_ReolinkHttpClient();
17375
17889
 
@@ -20329,8 +20843,8 @@ var parseSirenStatusListPushXml = (xml) => {
20329
20843
  };
20330
20844
 
20331
20845
  // src/emailPush/bus.ts
20332
- var import_node_events5 = require("events");
20333
- var emitter = new import_node_events5.EventEmitter();
20846
+ var import_node_events6 = require("events");
20847
+ var emitter = new import_node_events6.EventEmitter();
20334
20848
  var cameraResolver = () => void 0;
20335
20849
  var lastEventByCamera = /* @__PURE__ */ new Map();
20336
20850
  var MAX_GLOBAL_EVENTS = 300;
@@ -24209,12 +24723,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
24209
24723
  let wideImg;
24210
24724
  let teleImg;
24211
24725
  try {
24212
- wideImg = await import_jimp.Jimp.read(wide);
24726
+ wideImg = await import_jimp2.Jimp.read(wide);
24213
24727
  } catch {
24214
24728
  return wide;
24215
24729
  }
24216
24730
  try {
24217
- teleImg = await import_jimp.Jimp.read(tele);
24731
+ teleImg = await import_jimp2.Jimp.read(tele);
24218
24732
  } catch {
24219
24733
  return wide;
24220
24734
  }
@@ -24248,7 +24762,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
24248
24762
  });
24249
24763
  teleImg.resize({ w: pipW, h: pipH });
24250
24764
  wideImg.composite(teleImg, left, top);
24251
- return await wideImg.getBuffer(import_jimp.JimpMime.jpeg, { quality: 80 });
24765
+ return await wideImg.getBuffer(import_jimp2.JimpMime.jpeg, { quality: 80 });
24252
24766
  }
24253
24767
  const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
24254
24768
  const variant = options?.variant ?? "default";
@@ -25206,7 +25720,7 @@ ${xml}`);
25206
25720
  const chunks = [];
25207
25721
  let stderr = "";
25208
25722
  let timedOut = false;
25209
- const ff = (0, import_node_child_process3.spawn)(params.ffmpegPath, [
25723
+ const ff = (0, import_node_child_process4.spawn)(params.ffmpegPath, [
25210
25724
  "-hide_banner",
25211
25725
  "-loglevel",
25212
25726
  "error",
@@ -25291,7 +25805,7 @@ ${xml}`);
25291
25805
  const chunks = [];
25292
25806
  let stderr = "";
25293
25807
  let timedOut = false;
25294
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25808
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25295
25809
  "-hide_banner",
25296
25810
  "-loglevel",
25297
25811
  "error",
@@ -25407,7 +25921,7 @@ ${xml}`);
25407
25921
  ensureEnabled: true
25408
25922
  });
25409
25923
  await new Promise((resolve, reject) => {
25410
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25924
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25411
25925
  "-hide_banner",
25412
25926
  "-loglevel",
25413
25927
  "error",
@@ -25463,7 +25977,7 @@ ${stderr}`));
25463
25977
  const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
25464
25978
  await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(params.outputPath), { recursive: true });
25465
25979
  await new Promise((resolve, reject) => {
25466
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25980
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25467
25981
  "-hide_banner",
25468
25982
  "-loglevel",
25469
25983
  "error",
@@ -26028,7 +26542,7 @@ ${stderr}`)
26028
26542
  * Convert a raw video keyframe to JPEG using ffmpeg.
26029
26543
  */
26030
26544
  async convertFrameToJpeg(params) {
26031
- const { spawn: spawn13 } = await import("child_process");
26545
+ const { spawn: spawn14 } = await import("child_process");
26032
26546
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
26033
26547
  const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
26034
26548
  return new Promise((resolve, reject) => {
@@ -26050,7 +26564,7 @@ ${stderr}`)
26050
26564
  "2",
26051
26565
  "pipe:1"
26052
26566
  ];
26053
- const proc = spawn13(ffmpeg, args, {
26567
+ const proc = spawn14(ffmpeg, args, {
26054
26568
  stdio: ["pipe", "pipe", "pipe"]
26055
26569
  });
26056
26570
  const chunks = [];
@@ -26193,7 +26707,7 @@ ${stderr}`)
26193
26707
  * Internal helper to mux video+audio into MP4 using ffmpeg.
26194
26708
  */
26195
26709
  async muxToMp4(params) {
26196
- const { spawn: spawn13 } = await import("child_process");
26710
+ const { spawn: spawn14 } = await import("child_process");
26197
26711
  const { randomUUID: randomUUID3 } = await import("crypto");
26198
26712
  const fs7 = await import("fs/promises");
26199
26713
  const os2 = await import("os");
@@ -26245,7 +26759,7 @@ ${stderr}`)
26245
26759
  outputPath
26246
26760
  );
26247
26761
  await new Promise((resolve, reject) => {
26248
- const p = spawn13(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26762
+ const p = spawn14(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26249
26763
  let stderr = "";
26250
26764
  p.stderr.on("data", (d) => {
26251
26765
  stderr += d.toString();
@@ -31232,7 +31746,7 @@ ${scheduleItems}
31232
31746
  "mjpeg",
31233
31747
  "pipe:1"
31234
31748
  ];
31235
- const ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31749
+ const ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31236
31750
  const chunks = [];
31237
31751
  let stderr = "";
31238
31752
  ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
@@ -31356,7 +31870,7 @@ ${scheduleItems}
31356
31870
  "pipe:1"
31357
31871
  ];
31358
31872
  }
31359
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31873
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31360
31874
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
31361
31875
  throw new Error("ffmpeg stdio streams not available");
31362
31876
  }
@@ -31603,7 +32117,7 @@ ${scheduleItems}
31603
32117
  "mp4",
31604
32118
  "pipe:1"
31605
32119
  ];
31606
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
32120
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31607
32121
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
31608
32122
  throw new Error("ffmpeg stdio streams not available");
31609
32123
  }
@@ -31812,7 +32326,7 @@ ${scheduleItems}
31812
32326
  "independent_segments+temp_file",
31813
32327
  playlistPath
31814
32328
  ];
31815
- ff = (0, import_node_child_process3.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
32329
+ ff = (0, import_node_child_process4.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
31816
32330
  if (!ff.stdin || !ff.stderr) {
31817
32331
  throw new Error("ffmpeg stdio streams not available");
31818
32332
  }
@@ -32784,14 +33298,14 @@ function buildHlsRedirectUrl(originalUrl) {
32784
33298
  }
32785
33299
 
32786
33300
  // src/reolink/discovery.ts
32787
- var import_node_child_process4 = require("child_process");
33301
+ var import_node_child_process5 = require("child_process");
32788
33302
  var import_node_crypto4 = require("crypto");
32789
33303
  var import_node_dgram2 = __toESM(require("dgram"), 1);
32790
33304
  var net3 = __toESM(require("net"), 1);
32791
33305
  var import_node_os2 = require("os");
32792
33306
  var import_node_util = require("util");
32793
33307
  init_ReolinkCgiApi();
32794
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process4.execFile);
33308
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process5.execFile);
32795
33309
  async function discoverViaUdpDirect(host, options) {
32796
33310
  if (!options.enableUdpDiscovery) return [];
32797
33311
  const logger = options.logger;
@@ -33833,7 +34347,7 @@ init_recordingFileName();
33833
34347
 
33834
34348
  // src/reolink/baichuan/endpoints-server.ts
33835
34349
  var import_node_http = __toESM(require("http"), 1);
33836
- var import_node_child_process5 = require("child_process");
34350
+ var import_node_child_process6 = require("child_process");
33837
34351
  function parseIntParam(v, def) {
33838
34352
  if (v == null) return def;
33839
34353
  const n = Number.parseInt(v, 10);
@@ -34072,7 +34586,7 @@ function createBaichuanEndpointsServer(opts) {
34072
34586
  "Cache-Control": "no-cache",
34073
34587
  Connection: "close"
34074
34588
  });
34075
- const ff2 = (0, import_node_child_process5.spawn)("ffmpeg", [
34589
+ const ff2 = (0, import_node_child_process6.spawn)("ffmpeg", [
34076
34590
  "-hide_banner",
34077
34591
  "-loglevel",
34078
34592
  "error",
@@ -34105,7 +34619,7 @@ function createBaichuanEndpointsServer(opts) {
34105
34619
  );
34106
34620
  res.setHeader("Cache-Control", "no-cache");
34107
34621
  res.setHeader("Connection", "close");
34108
- const ff = (0, import_node_child_process5.spawn)("ffmpeg", [
34622
+ const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
34109
34623
  "-hide_banner",
34110
34624
  "-loglevel",
34111
34625
  "error",
@@ -34216,7 +34730,7 @@ init_urls();
34216
34730
 
34217
34731
  // src/rtsp/server.ts
34218
34732
  var import_node_http2 = __toESM(require("http"), 1);
34219
- var import_node_child_process6 = require("child_process");
34733
+ var import_node_child_process7 = require("child_process");
34220
34734
  init_urls();
34221
34735
  function createRtspProxyServer(opts) {
34222
34736
  return import_node_http2.default.createServer((req, res) => {
@@ -34257,7 +34771,7 @@ function createRtspProxyServer(opts) {
34257
34771
  Connection: "close"
34258
34772
  });
34259
34773
  const rtspTransport = opts.rtspTransport ?? "tcp";
34260
- const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
34774
+ const ff = (0, import_node_child_process7.spawn)("ffmpeg", [
34261
34775
  "-hide_banner",
34262
34776
  "-loglevel",
34263
34777
  "error",
@@ -35129,9 +35643,9 @@ var import_node_net2 = __toESM(require("net"), 1);
35129
35643
  init_BaichuanVideoStream();
35130
35644
 
35131
35645
  // src/multifocal/compositeStream.ts
35132
- var import_node_child_process7 = require("child_process");
35646
+ var import_node_child_process8 = require("child_process");
35133
35647
  var import_node_crypto6 = require("crypto");
35134
- var import_node_events6 = require("events");
35648
+ var import_node_events7 = require("events");
35135
35649
  function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pipHeight, margin) {
35136
35650
  const pipW = Math.floor(pipWidth);
35137
35651
  const pipH = Math.floor(pipHeight);
@@ -35159,7 +35673,7 @@ function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pip
35159
35673
  return { x: m, y: m };
35160
35674
  }
35161
35675
  }
35162
- var CompositeStream = class extends import_node_events6.EventEmitter {
35676
+ var CompositeStream = class extends import_node_events7.EventEmitter {
35163
35677
  options;
35164
35678
  widerStream = null;
35165
35679
  teleStream = null;
@@ -35484,7 +35998,7 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35484
35998
  this.logger.log?.(
35485
35999
  `[CompositeStream] Starting ffmpeg (rtsp inputs): bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35486
36000
  );
35487
- this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
36001
+ this.ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegBin, ffmpegArgs, {
35488
36002
  stdio: ["ignore", "pipe", "pipe"]
35489
36003
  });
35490
36004
  this.ffmpegProcess.on("error", (error) => {
@@ -35614,7 +36128,7 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35614
36128
  this.logger.log?.(
35615
36129
  `[CompositeStream] Starting ffmpeg: bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35616
36130
  );
35617
- this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
36131
+ this.ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegBin, ffmpegArgs, {
35618
36132
  stdio: ["pipe", "pipe", "pipe", "pipe"]
35619
36133
  });
35620
36134
  this.ffmpegProcess.on("error", (error) => {
@@ -36192,7 +36706,8 @@ async function createRfc4571TcpServerInternal(options) {
36192
36706
  apisToClose.add(resolvedCompositeApis.widerApi);
36193
36707
  if (resolvedCompositeApis?.teleApi)
36194
36708
  apisToClose.add(resolvedCompositeApis.teleApi);
36195
- const uptimeRestartMs = uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
36709
+ const alwaysOnEnabled = Boolean(options.alwaysOn?.enabled) && !isComposite;
36710
+ const uptimeRestartMs = alwaysOnEnabled ? 0 : uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
36196
36711
  const variantSuffix = variant && variant !== "default" ? ` variant=${variant}` : "";
36197
36712
  const logPrefix = isComposite ? `[native-rfc4571 composite profile=${profile}${variantSuffix}${requestedId ? ` id=${requestedId}` : ""}]` : `[native-rfc4571 ch=${channel} profile=${profile}${variantSuffix}]`;
36198
36713
  const log = (message) => {
@@ -36216,6 +36731,7 @@ async function createRfc4571TcpServerInternal(options) {
36216
36731
  );
36217
36732
  let videoStream;
36218
36733
  let isCompositeStream = false;
36734
+ let alwaysOnController;
36219
36735
  if (isComposite) {
36220
36736
  const widerChannel = compositeOptions?.widerChannel ?? 0;
36221
36737
  const teleChannel = compositeOptions?.teleChannel ?? 1;
@@ -36358,7 +36874,7 @@ async function createRfc4571TcpServerInternal(options) {
36358
36874
  } else {
36359
36875
  streamClient = baseApi.client;
36360
36876
  }
36361
- videoStream = new BaichuanVideoStream({
36877
+ const createLiveStream = async () => new BaichuanVideoStream({
36362
36878
  client: streamClient,
36363
36879
  api: baseApi,
36364
36880
  channel: ch,
@@ -36366,10 +36882,39 @@ async function createRfc4571TcpServerInternal(options) {
36366
36882
  variant,
36367
36883
  logger
36368
36884
  });
36369
- await videoStream.start();
36370
- log(
36371
- `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36372
- );
36885
+ if (options.alwaysOn?.enabled) {
36886
+ const cvsOpts = {
36887
+ // ContinuousVideoStream owns the lifecycle: it calls createLiveStream
36888
+ // (which returns a started stream) and re-starts it internally on goLive.
36889
+ createLiveStream,
36890
+ logger
36891
+ };
36892
+ if (options.alwaysOn.idleFps !== void 0)
36893
+ cvsOpts.idleFps = options.alwaysOn.idleFps;
36894
+ if (options.alwaysOn.placeholder !== void 0)
36895
+ cvsOpts.placeholder = options.alwaysOn.placeholder;
36896
+ const cvs = new ContinuousVideoStream(cvsOpts);
36897
+ alwaysOnController = new AlwaysOnController({
36898
+ api: baseApi,
36899
+ channel: ch,
36900
+ options: options.alwaysOn,
36901
+ goLive: () => cvs.goLive(),
36902
+ goIdle: () => cvs.goIdle(),
36903
+ logger
36904
+ });
36905
+ await alwaysOnController.start();
36906
+ videoStream = cvs;
36907
+ log(
36908
+ `always-on stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36909
+ );
36910
+ } else {
36911
+ const live = await createLiveStream();
36912
+ await live.start();
36913
+ videoStream = live;
36914
+ log(
36915
+ `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36916
+ );
36917
+ }
36373
36918
  }
36374
36919
  const waitForKeyframe = async () => {
36375
36920
  if (isCompositeStream) {
@@ -36497,6 +37042,12 @@ async function createRfc4571TcpServerInternal(options) {
36497
37042
  try {
36498
37043
  keyframe = await waitForKeyframe();
36499
37044
  } catch (e) {
37045
+ if (alwaysOnController) {
37046
+ try {
37047
+ await alwaysOnController.stop();
37048
+ } catch {
37049
+ }
37050
+ }
36500
37051
  try {
36501
37052
  await videoStream.stop();
36502
37053
  } catch {
@@ -36745,12 +37296,13 @@ async function createRfc4571TcpServerInternal(options) {
36745
37296
  } catch {
36746
37297
  }
36747
37298
  muxer = makeMuxer();
37299
+ const restartable = videoStream;
36748
37300
  try {
36749
- await videoStream.stop();
37301
+ await restartable.stop();
36750
37302
  } catch {
36751
37303
  }
36752
37304
  try {
36753
- await videoStream.start();
37305
+ await restartable.start();
36754
37306
  } catch (e) {
36755
37307
  restarting = false;
36756
37308
  close(e).catch(() => {
@@ -36771,6 +37323,12 @@ async function createRfc4571TcpServerInternal(options) {
36771
37323
  cancelIdleTeardown();
36772
37324
  const reasonStr = reason?.message || reason?.toString?.() || reason || "requested";
36773
37325
  muxer.close();
37326
+ if (alwaysOnController) {
37327
+ try {
37328
+ await alwaysOnController.stop();
37329
+ } catch {
37330
+ }
37331
+ }
36774
37332
  try {
36775
37333
  await videoStream.stop();
36776
37334
  } catch {
@@ -37370,7 +37928,7 @@ async function createRfc4571TcpServerForReplay(options) {
37370
37928
 
37371
37929
  // src/rfc/replay-http-server.ts
37372
37930
  var import_node_http3 = __toESM(require("http"), 1);
37373
- var import_node_child_process8 = require("child_process");
37931
+ var import_node_child_process9 = require("child_process");
37374
37932
  var import_node_stream2 = require("stream");
37375
37933
  async function createReplayHttpServer(options) {
37376
37934
  const {
@@ -37524,7 +38082,7 @@ async function createReplayHttpServer(options) {
37524
38082
  "pipe:1"
37525
38083
  ];
37526
38084
  log(`spawning ffmpeg: ${ffmpegPath} ${ffmpegArgs.join(" ")}`);
37527
- ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegPath, ffmpegArgs, {
38085
+ ffmpegProcess = (0, import_node_child_process9.spawn)(ffmpegPath, ffmpegArgs, {
37528
38086
  stdio: ["pipe", "pipe", "pipe"]
37529
38087
  });
37530
38088
  ffmpegProcess.stdout?.pipe(outputStream).pipe(res);
@@ -37625,7 +38183,7 @@ async function createReplayHttpServer(options) {
37625
38183
  init_BaichuanVideoStream();
37626
38184
 
37627
38185
  // src/baichuan/stream/Go2rtcTcpServer.ts
37628
- var import_node_events7 = require("events");
38186
+ var import_node_events8 = require("events");
37629
38187
  var net4 = __toESM(require("net"), 1);
37630
38188
  init_H264Converter();
37631
38189
  init_H265Converter();
@@ -37737,7 +38295,7 @@ var NativeStreamFanout2 = class {
37737
38295
  this.pumpPromise = null;
37738
38296
  }
37739
38297
  };
37740
- var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEmitter {
38298
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events8.EventEmitter {
37741
38299
  api;
37742
38300
  channel;
37743
38301
  profile;
@@ -38428,8 +38986,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEm
38428
38986
  };
38429
38987
 
38430
38988
  // src/baichuan/stream/BaichuanHttpStreamServer.ts
38431
- var import_node_events8 = require("events");
38432
- var import_node_child_process9 = require("child_process");
38989
+ var import_node_events9 = require("events");
38990
+ var import_node_child_process10 = require("child_process");
38433
38991
  var http4 = __toESM(require("http"), 1);
38434
38992
  var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
38435
38993
  var NAL_START_CODE_3B3 = Buffer.from([0, 0, 1]);
@@ -38475,7 +39033,7 @@ function isH264KeyframeFromAnnexB(annexB) {
38475
39033
  }
38476
39034
  return false;
38477
39035
  }
38478
- var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
39036
+ var BaichuanHttpStreamServer = class extends import_node_events9.EventEmitter {
38479
39037
  videoStream;
38480
39038
  listenPort;
38481
39039
  path;
@@ -38539,7 +39097,7 @@ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
38539
39097
  this.httpServer.on("error", reject);
38540
39098
  });
38541
39099
  this.logger.info(`[BaichuanHttpStreamServer] Starting ffmpeg for H.264 -> MPEG-TS conversion...`);
38542
- const ffmpeg = (0, import_node_child_process9.spawn)("ffmpeg", [
39100
+ const ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", [
38543
39101
  "-hide_banner",
38544
39102
  // ffmpeg warnings often include non-fatal decode messages (e.g. decode_slice_header),
38545
39103
  // which we don't want to treat as application errors.
@@ -38747,15 +39305,15 @@ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
38747
39305
  };
38748
39306
 
38749
39307
  // src/baichuan/stream/BaichuanMjpegServer.ts
38750
- var import_node_events10 = require("events");
39308
+ var import_node_events11 = require("events");
38751
39309
  var http5 = __toESM(require("http"), 1);
38752
39310
 
38753
39311
  // src/baichuan/stream/MjpegTransformer.ts
38754
- var import_node_events9 = require("events");
38755
- var import_node_child_process10 = require("child_process");
39312
+ var import_node_events10 = require("events");
39313
+ var import_node_child_process11 = require("child_process");
38756
39314
  var JPEG_SOI = Buffer.from([255, 216]);
38757
39315
  var JPEG_EOI = Buffer.from([255, 217]);
38758
- var MjpegTransformer = class extends import_node_events9.EventEmitter {
39316
+ var MjpegTransformer = class extends import_node_events10.EventEmitter {
38759
39317
  options;
38760
39318
  ffmpeg = null;
38761
39319
  started = false;
@@ -38813,7 +39371,7 @@ var MjpegTransformer = class extends import_node_events9.EventEmitter {
38813
39371
  "pipe:1"
38814
39372
  );
38815
39373
  this.log("debug", `Starting FFmpeg with args: ${args.join(" ")}`);
38816
- this.ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", args, {
39374
+ this.ffmpeg = (0, import_node_child_process11.spawn)("ffmpeg", args, {
38817
39375
  stdio: ["pipe", "pipe", "pipe"]
38818
39376
  });
38819
39377
  this.ffmpeg.stdout.on("data", (data) => {
@@ -38954,7 +39512,7 @@ Content-Length: ${frame.length}\r
38954
39512
  // src/baichuan/stream/BaichuanMjpegServer.ts
38955
39513
  init_H264Converter();
38956
39514
  init_H265Converter();
38957
- var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
39515
+ var BaichuanMjpegServer = class extends import_node_events11.EventEmitter {
38958
39516
  options;
38959
39517
  clients = /* @__PURE__ */ new Map();
38960
39518
  httpServer = null;
@@ -39235,14 +39793,14 @@ var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
39235
39793
  };
39236
39794
 
39237
39795
  // src/baichuan/stream/BaichuanWebRTCServer.ts
39238
- var import_node_events12 = require("events");
39796
+ var import_node_events13 = require("events");
39239
39797
  init_BcMediaAnnexBDecoder();
39240
39798
 
39241
39799
  // src/baichuan/stream/AacToOpusTranscoder.ts
39242
- var import_node_child_process11 = require("child_process");
39800
+ var import_node_child_process12 = require("child_process");
39243
39801
  var import_node_dgram3 = require("dgram");
39244
- var import_node_events11 = require("events");
39245
- var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
39802
+ var import_node_events12 = require("events");
39803
+ var AacToOpusTranscoder = class extends import_node_events12.EventEmitter {
39246
39804
  opts;
39247
39805
  socket = null;
39248
39806
  ffmpeg = null;
@@ -39319,7 +39877,7 @@ var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
39319
39877
  `rtp://127.0.0.1:${this.port}`
39320
39878
  ];
39321
39879
  this.log("info", `spawning ffmpeg with: ${this.opts.ffmpegPath} ${args.join(" ")}`);
39322
- this.ffmpeg = (0, import_node_child_process11.spawn)(this.opts.ffmpegPath, args, {
39880
+ this.ffmpeg = (0, import_node_child_process12.spawn)(this.opts.ffmpegPath, args, {
39323
39881
  stdio: ["pipe", "ignore", "pipe"]
39324
39882
  });
39325
39883
  this.ffmpeg.on("error", (err) => {
@@ -39459,7 +40017,7 @@ function getH264NalType(nalUnit) {
39459
40017
  function getH265NalType2(nalUnit) {
39460
40018
  return nalUnit[0] >> 1 & 63;
39461
40019
  }
39462
- var BaichuanWebRTCServer = class extends import_node_events12.EventEmitter {
40020
+ var BaichuanWebRTCServer = class extends import_node_events13.EventEmitter {
39463
40021
  options;
39464
40022
  sessions = /* @__PURE__ */ new Map();
39465
40023
  sessionIdCounter = 0;
@@ -40451,12 +41009,12 @@ Error: ${err}`
40451
41009
  };
40452
41010
 
40453
41011
  // src/baichuan/stream/BaichuanHlsServer.ts
40454
- var import_node_events13 = require("events");
41012
+ var import_node_events14 = require("events");
40455
41013
  var import_node_fs = __toESM(require("fs"), 1);
40456
41014
  var import_promises3 = __toESM(require("fs/promises"), 1);
40457
41015
  var import_node_os3 = __toESM(require("os"), 1);
40458
41016
  var import_node_path3 = __toESM(require("path"), 1);
40459
- var import_node_child_process12 = require("child_process");
41017
+ var import_node_child_process13 = require("child_process");
40460
41018
  init_BcMediaAnnexBDecoder();
40461
41019
  init_H264Converter();
40462
41020
  init_H265Converter();
@@ -40531,7 +41089,7 @@ function getNalTypes(codec, annexB) {
40531
41089
  }
40532
41090
  });
40533
41091
  }
40534
- var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
41092
+ var BaichuanHlsServer = class extends import_node_events14.EventEmitter {
40535
41093
  api;
40536
41094
  channel;
40537
41095
  profile;
@@ -40933,7 +41491,7 @@ var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
40933
41491
  this.segmentPattern,
40934
41492
  this.playlistPath
40935
41493
  );
40936
- const p = (0, import_node_child_process12.spawn)(this.ffmpegPath, args, {
41494
+ const p = (0, import_node_child_process13.spawn)(this.ffmpegPath, args, {
40937
41495
  stdio: ["pipe", "ignore", "pipe"]
40938
41496
  });
40939
41497
  p.on("error", (err) => {
@@ -41057,13 +41615,13 @@ async function pingHost(host, timeoutMs = 3e3) {
41057
41615
  }
41058
41616
  return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
41059
41617
  };
41060
- const { spawn: spawn13 } = await import("child_process");
41618
+ const { spawn: spawn14 } = await import("child_process");
41061
41619
  for (const bin of pingCandidates) {
41062
41620
  const ranOk = await new Promise((resolve) => {
41063
41621
  let settled = false;
41064
41622
  let child;
41065
41623
  try {
41066
- child = spawn13(bin, pingArgs(bin), { stdio: "ignore" });
41624
+ child = spawn14(bin, pingArgs(bin), { stdio: "ignore" });
41067
41625
  } catch {
41068
41626
  resolve("spawn-failed");
41069
41627
  return;
@@ -41696,10 +42254,10 @@ async function autoDetectDeviceType(inputs) {
41696
42254
  }
41697
42255
 
41698
42256
  // src/multifocal/compositeRtspServer.ts
41699
- var import_node_events14 = require("events");
41700
- var import_node_child_process13 = require("child_process");
42257
+ var import_node_events15 = require("events");
42258
+ var import_node_child_process14 = require("child_process");
41701
42259
  var net5 = __toESM(require("net"), 1);
41702
- var CompositeRtspServer = class extends import_node_events14.EventEmitter {
42260
+ var CompositeRtspServer = class extends import_node_events15.EventEmitter {
41703
42261
  options;
41704
42262
  compositeStream = null;
41705
42263
  rtspServer = null;
@@ -41804,7 +42362,7 @@ var CompositeRtspServer = class extends import_node_events14.EventEmitter {
41804
42362
  this.logger.log?.(
41805
42363
  `[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
41806
42364
  );
41807
- this.ffmpegProcess = (0, import_node_child_process13.spawn)("ffmpeg", ffmpegArgs, {
42365
+ this.ffmpegProcess = (0, import_node_child_process14.spawn)("ffmpeg", ffmpegArgs, {
41808
42366
  stdio: ["pipe", "pipe", "pipe"]
41809
42367
  });
41810
42368
  this.ffmpegProcess.on("error", (error) => {
@@ -42475,7 +43033,7 @@ var RtspBackchannel = class _RtspBackchannel {
42475
43033
  };
42476
43034
 
42477
43035
  // src/baichuan/stream/BaichuanRtspBackchannelServer.ts
42478
- var import_node_events15 = require("events");
43036
+ var import_node_events16 = require("events");
42479
43037
  var net6 = __toESM(require("net"), 1);
42480
43038
  var crypto3 = __toESM(require("crypto"), 1);
42481
43039
  var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
@@ -42532,7 +43090,7 @@ function extractPublicEndpoint(url, requestText) {
42532
43090
  if (hostHeader) return hostHeader;
42533
43091
  return null;
42534
43092
  }
42535
- var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
43093
+ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events16.EventEmitter {
42536
43094
  listenHost;
42537
43095
  listenPort;
42538
43096
  logger;
@@ -43513,7 +44071,9 @@ function buildInitialStatus(config) {
43513
44071
  // Annotate the CommonJS export names for ESM import in node:
43514
44072
  0 && (module.exports = {
43515
44073
  ALL_UDP_DISCOVERY_METHODS,
44074
+ ALWAYS_ON_DEFAULTS,
43516
44075
  AesStreamDecryptor,
44076
+ AlwaysOnController,
43517
44077
  AutodiscoveryClient,
43518
44078
  BC_AES_IV,
43519
44079
  BC_CLASS_FILE_DOWNLOAD,
@@ -43670,6 +44230,7 @@ function buildInitialStatus(config) {
43670
44230
  BcUdpStream,
43671
44231
  CompositeRtspServer,
43672
44232
  CompositeStream,
44233
+ ContinuousVideoStream,
43673
44234
  DEFAULT_SHELTER_CANVAS,
43674
44235
  DUAL_LENS_DUAL_MOTION_MODELS,
43675
44236
  DUAL_LENS_MODELS,
@@ -43683,6 +44244,7 @@ function buildInitialStatus(config) {
43683
44244
  MpegTsMuxer,
43684
44245
  NVR_HUB_EXACT_TYPES,
43685
44246
  NVR_HUB_MODEL_PATTERNS,
44247
+ PlaceholderRenderer,
43686
44248
  ReolinkBaichuanApi,
43687
44249
  ReolinkCgiApi,
43688
44250
  ReolinkHttpClient,