@apocaliss92/nodelink-js 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -9126,6 +9130,7 @@ function readCache(uid, now) {
9126
9130
  }
9127
9131
  async function getServerBinding(uid, options = {}) {
9128
9132
  if (!uid || typeof uid !== "string") return void 0;
9133
+ uid = uid.toUpperCase();
9129
9134
  const now = Date.now();
9130
9135
  const cached = readCache(uid, now);
9131
9136
  if (cached?.kind === "ok") return cached.response;
@@ -14067,14 +14072,14 @@ init_ReolinkHttpClient();
14067
14072
  init_ReolinkCgiApi();
14068
14073
 
14069
14074
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
14070
- var import_node_child_process3 = require("child_process");
14075
+ var import_node_child_process4 = require("child_process");
14071
14076
  var import_promises2 = require("fs/promises");
14072
14077
  var import_node_path2 = require("path");
14073
14078
  var import_node_stream = require("stream");
14074
14079
 
14075
14080
  // src/baichuan/stream/BaichuanRtspServer.ts
14076
- var import_node_events4 = require("events");
14077
- var import_node_child_process2 = require("child_process");
14081
+ var import_node_events5 = require("events");
14082
+ var import_node_child_process3 = require("child_process");
14078
14083
  var net2 = __toESM(require("net"), 1);
14079
14084
  var dgram2 = __toESM(require("dgram"), 1);
14080
14085
  var crypto = __toESM(require("crypto"), 1);
@@ -14435,6 +14440,358 @@ async function* createNativeStream(api, channel, profile, options) {
14435
14440
  }
14436
14441
  }
14437
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
+
14438
14795
  // src/baichuan/stream/rtspFlow.ts
14439
14796
  init_H264Converter();
14440
14797
  init_H265Converter();
@@ -14661,7 +15018,7 @@ function envBool(value, defaultValue) {
14661
15018
  if (v === "0" || v === "false" || v === "no" || v === "off") return false;
14662
15019
  return defaultValue;
14663
15020
  }
14664
- var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.EventEmitter {
15021
+ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.EventEmitter {
14665
15022
  api;
14666
15023
  channel;
14667
15024
  profile;
@@ -14676,6 +15033,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14676
15033
  deviceId;
14677
15034
  dedicatedSessionRelease;
14678
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;
14679
15042
  // Authentication
14680
15043
  authCredentials = [];
14681
15044
  requireAuth;
@@ -14690,6 +15053,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14690
15053
  // Set of client IDs (IP:port)
14691
15054
  nativeStreamActive = false;
14692
15055
  // Whether the native stream is currently active
15056
+ tearingDown = false;
15057
+ // True while stop() is running; suppresses onEnd-driven restarts
14693
15058
  clientConnectionServer;
14694
15059
  // TCP server to track connections
14695
15060
  streamMetadata = null;
@@ -14877,6 +15242,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14877
15242
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
14878
15243
  this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
14879
15244
  this.lazyMetadata = options.lazyMetadata ?? false;
15245
+ this.alwaysOnOptions = options.alwaysOn;
14880
15246
  const transport = this.api.client.getTransport();
14881
15247
  this.flow = createRtspFlow(transport, "H264");
14882
15248
  }
@@ -16000,7 +16366,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16000
16366
  this.rtspDebugLog(
16001
16367
  `Spawning ffmpeg for client ${clientId}: ffmpeg ${ffmpegArgs.join(" ")}`
16002
16368
  );
16003
- ffmpeg = (0, import_node_child_process2.spawn)("ffmpeg", ffmpegArgs, {
16369
+ ffmpeg = (0, import_node_child_process3.spawn)("ffmpeg", ffmpegArgs, {
16004
16370
  stdio
16005
16371
  });
16006
16372
  try {
@@ -16364,6 +16730,141 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16364
16730
  }
16365
16731
  });
16366
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
+ }
16367
16868
  /**
16368
16869
  * Start native stream (mark as active).
16369
16870
  * Each client will create its own generator, so we just track that the stream is active.
@@ -16425,7 +16926,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16425
16926
  await this.flow.startKeepAlive(this.api);
16426
16927
  this.nativeFanout = new NativeStreamFanout({
16427
16928
  maxQueueItems: 200,
16428
- 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, {
16429
16930
  variant: this.variant,
16430
16931
  ...dedicatedClient ? { client: dedicatedClient } : {},
16431
16932
  signal
@@ -16508,6 +17009,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16508
17009
  } catch {
16509
17010
  }
16510
17011
  }
17012
+ if (this.tearingDown) return;
16511
17013
  if (this.connectedClients.size > 0 && hadFrames) {
16512
17014
  this.logger.info(
16513
17015
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
@@ -16521,7 +17023,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16521
17023
  });
16522
17024
  this.nativeFanout.start();
16523
17025
  this.clearNoFrameDeadlineTimer();
16524
- if (this.nativeStreamNoFrameDeadlineMs > 0) {
17026
+ if (this.nativeStreamNoFrameDeadlineMs > 0 && !this.alwaysOnOptions?.enabled) {
16525
17027
  this.noFrameDeadlineTimer = setTimeout(() => {
16526
17028
  this.noFrameDeadlineTimer = void 0;
16527
17029
  if (!this.firstFrameReceived && this.nativeStreamActive) {
@@ -16534,7 +17036,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16534
17036
  this.noFrameDeadlineTimer?.unref?.();
16535
17037
  }
16536
17038
  this.clearNoClientAutoStopTimer();
16537
- if (this.nativeStreamPrimeIdleStopMs > 0) {
17039
+ if (this.nativeStreamPrimeIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
16538
17040
  this.noClientAutoStopTimer = setTimeout(() => {
16539
17041
  if (this.connectedClients.size === 0) {
16540
17042
  this.rtspDebugLog(
@@ -16621,7 +17123,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16621
17123
  this.emit("clientDisconnected", clientId);
16622
17124
  if (this.connectedClients.size === 0) {
16623
17125
  this.clearNoClientAutoStopTimer();
16624
- if (this.nativeStreamIdleStopMs > 0) {
17126
+ if (this.nativeStreamIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
16625
17127
  this.noClientAutoStopTimer = setTimeout(() => {
16626
17128
  if (this.connectedClients.size === 0) {
16627
17129
  void this.stopNativeStream();
@@ -16692,9 +17194,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
16692
17194
  if (!this.active) {
16693
17195
  return;
16694
17196
  }
17197
+ this.tearingDown = true;
16695
17198
  this.logger.info(
16696
17199
  `[BaichuanRtspServer] Stopping RTSP server on ${this.listenHost}:${this.listenPort}...`
16697
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
+ }
16698
17213
  await this.stopNativeStream();
16699
17214
  const clientIds = Array.from(this.connectedClients);
16700
17215
  for (const clientId of clientIds) {
@@ -17368,7 +17883,7 @@ function buildSetSystemGeneralXml(patch) {
17368
17883
  }
17369
17884
 
17370
17885
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
17371
- var import_jimp = require("jimp");
17886
+ var import_jimp2 = require("jimp");
17372
17887
  init_ReolinkCgiApi();
17373
17888
  init_ReolinkHttpClient();
17374
17889
 
@@ -20328,8 +20843,8 @@ var parseSirenStatusListPushXml = (xml) => {
20328
20843
  };
20329
20844
 
20330
20845
  // src/emailPush/bus.ts
20331
- var import_node_events5 = require("events");
20332
- var emitter = new import_node_events5.EventEmitter();
20846
+ var import_node_events6 = require("events");
20847
+ var emitter = new import_node_events6.EventEmitter();
20333
20848
  var cameraResolver = () => void 0;
20334
20849
  var lastEventByCamera = /* @__PURE__ */ new Map();
20335
20850
  var MAX_GLOBAL_EVENTS = 300;
@@ -24208,12 +24723,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
24208
24723
  let wideImg;
24209
24724
  let teleImg;
24210
24725
  try {
24211
- wideImg = await import_jimp.Jimp.read(wide);
24726
+ wideImg = await import_jimp2.Jimp.read(wide);
24212
24727
  } catch {
24213
24728
  return wide;
24214
24729
  }
24215
24730
  try {
24216
- teleImg = await import_jimp.Jimp.read(tele);
24731
+ teleImg = await import_jimp2.Jimp.read(tele);
24217
24732
  } catch {
24218
24733
  return wide;
24219
24734
  }
@@ -24247,7 +24762,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
24247
24762
  });
24248
24763
  teleImg.resize({ w: pipW, h: pipH });
24249
24764
  wideImg.composite(teleImg, left, top);
24250
- return await wideImg.getBuffer(import_jimp.JimpMime.jpeg, { quality: 80 });
24765
+ return await wideImg.getBuffer(import_jimp2.JimpMime.jpeg, { quality: 80 });
24251
24766
  }
24252
24767
  const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
24253
24768
  const variant = options?.variant ?? "default";
@@ -25205,7 +25720,7 @@ ${xml}`);
25205
25720
  const chunks = [];
25206
25721
  let stderr = "";
25207
25722
  let timedOut = false;
25208
- const ff = (0, import_node_child_process3.spawn)(params.ffmpegPath, [
25723
+ const ff = (0, import_node_child_process4.spawn)(params.ffmpegPath, [
25209
25724
  "-hide_banner",
25210
25725
  "-loglevel",
25211
25726
  "error",
@@ -25290,7 +25805,7 @@ ${xml}`);
25290
25805
  const chunks = [];
25291
25806
  let stderr = "";
25292
25807
  let timedOut = false;
25293
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25808
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25294
25809
  "-hide_banner",
25295
25810
  "-loglevel",
25296
25811
  "error",
@@ -25406,7 +25921,7 @@ ${xml}`);
25406
25921
  ensureEnabled: true
25407
25922
  });
25408
25923
  await new Promise((resolve, reject) => {
25409
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25924
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25410
25925
  "-hide_banner",
25411
25926
  "-loglevel",
25412
25927
  "error",
@@ -25462,7 +25977,7 @@ ${stderr}`));
25462
25977
  const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
25463
25978
  await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(params.outputPath), { recursive: true });
25464
25979
  await new Promise((resolve, reject) => {
25465
- const ff = (0, import_node_child_process3.spawn)(ffmpegPath, [
25980
+ const ff = (0, import_node_child_process4.spawn)(ffmpegPath, [
25466
25981
  "-hide_banner",
25467
25982
  "-loglevel",
25468
25983
  "error",
@@ -26027,7 +26542,7 @@ ${stderr}`)
26027
26542
  * Convert a raw video keyframe to JPEG using ffmpeg.
26028
26543
  */
26029
26544
  async convertFrameToJpeg(params) {
26030
- const { spawn: spawn13 } = await import("child_process");
26545
+ const { spawn: spawn14 } = await import("child_process");
26031
26546
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
26032
26547
  const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
26033
26548
  return new Promise((resolve, reject) => {
@@ -26049,7 +26564,7 @@ ${stderr}`)
26049
26564
  "2",
26050
26565
  "pipe:1"
26051
26566
  ];
26052
- const proc = spawn13(ffmpeg, args, {
26567
+ const proc = spawn14(ffmpeg, args, {
26053
26568
  stdio: ["pipe", "pipe", "pipe"]
26054
26569
  });
26055
26570
  const chunks = [];
@@ -26192,7 +26707,7 @@ ${stderr}`)
26192
26707
  * Internal helper to mux video+audio into MP4 using ffmpeg.
26193
26708
  */
26194
26709
  async muxToMp4(params) {
26195
- const { spawn: spawn13 } = await import("child_process");
26710
+ const { spawn: spawn14 } = await import("child_process");
26196
26711
  const { randomUUID: randomUUID3 } = await import("crypto");
26197
26712
  const fs7 = await import("fs/promises");
26198
26713
  const os2 = await import("os");
@@ -26244,7 +26759,7 @@ ${stderr}`)
26244
26759
  outputPath
26245
26760
  );
26246
26761
  await new Promise((resolve, reject) => {
26247
- const p = spawn13(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26762
+ const p = spawn14(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
26248
26763
  let stderr = "";
26249
26764
  p.stderr.on("data", (d) => {
26250
26765
  stderr += d.toString();
@@ -31231,7 +31746,7 @@ ${scheduleItems}
31231
31746
  "mjpeg",
31232
31747
  "pipe:1"
31233
31748
  ];
31234
- 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"] });
31235
31750
  const chunks = [];
31236
31751
  let stderr = "";
31237
31752
  ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
@@ -31355,7 +31870,7 @@ ${scheduleItems}
31355
31870
  "pipe:1"
31356
31871
  ];
31357
31872
  }
31358
- 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"] });
31359
31874
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
31360
31875
  throw new Error("ffmpeg stdio streams not available");
31361
31876
  }
@@ -31602,7 +32117,7 @@ ${scheduleItems}
31602
32117
  "mp4",
31603
32118
  "pipe:1"
31604
32119
  ];
31605
- 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"] });
31606
32121
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
31607
32122
  throw new Error("ffmpeg stdio streams not available");
31608
32123
  }
@@ -31811,7 +32326,7 @@ ${scheduleItems}
31811
32326
  "independent_segments+temp_file",
31812
32327
  playlistPath
31813
32328
  ];
31814
- 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"] });
31815
32330
  if (!ff.stdin || !ff.stderr) {
31816
32331
  throw new Error("ffmpeg stdio streams not available");
31817
32332
  }
@@ -32783,14 +33298,14 @@ function buildHlsRedirectUrl(originalUrl) {
32783
33298
  }
32784
33299
 
32785
33300
  // src/reolink/discovery.ts
32786
- var import_node_child_process4 = require("child_process");
33301
+ var import_node_child_process5 = require("child_process");
32787
33302
  var import_node_crypto4 = require("crypto");
32788
33303
  var import_node_dgram2 = __toESM(require("dgram"), 1);
32789
33304
  var net3 = __toESM(require("net"), 1);
32790
33305
  var import_node_os2 = require("os");
32791
33306
  var import_node_util = require("util");
32792
33307
  init_ReolinkCgiApi();
32793
- 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);
32794
33309
  async function discoverViaUdpDirect(host, options) {
32795
33310
  if (!options.enableUdpDiscovery) return [];
32796
33311
  const logger = options.logger;
@@ -33832,7 +34347,7 @@ init_recordingFileName();
33832
34347
 
33833
34348
  // src/reolink/baichuan/endpoints-server.ts
33834
34349
  var import_node_http = __toESM(require("http"), 1);
33835
- var import_node_child_process5 = require("child_process");
34350
+ var import_node_child_process6 = require("child_process");
33836
34351
  function parseIntParam(v, def) {
33837
34352
  if (v == null) return def;
33838
34353
  const n = Number.parseInt(v, 10);
@@ -34071,7 +34586,7 @@ function createBaichuanEndpointsServer(opts) {
34071
34586
  "Cache-Control": "no-cache",
34072
34587
  Connection: "close"
34073
34588
  });
34074
- const ff2 = (0, import_node_child_process5.spawn)("ffmpeg", [
34589
+ const ff2 = (0, import_node_child_process6.spawn)("ffmpeg", [
34075
34590
  "-hide_banner",
34076
34591
  "-loglevel",
34077
34592
  "error",
@@ -34104,7 +34619,7 @@ function createBaichuanEndpointsServer(opts) {
34104
34619
  );
34105
34620
  res.setHeader("Cache-Control", "no-cache");
34106
34621
  res.setHeader("Connection", "close");
34107
- const ff = (0, import_node_child_process5.spawn)("ffmpeg", [
34622
+ const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
34108
34623
  "-hide_banner",
34109
34624
  "-loglevel",
34110
34625
  "error",
@@ -34215,7 +34730,7 @@ init_urls();
34215
34730
 
34216
34731
  // src/rtsp/server.ts
34217
34732
  var import_node_http2 = __toESM(require("http"), 1);
34218
- var import_node_child_process6 = require("child_process");
34733
+ var import_node_child_process7 = require("child_process");
34219
34734
  init_urls();
34220
34735
  function createRtspProxyServer(opts) {
34221
34736
  return import_node_http2.default.createServer((req, res) => {
@@ -34256,7 +34771,7 @@ function createRtspProxyServer(opts) {
34256
34771
  Connection: "close"
34257
34772
  });
34258
34773
  const rtspTransport = opts.rtspTransport ?? "tcp";
34259
- const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
34774
+ const ff = (0, import_node_child_process7.spawn)("ffmpeg", [
34260
34775
  "-hide_banner",
34261
34776
  "-loglevel",
34262
34777
  "error",
@@ -35128,9 +35643,9 @@ var import_node_net2 = __toESM(require("net"), 1);
35128
35643
  init_BaichuanVideoStream();
35129
35644
 
35130
35645
  // src/multifocal/compositeStream.ts
35131
- var import_node_child_process7 = require("child_process");
35646
+ var import_node_child_process8 = require("child_process");
35132
35647
  var import_node_crypto6 = require("crypto");
35133
- var import_node_events6 = require("events");
35648
+ var import_node_events7 = require("events");
35134
35649
  function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pipHeight, margin) {
35135
35650
  const pipW = Math.floor(pipWidth);
35136
35651
  const pipH = Math.floor(pipHeight);
@@ -35158,7 +35673,7 @@ function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pip
35158
35673
  return { x: m, y: m };
35159
35674
  }
35160
35675
  }
35161
- var CompositeStream = class extends import_node_events6.EventEmitter {
35676
+ var CompositeStream = class extends import_node_events7.EventEmitter {
35162
35677
  options;
35163
35678
  widerStream = null;
35164
35679
  teleStream = null;
@@ -35483,7 +35998,7 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35483
35998
  this.logger.log?.(
35484
35999
  `[CompositeStream] Starting ffmpeg (rtsp inputs): bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35485
36000
  );
35486
- this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
36001
+ this.ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegBin, ffmpegArgs, {
35487
36002
  stdio: ["ignore", "pipe", "pipe"]
35488
36003
  });
35489
36004
  this.ffmpegProcess.on("error", (error) => {
@@ -35613,7 +36128,7 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35613
36128
  this.logger.log?.(
35614
36129
  `[CompositeStream] Starting ffmpeg: bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35615
36130
  );
35616
- this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
36131
+ this.ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegBin, ffmpegArgs, {
35617
36132
  stdio: ["pipe", "pipe", "pipe", "pipe"]
35618
36133
  });
35619
36134
  this.ffmpegProcess.on("error", (error) => {
@@ -36191,7 +36706,8 @@ async function createRfc4571TcpServerInternal(options) {
36191
36706
  apisToClose.add(resolvedCompositeApis.widerApi);
36192
36707
  if (resolvedCompositeApis?.teleApi)
36193
36708
  apisToClose.add(resolvedCompositeApis.teleApi);
36194
- const uptimeRestartMs = uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
36709
+ const alwaysOnEnabled = Boolean(options.alwaysOn?.enabled) && !isComposite;
36710
+ const uptimeRestartMs = alwaysOnEnabled ? 0 : uptimeRestartMsOpt ?? (isComposite ? 6e4 : 1e4);
36195
36711
  const variantSuffix = variant && variant !== "default" ? ` variant=${variant}` : "";
36196
36712
  const logPrefix = isComposite ? `[native-rfc4571 composite profile=${profile}${variantSuffix}${requestedId ? ` id=${requestedId}` : ""}]` : `[native-rfc4571 ch=${channel} profile=${profile}${variantSuffix}]`;
36197
36713
  const log = (message) => {
@@ -36215,6 +36731,7 @@ async function createRfc4571TcpServerInternal(options) {
36215
36731
  );
36216
36732
  let videoStream;
36217
36733
  let isCompositeStream = false;
36734
+ let alwaysOnController;
36218
36735
  if (isComposite) {
36219
36736
  const widerChannel = compositeOptions?.widerChannel ?? 0;
36220
36737
  const teleChannel = compositeOptions?.teleChannel ?? 1;
@@ -36357,7 +36874,7 @@ async function createRfc4571TcpServerInternal(options) {
36357
36874
  } else {
36358
36875
  streamClient = baseApi.client;
36359
36876
  }
36360
- videoStream = new BaichuanVideoStream({
36877
+ const createLiveStream = async () => new BaichuanVideoStream({
36361
36878
  client: streamClient,
36362
36879
  api: baseApi,
36363
36880
  channel: ch,
@@ -36365,10 +36882,39 @@ async function createRfc4571TcpServerInternal(options) {
36365
36882
  variant,
36366
36883
  logger
36367
36884
  });
36368
- await videoStream.start();
36369
- log(
36370
- `stream started (ch=${ch} profile=${profile}${deviceId ? ` dedicated=${deviceId}` : ""})`
36371
- );
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
+ }
36372
36918
  }
36373
36919
  const waitForKeyframe = async () => {
36374
36920
  if (isCompositeStream) {
@@ -36496,6 +37042,12 @@ async function createRfc4571TcpServerInternal(options) {
36496
37042
  try {
36497
37043
  keyframe = await waitForKeyframe();
36498
37044
  } catch (e) {
37045
+ if (alwaysOnController) {
37046
+ try {
37047
+ await alwaysOnController.stop();
37048
+ } catch {
37049
+ }
37050
+ }
36499
37051
  try {
36500
37052
  await videoStream.stop();
36501
37053
  } catch {
@@ -36744,12 +37296,13 @@ async function createRfc4571TcpServerInternal(options) {
36744
37296
  } catch {
36745
37297
  }
36746
37298
  muxer = makeMuxer();
37299
+ const restartable = videoStream;
36747
37300
  try {
36748
- await videoStream.stop();
37301
+ await restartable.stop();
36749
37302
  } catch {
36750
37303
  }
36751
37304
  try {
36752
- await videoStream.start();
37305
+ await restartable.start();
36753
37306
  } catch (e) {
36754
37307
  restarting = false;
36755
37308
  close(e).catch(() => {
@@ -36770,6 +37323,12 @@ async function createRfc4571TcpServerInternal(options) {
36770
37323
  cancelIdleTeardown();
36771
37324
  const reasonStr = reason?.message || reason?.toString?.() || reason || "requested";
36772
37325
  muxer.close();
37326
+ if (alwaysOnController) {
37327
+ try {
37328
+ await alwaysOnController.stop();
37329
+ } catch {
37330
+ }
37331
+ }
36773
37332
  try {
36774
37333
  await videoStream.stop();
36775
37334
  } catch {
@@ -37369,7 +37928,7 @@ async function createRfc4571TcpServerForReplay(options) {
37369
37928
 
37370
37929
  // src/rfc/replay-http-server.ts
37371
37930
  var import_node_http3 = __toESM(require("http"), 1);
37372
- var import_node_child_process8 = require("child_process");
37931
+ var import_node_child_process9 = require("child_process");
37373
37932
  var import_node_stream2 = require("stream");
37374
37933
  async function createReplayHttpServer(options) {
37375
37934
  const {
@@ -37523,7 +38082,7 @@ async function createReplayHttpServer(options) {
37523
38082
  "pipe:1"
37524
38083
  ];
37525
38084
  log(`spawning ffmpeg: ${ffmpegPath} ${ffmpegArgs.join(" ")}`);
37526
- ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegPath, ffmpegArgs, {
38085
+ ffmpegProcess = (0, import_node_child_process9.spawn)(ffmpegPath, ffmpegArgs, {
37527
38086
  stdio: ["pipe", "pipe", "pipe"]
37528
38087
  });
37529
38088
  ffmpegProcess.stdout?.pipe(outputStream).pipe(res);
@@ -37624,7 +38183,7 @@ async function createReplayHttpServer(options) {
37624
38183
  init_BaichuanVideoStream();
37625
38184
 
37626
38185
  // src/baichuan/stream/Go2rtcTcpServer.ts
37627
- var import_node_events7 = require("events");
38186
+ var import_node_events8 = require("events");
37628
38187
  var net4 = __toESM(require("net"), 1);
37629
38188
  init_H264Converter();
37630
38189
  init_H265Converter();
@@ -37736,7 +38295,7 @@ var NativeStreamFanout2 = class {
37736
38295
  this.pumpPromise = null;
37737
38296
  }
37738
38297
  };
37739
- var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEmitter {
38298
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events8.EventEmitter {
37740
38299
  api;
37741
38300
  channel;
37742
38301
  profile;
@@ -38427,8 +38986,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events7.EventEm
38427
38986
  };
38428
38987
 
38429
38988
  // src/baichuan/stream/BaichuanHttpStreamServer.ts
38430
- var import_node_events8 = require("events");
38431
- var import_node_child_process9 = require("child_process");
38989
+ var import_node_events9 = require("events");
38990
+ var import_node_child_process10 = require("child_process");
38432
38991
  var http4 = __toESM(require("http"), 1);
38433
38992
  var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
38434
38993
  var NAL_START_CODE_3B3 = Buffer.from([0, 0, 1]);
@@ -38474,7 +39033,7 @@ function isH264KeyframeFromAnnexB(annexB) {
38474
39033
  }
38475
39034
  return false;
38476
39035
  }
38477
- var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
39036
+ var BaichuanHttpStreamServer = class extends import_node_events9.EventEmitter {
38478
39037
  videoStream;
38479
39038
  listenPort;
38480
39039
  path;
@@ -38538,7 +39097,7 @@ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
38538
39097
  this.httpServer.on("error", reject);
38539
39098
  });
38540
39099
  this.logger.info(`[BaichuanHttpStreamServer] Starting ffmpeg for H.264 -> MPEG-TS conversion...`);
38541
- const ffmpeg = (0, import_node_child_process9.spawn)("ffmpeg", [
39100
+ const ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", [
38542
39101
  "-hide_banner",
38543
39102
  // ffmpeg warnings often include non-fatal decode messages (e.g. decode_slice_header),
38544
39103
  // which we don't want to treat as application errors.
@@ -38746,15 +39305,15 @@ var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
38746
39305
  };
38747
39306
 
38748
39307
  // src/baichuan/stream/BaichuanMjpegServer.ts
38749
- var import_node_events10 = require("events");
39308
+ var import_node_events11 = require("events");
38750
39309
  var http5 = __toESM(require("http"), 1);
38751
39310
 
38752
39311
  // src/baichuan/stream/MjpegTransformer.ts
38753
- var import_node_events9 = require("events");
38754
- var import_node_child_process10 = require("child_process");
39312
+ var import_node_events10 = require("events");
39313
+ var import_node_child_process11 = require("child_process");
38755
39314
  var JPEG_SOI = Buffer.from([255, 216]);
38756
39315
  var JPEG_EOI = Buffer.from([255, 217]);
38757
- var MjpegTransformer = class extends import_node_events9.EventEmitter {
39316
+ var MjpegTransformer = class extends import_node_events10.EventEmitter {
38758
39317
  options;
38759
39318
  ffmpeg = null;
38760
39319
  started = false;
@@ -38812,7 +39371,7 @@ var MjpegTransformer = class extends import_node_events9.EventEmitter {
38812
39371
  "pipe:1"
38813
39372
  );
38814
39373
  this.log("debug", `Starting FFmpeg with args: ${args.join(" ")}`);
38815
- this.ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", args, {
39374
+ this.ffmpeg = (0, import_node_child_process11.spawn)("ffmpeg", args, {
38816
39375
  stdio: ["pipe", "pipe", "pipe"]
38817
39376
  });
38818
39377
  this.ffmpeg.stdout.on("data", (data) => {
@@ -38953,7 +39512,7 @@ Content-Length: ${frame.length}\r
38953
39512
  // src/baichuan/stream/BaichuanMjpegServer.ts
38954
39513
  init_H264Converter();
38955
39514
  init_H265Converter();
38956
- var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
39515
+ var BaichuanMjpegServer = class extends import_node_events11.EventEmitter {
38957
39516
  options;
38958
39517
  clients = /* @__PURE__ */ new Map();
38959
39518
  httpServer = null;
@@ -39234,14 +39793,14 @@ var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
39234
39793
  };
39235
39794
 
39236
39795
  // src/baichuan/stream/BaichuanWebRTCServer.ts
39237
- var import_node_events12 = require("events");
39796
+ var import_node_events13 = require("events");
39238
39797
  init_BcMediaAnnexBDecoder();
39239
39798
 
39240
39799
  // src/baichuan/stream/AacToOpusTranscoder.ts
39241
- var import_node_child_process11 = require("child_process");
39800
+ var import_node_child_process12 = require("child_process");
39242
39801
  var import_node_dgram3 = require("dgram");
39243
- var import_node_events11 = require("events");
39244
- var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
39802
+ var import_node_events12 = require("events");
39803
+ var AacToOpusTranscoder = class extends import_node_events12.EventEmitter {
39245
39804
  opts;
39246
39805
  socket = null;
39247
39806
  ffmpeg = null;
@@ -39318,7 +39877,7 @@ var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
39318
39877
  `rtp://127.0.0.1:${this.port}`
39319
39878
  ];
39320
39879
  this.log("info", `spawning ffmpeg with: ${this.opts.ffmpegPath} ${args.join(" ")}`);
39321
- 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, {
39322
39881
  stdio: ["pipe", "ignore", "pipe"]
39323
39882
  });
39324
39883
  this.ffmpeg.on("error", (err) => {
@@ -39458,7 +40017,7 @@ function getH264NalType(nalUnit) {
39458
40017
  function getH265NalType2(nalUnit) {
39459
40018
  return nalUnit[0] >> 1 & 63;
39460
40019
  }
39461
- var BaichuanWebRTCServer = class extends import_node_events12.EventEmitter {
40020
+ var BaichuanWebRTCServer = class extends import_node_events13.EventEmitter {
39462
40021
  options;
39463
40022
  sessions = /* @__PURE__ */ new Map();
39464
40023
  sessionIdCounter = 0;
@@ -40450,12 +41009,12 @@ Error: ${err}`
40450
41009
  };
40451
41010
 
40452
41011
  // src/baichuan/stream/BaichuanHlsServer.ts
40453
- var import_node_events13 = require("events");
41012
+ var import_node_events14 = require("events");
40454
41013
  var import_node_fs = __toESM(require("fs"), 1);
40455
41014
  var import_promises3 = __toESM(require("fs/promises"), 1);
40456
41015
  var import_node_os3 = __toESM(require("os"), 1);
40457
41016
  var import_node_path3 = __toESM(require("path"), 1);
40458
- var import_node_child_process12 = require("child_process");
41017
+ var import_node_child_process13 = require("child_process");
40459
41018
  init_BcMediaAnnexBDecoder();
40460
41019
  init_H264Converter();
40461
41020
  init_H265Converter();
@@ -40530,7 +41089,7 @@ function getNalTypes(codec, annexB) {
40530
41089
  }
40531
41090
  });
40532
41091
  }
40533
- var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
41092
+ var BaichuanHlsServer = class extends import_node_events14.EventEmitter {
40534
41093
  api;
40535
41094
  channel;
40536
41095
  profile;
@@ -40932,7 +41491,7 @@ var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
40932
41491
  this.segmentPattern,
40933
41492
  this.playlistPath
40934
41493
  );
40935
- const p = (0, import_node_child_process12.spawn)(this.ffmpegPath, args, {
41494
+ const p = (0, import_node_child_process13.spawn)(this.ffmpegPath, args, {
40936
41495
  stdio: ["pipe", "ignore", "pipe"]
40937
41496
  });
40938
41497
  p.on("error", (err) => {
@@ -40977,7 +41536,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
40977
41536
  return methods.filter((m) => m === "local-direct");
40978
41537
  }
40979
41538
  function normalizeUid(uid) {
40980
- const v = uid?.trim();
41539
+ const v = uid?.trim().toUpperCase();
40981
41540
  return v ? v : void 0;
40982
41541
  }
40983
41542
  function maskUid(uid) {
@@ -41056,13 +41615,13 @@ async function pingHost(host, timeoutMs = 3e3) {
41056
41615
  }
41057
41616
  return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
41058
41617
  };
41059
- const { spawn: spawn13 } = await import("child_process");
41618
+ const { spawn: spawn14 } = await import("child_process");
41060
41619
  for (const bin of pingCandidates) {
41061
41620
  const ranOk = await new Promise((resolve) => {
41062
41621
  let settled = false;
41063
41622
  let child;
41064
41623
  try {
41065
- child = spawn13(bin, pingArgs(bin), { stdio: "ignore" });
41624
+ child = spawn14(bin, pingArgs(bin), { stdio: "ignore" });
41066
41625
  } catch {
41067
41626
  resolve("spawn-failed");
41068
41627
  return;
@@ -41695,10 +42254,10 @@ async function autoDetectDeviceType(inputs) {
41695
42254
  }
41696
42255
 
41697
42256
  // src/multifocal/compositeRtspServer.ts
41698
- var import_node_events14 = require("events");
41699
- var import_node_child_process13 = require("child_process");
42257
+ var import_node_events15 = require("events");
42258
+ var import_node_child_process14 = require("child_process");
41700
42259
  var net5 = __toESM(require("net"), 1);
41701
- var CompositeRtspServer = class extends import_node_events14.EventEmitter {
42260
+ var CompositeRtspServer = class extends import_node_events15.EventEmitter {
41702
42261
  options;
41703
42262
  compositeStream = null;
41704
42263
  rtspServer = null;
@@ -41803,7 +42362,7 @@ var CompositeRtspServer = class extends import_node_events14.EventEmitter {
41803
42362
  this.logger.log?.(
41804
42363
  `[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
41805
42364
  );
41806
- this.ffmpegProcess = (0, import_node_child_process13.spawn)("ffmpeg", ffmpegArgs, {
42365
+ this.ffmpegProcess = (0, import_node_child_process14.spawn)("ffmpeg", ffmpegArgs, {
41807
42366
  stdio: ["pipe", "pipe", "pipe"]
41808
42367
  });
41809
42368
  this.ffmpegProcess.on("error", (error) => {
@@ -42474,7 +43033,7 @@ var RtspBackchannel = class _RtspBackchannel {
42474
43033
  };
42475
43034
 
42476
43035
  // src/baichuan/stream/BaichuanRtspBackchannelServer.ts
42477
- var import_node_events15 = require("events");
43036
+ var import_node_events16 = require("events");
42478
43037
  var net6 = __toESM(require("net"), 1);
42479
43038
  var crypto3 = __toESM(require("crypto"), 1);
42480
43039
  var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
@@ -42531,7 +43090,7 @@ function extractPublicEndpoint(url, requestText) {
42531
43090
  if (hostHeader) return hostHeader;
42532
43091
  return null;
42533
43092
  }
42534
- var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
43093
+ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events16.EventEmitter {
42535
43094
  listenHost;
42536
43095
  listenPort;
42537
43096
  logger;
@@ -43512,7 +44071,9 @@ function buildInitialStatus(config) {
43512
44071
  // Annotate the CommonJS export names for ESM import in node:
43513
44072
  0 && (module.exports = {
43514
44073
  ALL_UDP_DISCOVERY_METHODS,
44074
+ ALWAYS_ON_DEFAULTS,
43515
44075
  AesStreamDecryptor,
44076
+ AlwaysOnController,
43516
44077
  AutodiscoveryClient,
43517
44078
  BC_AES_IV,
43518
44079
  BC_CLASS_FILE_DOWNLOAD,
@@ -43669,6 +44230,7 @@ function buildInitialStatus(config) {
43669
44230
  BcUdpStream,
43670
44231
  CompositeRtspServer,
43671
44232
  CompositeStream,
44233
+ ContinuousVideoStream,
43672
44234
  DEFAULT_SHELTER_CANVAS,
43673
44235
  DUAL_LENS_DUAL_MOTION_MODELS,
43674
44236
  DUAL_LENS_MODELS,
@@ -43682,6 +44244,7 @@ function buildInitialStatus(config) {
43682
44244
  MpegTsMuxer,
43683
44245
  NVR_HUB_EXACT_TYPES,
43684
44246
  NVR_HUB_MODEL_PATTERNS,
44247
+ PlaceholderRenderer,
43685
44248
  ReolinkBaichuanApi,
43686
44249
  ReolinkCgiApi,
43687
44250
  ReolinkHttpClient,