@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.
@@ -5977,9 +5977,354 @@ async function* createNativeStream(api, channel, profile, options) {
5977
5977
  }
5978
5978
  }
5979
5979
 
5980
- // src/baichuan/stream/BaichuanRtspServer.ts
5981
- import { EventEmitter as EventEmitter3 } from "events";
5980
+ // src/baichuan/stream/alwaysOnTypes.ts
5981
+ var ALWAYS_ON_DEFAULTS = {
5982
+ triggers: ["motion", "doorbell"],
5983
+ windowMs: 15e3,
5984
+ idleFps: 1,
5985
+ primeOnStart: true,
5986
+ placeholder: { enabled: true, text: "Sleeping", opacity: 0.5 }
5987
+ };
5988
+
5989
+ // src/baichuan/stream/PlaceholderRenderer.ts
5982
5990
  import { spawn } from "child_process";
5991
+ import { Jimp, JimpMime, loadFont, measureText, measureTextHeight } from "jimp";
5992
+ import { SANS_32_WHITE, SANS_64_WHITE, SANS_128_WHITE } from "jimp/fonts";
5993
+ function ffmpegCodec(videoType) {
5994
+ if (videoType === "H265") {
5995
+ return {
5996
+ inputFormat: "hevc",
5997
+ encoder: "libx265",
5998
+ outputFormat: "hevc"
5999
+ };
6000
+ }
6001
+ return {
6002
+ inputFormat: "h264",
6003
+ encoder: "libx264",
6004
+ outputFormat: "h264"
6005
+ };
6006
+ }
6007
+ function runFfmpeg(args, input) {
6008
+ return new Promise((resolve, reject) => {
6009
+ const proc = spawn("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
6010
+ const stdoutChunks = [];
6011
+ const stderrChunks = [];
6012
+ proc.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
6013
+ proc.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
6014
+ proc.on("error", (error) => reject(error));
6015
+ proc.on("close", (code) => {
6016
+ if (code === 0) {
6017
+ resolve(Buffer.concat(stdoutChunks));
6018
+ return;
6019
+ }
6020
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
6021
+ reject(new Error(`ffmpeg exited with code ${code}: ${stderr}`));
6022
+ });
6023
+ const stdin = proc.stdin;
6024
+ if (!stdin) {
6025
+ reject(new Error("ffmpeg stdin not available"));
6026
+ return;
6027
+ }
6028
+ stdin.on("error", (error) => reject(error));
6029
+ stdin.end(input);
6030
+ });
6031
+ }
6032
+ var PlaceholderRenderer = class {
6033
+ opts;
6034
+ logger;
6035
+ constructor(args) {
6036
+ this.opts = { ...ALWAYS_ON_DEFAULTS.placeholder, ...args.placeholder ?? {} };
6037
+ this.logger = args.logger;
6038
+ }
6039
+ /** Returns the access unit bytes to emit as placeholder, or null if none available. */
6040
+ async render(keyframe) {
6041
+ if (!keyframe) return null;
6042
+ if (!this.opts.enabled) return keyframe.data;
6043
+ try {
6044
+ const jpeg = await this.decodeToJpeg(keyframe);
6045
+ const decorated = await this.decorate(jpeg);
6046
+ const idr = await this.encodeIdr(decorated, keyframe.videoType);
6047
+ if (!idr || idr.length === 0) {
6048
+ throw new Error("ffmpeg produced empty IDR output");
6049
+ }
6050
+ return idr;
6051
+ } catch (error) {
6052
+ this.logger?.warn?.(
6053
+ "PlaceholderRenderer: decoration failed, falling back to raw keyframe",
6054
+ error instanceof Error ? error.message : error
6055
+ );
6056
+ return keyframe.data;
6057
+ }
6058
+ }
6059
+ /** Decodes the cached keyframe access unit into a single JPEG still via ffmpeg. */
6060
+ async decodeToJpeg(keyframe) {
6061
+ const { inputFormat } = ffmpegCodec(keyframe.videoType);
6062
+ return runFfmpeg(
6063
+ [
6064
+ "-hide_banner",
6065
+ "-loglevel",
6066
+ "error",
6067
+ "-f",
6068
+ inputFormat,
6069
+ "-i",
6070
+ "pipe:0",
6071
+ "-frames:v",
6072
+ "1",
6073
+ "-f",
6074
+ "mjpeg",
6075
+ "pipe:1"
6076
+ ],
6077
+ keyframe.data
6078
+ );
6079
+ }
6080
+ /** Dims the still and prints the overlay text using jimp, returning a JPEG buffer. */
6081
+ async decorate(jpeg) {
6082
+ const image = await Jimp.read(jpeg);
6083
+ const op = Math.max(0, Math.min(1, this.opts.opacity));
6084
+ if (op < 1) {
6085
+ const data = image.bitmap.data;
6086
+ for (let i = 0; i < data.length; i += 4) {
6087
+ data[i] = data[i] * op;
6088
+ data[i + 1] = data[i + 1] * op;
6089
+ data[i + 2] = data[i + 2] * op;
6090
+ }
6091
+ }
6092
+ const fontDef = image.width >= 1280 ? SANS_128_WHITE : image.width >= 640 ? SANS_64_WHITE : SANS_32_WHITE;
6093
+ const font = await loadFont(fontDef);
6094
+ const text = this.opts.text;
6095
+ const textWidth = measureText(font, text);
6096
+ const textHeight = measureTextHeight(font, text, image.width);
6097
+ const x = Math.max(0, Math.round((image.width - textWidth) / 2));
6098
+ const y = Math.max(0, Math.round((image.height - textHeight) / 2));
6099
+ image.print({ font, x, y, text });
6100
+ return image.getBuffer(JimpMime.jpeg);
6101
+ }
6102
+ /** Encodes the decorated JPEG into a single IDR access unit in the target codec. */
6103
+ async encodeIdr(jpeg, videoType) {
6104
+ const { encoder, outputFormat } = ffmpegCodec(videoType);
6105
+ return runFfmpeg(
6106
+ [
6107
+ "-hide_banner",
6108
+ "-loglevel",
6109
+ "error",
6110
+ "-f",
6111
+ "image2pipe",
6112
+ "-i",
6113
+ "pipe:0",
6114
+ "-frames:v",
6115
+ "1",
6116
+ "-c:v",
6117
+ encoder,
6118
+ "-pix_fmt",
6119
+ "yuv420p",
6120
+ "-f",
6121
+ outputFormat,
6122
+ "pipe:1"
6123
+ ],
6124
+ jpeg
6125
+ );
6126
+ }
6127
+ };
6128
+
6129
+ // src/baichuan/stream/ContinuousVideoStream.ts
6130
+ import { EventEmitter as EventEmitter3 } from "events";
6131
+ var ContinuousVideoStream = class extends EventEmitter3 {
6132
+ constructor(opts) {
6133
+ super();
6134
+ this.opts = opts;
6135
+ this.idleFps = Math.max(0.1, opts.idleFps ?? ALWAYS_ON_DEFAULTS.idleFps);
6136
+ this.logger = opts.logger;
6137
+ const rendererArgs = {};
6138
+ if (opts.placeholder !== void 0) rendererArgs.placeholder = opts.placeholder;
6139
+ if (opts.logger !== void 0) rendererArgs.logger = opts.logger;
6140
+ this.renderer = opts.renderer ?? new PlaceholderRenderer(rendererArgs);
6141
+ }
6142
+ live = null;
6143
+ lastKeyframe = null;
6144
+ lastMicroseconds = 0;
6145
+ idleFps;
6146
+ renderer;
6147
+ logger;
6148
+ stopped = false;
6149
+ starting = false;
6150
+ idleTimer = null;
6151
+ idlePlaceholder = null;
6152
+ hasCachedKeyframe() {
6153
+ return this.lastKeyframe !== null;
6154
+ }
6155
+ async goLive() {
6156
+ if (this.stopped || this.live || this.starting) return;
6157
+ this.starting = true;
6158
+ try {
6159
+ this.stopIdleLoop();
6160
+ const stream = await this.opts.createLiveStream();
6161
+ this.live = stream;
6162
+ stream.on("videoAccessUnit", this.onLiveAccessUnit);
6163
+ stream.on("additionalHeader", this.onAdditionalHeader);
6164
+ stream.on("audioFrame", this.onAudioFrame);
6165
+ stream.on("error", this.onLiveError);
6166
+ await stream.start().catch((e) => this.emit("error", e));
6167
+ } finally {
6168
+ this.starting = false;
6169
+ }
6170
+ }
6171
+ async goIdle() {
6172
+ if (!this.live) return;
6173
+ const s = this.live;
6174
+ this.live = null;
6175
+ s.off("videoAccessUnit", this.onLiveAccessUnit);
6176
+ s.off("additionalHeader", this.onAdditionalHeader);
6177
+ s.off("audioFrame", this.onAudioFrame);
6178
+ s.off("error", this.onLiveError);
6179
+ await s.stop().catch(() => {
6180
+ });
6181
+ await this.startIdleLoop();
6182
+ }
6183
+ async stop() {
6184
+ this.stopped = true;
6185
+ await this.goIdle();
6186
+ this.stopIdleLoop();
6187
+ this.emit("close");
6188
+ }
6189
+ async startIdleLoop() {
6190
+ if (this.stopped) return;
6191
+ this.idlePlaceholder = await this.renderer.render(this.lastKeyframe);
6192
+ if (!this.idlePlaceholder || !this.lastKeyframe) {
6193
+ this.logger?.debug?.("[ContinuousVideoStream] no keyframe yet; idle loop deferred");
6194
+ return;
6195
+ }
6196
+ const stepUs = Math.round(1e6 / this.idleFps);
6197
+ const videoType = this.lastKeyframe.videoType;
6198
+ this.idleTimer = setInterval(() => {
6199
+ if (!this.idlePlaceholder) return;
6200
+ this.lastMicroseconds += stepUs;
6201
+ this.emit("videoAccessUnit", {
6202
+ data: this.idlePlaceholder,
6203
+ isKeyframe: true,
6204
+ videoType,
6205
+ microseconds: this.lastMicroseconds
6206
+ });
6207
+ }, Math.round(1e3 / this.idleFps));
6208
+ }
6209
+ stopIdleLoop() {
6210
+ if (this.idleTimer) {
6211
+ clearInterval(this.idleTimer);
6212
+ this.idleTimer = null;
6213
+ }
6214
+ this.idlePlaceholder = null;
6215
+ }
6216
+ onLiveAccessUnit = (au) => {
6217
+ if (au.isKeyframe) {
6218
+ this.lastKeyframe = { data: au.data, videoType: au.videoType };
6219
+ }
6220
+ this.lastMicroseconds = au.microseconds;
6221
+ this.emit("videoAccessUnit", au);
6222
+ };
6223
+ onAdditionalHeader = (h) => this.emit("additionalHeader", h);
6224
+ onAudioFrame = (a) => this.emit("audioFrame", a);
6225
+ onLiveError = (e) => this.emit("error", e);
6226
+ };
6227
+
6228
+ // src/baichuan/stream/AlwaysOnController.ts
6229
+ var AlwaysOnController = class {
6230
+ constructor(o) {
6231
+ this.o = o;
6232
+ this.triggers = new Set(o.options.triggers ?? ALWAYS_ON_DEFAULTS.triggers);
6233
+ this.windowMs = o.options.windowMs ?? ALWAYS_ON_DEFAULTS.windowMs;
6234
+ this.primeOnStart = o.options.primeOnStart ?? ALWAYS_ON_DEFAULTS.primeOnStart;
6235
+ this.logger = o.logger;
6236
+ }
6237
+ triggers;
6238
+ windowMs;
6239
+ primeOnStart;
6240
+ logger;
6241
+ windowTimer = null;
6242
+ live = false;
6243
+ started = false;
6244
+ handler = (e) => void this.onEvent(e);
6245
+ get windowSeconds() {
6246
+ return Math.round(this.windowMs / 1e3);
6247
+ }
6248
+ async start() {
6249
+ if (this.started) return;
6250
+ this.started = true;
6251
+ await this.o.api.onSimpleEvent(this.handler);
6252
+ this.logger?.info?.(
6253
+ `[AlwaysOnController] started ch${this.o.channel} \u2014 triggers=[${[...this.triggers].join(", ")}], window=${this.windowSeconds}s, primeOnStart=${this.primeOnStart}`
6254
+ );
6255
+ if (this.primeOnStart) {
6256
+ await this.openWindow("prime");
6257
+ }
6258
+ }
6259
+ async stop() {
6260
+ if (!this.started) return;
6261
+ this.started = false;
6262
+ if (this.windowTimer) {
6263
+ clearTimeout(this.windowTimer);
6264
+ this.windowTimer = null;
6265
+ }
6266
+ await this.o.api.offSimpleEvent(this.handler).catch(() => {
6267
+ });
6268
+ if (this.live) {
6269
+ this.live = false;
6270
+ await this.o.goIdle().catch(() => {
6271
+ });
6272
+ }
6273
+ this.logger?.info?.(`[AlwaysOnController] stopped ch${this.o.channel}`);
6274
+ }
6275
+ async onEvent(e) {
6276
+ if (e.channel !== this.o.channel) return;
6277
+ if (!this.triggers.has(e.type)) {
6278
+ this.logger?.debug?.(
6279
+ `[AlwaysOnController] event '${e.type}' ch${e.channel} ignored (not a configured trigger)`
6280
+ );
6281
+ return;
6282
+ }
6283
+ await this.openWindow(e.type);
6284
+ }
6285
+ async openWindow(reason) {
6286
+ if (this.windowTimer) clearTimeout(this.windowTimer);
6287
+ if (!this.live) {
6288
+ this.live = true;
6289
+ try {
6290
+ await this.o.api.wakeUp(this.o.channel).catch(() => {
6291
+ });
6292
+ await this.o.goLive();
6293
+ this.logger?.info?.(
6294
+ `[AlwaysOnController] live window OPENED (trigger=${reason}) \u2014 streaming real frames; will sleep in ${this.windowSeconds}s without new events`
6295
+ );
6296
+ } catch (err) {
6297
+ this.live = false;
6298
+ this.logger?.warn?.(
6299
+ `[AlwaysOnController] goLive failed: ${err?.message}`
6300
+ );
6301
+ return;
6302
+ }
6303
+ } else {
6304
+ this.logger?.info?.(
6305
+ `[AlwaysOnController] live window EXTENDED (trigger=${reason}) \u2014 sleep timer reset to ${this.windowSeconds}s`
6306
+ );
6307
+ }
6308
+ this.windowTimer = setTimeout(() => void this.closeWindow(), this.windowMs);
6309
+ }
6310
+ async closeWindow() {
6311
+ this.windowTimer = null;
6312
+ if (!this.live) return;
6313
+ this.live = false;
6314
+ this.logger?.info?.(
6315
+ `[AlwaysOnController] live window CLOSED \u2014 going idle (placeholder); camera can sleep`
6316
+ );
6317
+ await this.o.goIdle().catch(
6318
+ (err) => this.logger?.warn?.(
6319
+ `[AlwaysOnController] goIdle failed: ${err?.message}`
6320
+ )
6321
+ );
6322
+ }
6323
+ };
6324
+
6325
+ // src/baichuan/stream/BaichuanRtspServer.ts
6326
+ import { EventEmitter as EventEmitter4 } from "events";
6327
+ import { spawn as spawn2 } from "child_process";
5983
6328
  import * as net2 from "net";
5984
6329
  import * as dgram2 from "dgram";
5985
6330
  import * as crypto from "crypto";
@@ -6206,7 +6551,7 @@ function envBool(value, defaultValue) {
6206
6551
  if (v === "0" || v === "false" || v === "no" || v === "off") return false;
6207
6552
  return defaultValue;
6208
6553
  }
6209
- var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6554
+ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter4 {
6210
6555
  api;
6211
6556
  channel;
6212
6557
  profile;
@@ -6221,6 +6566,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6221
6566
  deviceId;
6222
6567
  dedicatedSessionRelease;
6223
6568
  externalListener;
6569
+ // Always-on continuous stream (battery cameras). Populated only when
6570
+ // `options.alwaysOn?.enabled`; the default (non-alwaysOn) path leaves these
6571
+ // null/undefined and is byte-for-byte equivalent in behaviour.
6572
+ alwaysOnOptions;
6573
+ continuousStream = null;
6574
+ alwaysOnController = null;
6224
6575
  // Authentication
6225
6576
  authCredentials = [];
6226
6577
  requireAuth;
@@ -6235,6 +6586,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6235
6586
  // Set of client IDs (IP:port)
6236
6587
  nativeStreamActive = false;
6237
6588
  // Whether the native stream is currently active
6589
+ tearingDown = false;
6590
+ // True while stop() is running; suppresses onEnd-driven restarts
6238
6591
  clientConnectionServer;
6239
6592
  // TCP server to track connections
6240
6593
  streamMetadata = null;
@@ -6422,6 +6775,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6422
6775
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
6423
6776
  this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
6424
6777
  this.lazyMetadata = options.lazyMetadata ?? false;
6778
+ this.alwaysOnOptions = options.alwaysOn;
6425
6779
  const transport = this.api.client.getTransport();
6426
6780
  this.flow = createRtspFlow(transport, "H264");
6427
6781
  }
@@ -7545,7 +7899,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7545
7899
  this.rtspDebugLog(
7546
7900
  `Spawning ffmpeg for client ${clientId}: ffmpeg ${ffmpegArgs.join(" ")}`
7547
7901
  );
7548
- ffmpeg = spawn("ffmpeg", ffmpegArgs, {
7902
+ ffmpeg = spawn2("ffmpeg", ffmpegArgs, {
7549
7903
  stdio
7550
7904
  });
7551
7905
  try {
@@ -7909,6 +8263,141 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7909
8263
  }
7910
8264
  });
7911
8265
  }
8266
+ /**
8267
+ * Always-on source: bridge a {@link ContinuousVideoStream} into the existing
8268
+ * fanout. Yields the same frame shape that `createNativeStream` produces, so
8269
+ * the rest of the pipeline (prebuffer, param-set extraction, per-client
8270
+ * subscribe, ffmpeg/direct-RTP) is unchanged.
8271
+ *
8272
+ * The CVS itself is long-lived (created once, reused across native-stream
8273
+ * restarts) and is driven by the {@link AlwaysOnController}, which opens/closes
8274
+ * live windows from camera events. Each fanout source generator only forwards
8275
+ * CVS events to the fanout pump for as long as `signal` is not aborted.
8276
+ */
8277
+ async *createContinuousSource(dedicatedClient, signal) {
8278
+ const cvs = this.ensureContinuousStream(dedicatedClient);
8279
+ const queue = [];
8280
+ const MAX_QUEUE = 200;
8281
+ let wake = null;
8282
+ let done = false;
8283
+ const push = (frame) => {
8284
+ queue.push(frame);
8285
+ if (queue.length > MAX_QUEUE) {
8286
+ queue.splice(0, queue.length - MAX_QUEUE);
8287
+ }
8288
+ if (wake) {
8289
+ const w = wake;
8290
+ wake = null;
8291
+ w();
8292
+ }
8293
+ };
8294
+ const onVideo = (au) => {
8295
+ push({
8296
+ audio: false,
8297
+ data: au.data,
8298
+ codec: null,
8299
+ sampleRate: null,
8300
+ microseconds: au.microseconds,
8301
+ videoType: au.videoType,
8302
+ isKeyframe: au.isKeyframe
8303
+ });
8304
+ };
8305
+ const onAudio = (frame) => {
8306
+ push({
8307
+ audio: true,
8308
+ data: frame,
8309
+ codec: "aac",
8310
+ sampleRate: 8e3,
8311
+ microseconds: null
8312
+ });
8313
+ };
8314
+ const finish = () => {
8315
+ done = true;
8316
+ if (wake) {
8317
+ const w = wake;
8318
+ wake = null;
8319
+ w();
8320
+ }
8321
+ };
8322
+ const onAbort = () => finish();
8323
+ cvs.on("videoAccessUnit", onVideo);
8324
+ cvs.on("audioFrame", onAudio);
8325
+ cvs.on("close", finish);
8326
+ if (signal.aborted) {
8327
+ done = true;
8328
+ } else {
8329
+ signal.addEventListener("abort", onAbort);
8330
+ }
8331
+ try {
8332
+ while (!done && !signal.aborted) {
8333
+ if (queue.length > 0) {
8334
+ yield queue.shift();
8335
+ } else {
8336
+ await new Promise((resolve) => {
8337
+ wake = resolve;
8338
+ if (done || signal.aborted) {
8339
+ wake = null;
8340
+ resolve();
8341
+ }
8342
+ });
8343
+ }
8344
+ }
8345
+ while (queue.length > 0 && !signal.aborted) {
8346
+ yield queue.shift();
8347
+ }
8348
+ } finally {
8349
+ cvs.off("videoAccessUnit", onVideo);
8350
+ cvs.off("audioFrame", onAudio);
8351
+ cvs.off("close", finish);
8352
+ signal.removeEventListener("abort", onAbort);
8353
+ }
8354
+ }
8355
+ /**
8356
+ * Lazily build the long-lived {@link ContinuousVideoStream} +
8357
+ * {@link AlwaysOnController} for always-on mode. Both are created once and
8358
+ * reused for the lifetime of the server (across native-stream restarts).
8359
+ */
8360
+ ensureContinuousStream(dedicatedClient) {
8361
+ if (this.continuousStream) return this.continuousStream;
8362
+ const createLiveStream = async () => {
8363
+ const client = dedicatedClient ?? this.api.client;
8364
+ return new BaichuanVideoStream({
8365
+ client,
8366
+ api: this.api,
8367
+ channel: this.channel,
8368
+ profile: this.profile,
8369
+ ...this.variant !== "default" ? { variant: this.variant } : {},
8370
+ ...this.logger ? { logger: this.logger } : {}
8371
+ });
8372
+ };
8373
+ const cvsOptions = {
8374
+ createLiveStream,
8375
+ ...this.alwaysOnOptions?.idleFps !== void 0 ? { idleFps: this.alwaysOnOptions.idleFps } : {},
8376
+ ...this.alwaysOnOptions?.placeholder !== void 0 ? { placeholder: this.alwaysOnOptions.placeholder } : {},
8377
+ ...this.logger ? { logger: this.logger } : {}
8378
+ };
8379
+ const cvs = new ContinuousVideoStream(cvsOptions);
8380
+ cvs.on("error", (e) => {
8381
+ this.logger.warn(
8382
+ `[BaichuanRtspServer] ContinuousVideoStream error: ${e?.message ?? e}`
8383
+ );
8384
+ });
8385
+ this.continuousStream = cvs;
8386
+ this.alwaysOnController = new AlwaysOnController({
8387
+ api: this.api,
8388
+ channel: this.channel,
8389
+ options: this.alwaysOnOptions,
8390
+ goLive: () => cvs.goLive(),
8391
+ goIdle: () => cvs.goIdle(),
8392
+ ...this.logger ? { logger: this.logger } : {}
8393
+ });
8394
+ void this.alwaysOnController.start().catch((e) => {
8395
+ this.logger.warn(
8396
+ `[BaichuanRtspServer] AlwaysOnController start failed: ${e?.message ?? e}`
8397
+ );
8398
+ });
8399
+ return cvs;
8400
+ }
7912
8401
  /**
7913
8402
  * Start native stream (mark as active).
7914
8403
  * Each client will create its own generator, so we just track that the stream is active.
@@ -7970,7 +8459,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7970
8459
  await this.flow.startKeepAlive(this.api);
7971
8460
  this.nativeFanout = new NativeStreamFanout({
7972
8461
  maxQueueItems: 200,
7973
- createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
8462
+ createSource: (signal) => this.alwaysOnOptions?.enabled ? this.createContinuousSource(dedicatedClient, signal) : createNativeStream(this.api, this.channel, this.profile, {
7974
8463
  variant: this.variant,
7975
8464
  ...dedicatedClient ? { client: dedicatedClient } : {},
7976
8465
  signal
@@ -8053,6 +8542,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
8053
8542
  } catch {
8054
8543
  }
8055
8544
  }
8545
+ if (this.tearingDown) return;
8056
8546
  if (this.connectedClients.size > 0 && hadFrames) {
8057
8547
  this.logger.info(
8058
8548
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
@@ -8066,7 +8556,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
8066
8556
  });
8067
8557
  this.nativeFanout.start();
8068
8558
  this.clearNoFrameDeadlineTimer();
8069
- if (this.nativeStreamNoFrameDeadlineMs > 0) {
8559
+ if (this.nativeStreamNoFrameDeadlineMs > 0 && !this.alwaysOnOptions?.enabled) {
8070
8560
  this.noFrameDeadlineTimer = setTimeout(() => {
8071
8561
  this.noFrameDeadlineTimer = void 0;
8072
8562
  if (!this.firstFrameReceived && this.nativeStreamActive) {
@@ -8079,7 +8569,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
8079
8569
  this.noFrameDeadlineTimer?.unref?.();
8080
8570
  }
8081
8571
  this.clearNoClientAutoStopTimer();
8082
- if (this.nativeStreamPrimeIdleStopMs > 0) {
8572
+ if (this.nativeStreamPrimeIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
8083
8573
  this.noClientAutoStopTimer = setTimeout(() => {
8084
8574
  if (this.connectedClients.size === 0) {
8085
8575
  this.rtspDebugLog(
@@ -8166,7 +8656,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
8166
8656
  this.emit("clientDisconnected", clientId);
8167
8657
  if (this.connectedClients.size === 0) {
8168
8658
  this.clearNoClientAutoStopTimer();
8169
- if (this.nativeStreamIdleStopMs > 0) {
8659
+ if (this.nativeStreamIdleStopMs > 0 && !this.alwaysOnOptions?.enabled) {
8170
8660
  this.noClientAutoStopTimer = setTimeout(() => {
8171
8661
  if (this.connectedClients.size === 0) {
8172
8662
  void this.stopNativeStream();
@@ -8237,9 +8727,22 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
8237
8727
  if (!this.active) {
8238
8728
  return;
8239
8729
  }
8730
+ this.tearingDown = true;
8240
8731
  this.logger.info(
8241
8732
  `[BaichuanRtspServer] Stopping RTSP server on ${this.listenHost}:${this.listenPort}...`
8242
8733
  );
8734
+ if (this.alwaysOnController) {
8735
+ const controller = this.alwaysOnController;
8736
+ this.alwaysOnController = null;
8737
+ await controller.stop().catch(() => {
8738
+ });
8739
+ }
8740
+ if (this.continuousStream) {
8741
+ const cvs = this.continuousStream;
8742
+ this.continuousStream = null;
8743
+ await cvs.stop().catch(() => {
8744
+ });
8745
+ }
8243
8746
  await this.stopNativeStream();
8244
8747
  const clientIds = Array.from(this.connectedClients);
8245
8748
  for (const clientId of clientIds) {
@@ -9199,8 +9702,8 @@ function patchMotionSensitivityListXml(currentXml, bands) {
9199
9702
  }
9200
9703
 
9201
9704
  // src/emailPush/bus.ts
9202
- import { EventEmitter as EventEmitter4 } from "events";
9203
- var emitter = new EventEmitter4();
9705
+ import { EventEmitter as EventEmitter5 } from "events";
9706
+ var emitter = new EventEmitter5();
9204
9707
  var cameraResolver = () => void 0;
9205
9708
  var lastEventByCamera = /* @__PURE__ */ new Map();
9206
9709
  var MAX_GLOBAL_EVENTS = 300;
@@ -9253,7 +9756,7 @@ function _resetEmailPushBusForTests() {
9253
9756
  }
9254
9757
 
9255
9758
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
9256
- import { spawn as spawn2 } from "child_process";
9759
+ import { spawn as spawn3 } from "child_process";
9257
9760
  import { mkdir } from "fs/promises";
9258
9761
  import { dirname } from "path";
9259
9762
  import { PassThrough } from "stream";
@@ -9582,7 +10085,7 @@ function buildSetSystemGeneralXml(patch) {
9582
10085
  }
9583
10086
 
9584
10087
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
9585
- import { Jimp, JimpMime } from "jimp";
10088
+ import { Jimp as Jimp2, JimpMime as JimpMime2 } from "jimp";
9586
10089
 
9587
10090
  // src/reolink/baichuan/utils/abilityInfo.ts
9588
10091
  var parseAbilityInfoXml = (xml) => {
@@ -15709,12 +16212,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
15709
16212
  let wideImg;
15710
16213
  let teleImg;
15711
16214
  try {
15712
- wideImg = await Jimp.read(wide);
16215
+ wideImg = await Jimp2.read(wide);
15713
16216
  } catch {
15714
16217
  return wide;
15715
16218
  }
15716
16219
  try {
15717
- teleImg = await Jimp.read(tele);
16220
+ teleImg = await Jimp2.read(tele);
15718
16221
  } catch {
15719
16222
  return wide;
15720
16223
  }
@@ -15748,7 +16251,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
15748
16251
  });
15749
16252
  teleImg.resize({ w: pipW, h: pipH });
15750
16253
  wideImg.composite(teleImg, left, top);
15751
- return await wideImg.getBuffer(JimpMime.jpeg, { quality: 80 });
16254
+ return await wideImg.getBuffer(JimpMime2.jpeg, { quality: 80 });
15752
16255
  }
15753
16256
  const ch = channel !== void 0 ? this.normalizeChannel(channel) : 0;
15754
16257
  const variant = options?.variant ?? "default";
@@ -16706,7 +17209,7 @@ ${xml}`);
16706
17209
  const chunks = [];
16707
17210
  let stderr = "";
16708
17211
  let timedOut = false;
16709
- const ff = spawn2(params.ffmpegPath, [
17212
+ const ff = spawn3(params.ffmpegPath, [
16710
17213
  "-hide_banner",
16711
17214
  "-loglevel",
16712
17215
  "error",
@@ -16791,7 +17294,7 @@ ${xml}`);
16791
17294
  const chunks = [];
16792
17295
  let stderr = "";
16793
17296
  let timedOut = false;
16794
- const ff = spawn2(ffmpegPath, [
17297
+ const ff = spawn3(ffmpegPath, [
16795
17298
  "-hide_banner",
16796
17299
  "-loglevel",
16797
17300
  "error",
@@ -16907,7 +17410,7 @@ ${xml}`);
16907
17410
  ensureEnabled: true
16908
17411
  });
16909
17412
  await new Promise((resolve, reject) => {
16910
- const ff = spawn2(ffmpegPath, [
17413
+ const ff = spawn3(ffmpegPath, [
16911
17414
  "-hide_banner",
16912
17415
  "-loglevel",
16913
17416
  "error",
@@ -16963,7 +17466,7 @@ ${stderr}`));
16963
17466
  const atSeconds = Number.isFinite(params.atSeconds) && params.atSeconds >= 0 ? params.atSeconds : 0;
16964
17467
  await mkdir(dirname(params.outputPath), { recursive: true });
16965
17468
  await new Promise((resolve, reject) => {
16966
- const ff = spawn2(ffmpegPath, [
17469
+ const ff = spawn3(ffmpegPath, [
16967
17470
  "-hide_banner",
16968
17471
  "-loglevel",
16969
17472
  "error",
@@ -17528,7 +18031,7 @@ ${stderr}`)
17528
18031
  * Convert a raw video keyframe to JPEG using ffmpeg.
17529
18032
  */
17530
18033
  async convertFrameToJpeg(params) {
17531
- const { spawn: spawn3 } = await import("child_process");
18034
+ const { spawn: spawn4 } = await import("child_process");
17532
18035
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
17533
18036
  const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
17534
18037
  return new Promise((resolve, reject) => {
@@ -17550,7 +18053,7 @@ ${stderr}`)
17550
18053
  "2",
17551
18054
  "pipe:1"
17552
18055
  ];
17553
- const proc = spawn3(ffmpeg, args, {
18056
+ const proc = spawn4(ffmpeg, args, {
17554
18057
  stdio: ["pipe", "pipe", "pipe"]
17555
18058
  });
17556
18059
  const chunks = [];
@@ -17693,7 +18196,7 @@ ${stderr}`)
17693
18196
  * Internal helper to mux video+audio into MP4 using ffmpeg.
17694
18197
  */
17695
18198
  async muxToMp4(params) {
17696
- const { spawn: spawn3 } = await import("child_process");
18199
+ const { spawn: spawn4 } = await import("child_process");
17697
18200
  const { randomUUID: randomUUID3 } = await import("crypto");
17698
18201
  const fs = await import("fs/promises");
17699
18202
  const os = await import("os");
@@ -17745,7 +18248,7 @@ ${stderr}`)
17745
18248
  outputPath
17746
18249
  );
17747
18250
  await new Promise((resolve, reject) => {
17748
- const p = spawn3(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
18251
+ const p = spawn4(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
17749
18252
  let stderr = "";
17750
18253
  p.stderr.on("data", (d) => {
17751
18254
  stderr += d.toString();
@@ -22732,7 +23235,7 @@ ${scheduleItems}
22732
23235
  "mjpeg",
22733
23236
  "pipe:1"
22734
23237
  ];
22735
- const ff = spawn2("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
23238
+ const ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
22736
23239
  const chunks = [];
22737
23240
  let stderr = "";
22738
23241
  ff.stdout.on("data", (d) => chunks.push(Buffer.from(d)));
@@ -22856,7 +23359,7 @@ ${scheduleItems}
22856
23359
  "pipe:1"
22857
23360
  ];
22858
23361
  }
22859
- ff = spawn2("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
23362
+ ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
22860
23363
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
22861
23364
  throw new Error("ffmpeg stdio streams not available");
22862
23365
  }
@@ -23103,7 +23606,7 @@ ${scheduleItems}
23103
23606
  "mp4",
23104
23607
  "pipe:1"
23105
23608
  ];
23106
- ff = spawn2("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
23609
+ ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
23107
23610
  if (!ff.stdin || !ff.stdout || !ff.stderr) {
23108
23611
  throw new Error("ffmpeg stdio streams not available");
23109
23612
  }
@@ -23312,7 +23815,7 @@ ${scheduleItems}
23312
23815
  "independent_segments+temp_file",
23313
23816
  playlistPath
23314
23817
  ];
23315
- ff = spawn2("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
23818
+ ff = spawn3("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
23316
23819
  if (!ff.stdin || !ff.stderr) {
23317
23820
  throw new Error("ffmpeg stdio streams not available");
23318
23821
  }
@@ -24784,13 +25287,13 @@ async function pingHost(host, timeoutMs = 3e3) {
24784
25287
  }
24785
25288
  return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
24786
25289
  };
24787
- const { spawn: spawn3 } = await import("child_process");
25290
+ const { spawn: spawn4 } = await import("child_process");
24788
25291
  for (const bin of pingCandidates) {
24789
25292
  const ranOk = await new Promise((resolve) => {
24790
25293
  let settled = false;
24791
25294
  let child;
24792
25295
  try {
24793
- child = spawn3(bin, pingArgs(bin), { stdio: "ignore" });
25296
+ child = spawn4(bin, pingArgs(bin), { stdio: "ignore" });
24794
25297
  } catch {
24795
25298
  resolve("spawn-failed");
24796
25299
  return;
@@ -25444,6 +25947,10 @@ export {
25444
25947
  Intercom,
25445
25948
  BaichuanEventEmitter,
25446
25949
  createNativeStream,
25950
+ ALWAYS_ON_DEFAULTS,
25951
+ PlaceholderRenderer,
25952
+ ContinuousVideoStream,
25953
+ AlwaysOnController,
25447
25954
  BaichuanRtspServer,
25448
25955
  MpegTsMuxer,
25449
25956
  flattenAbilitiesForChannel,
@@ -25497,4 +26004,4 @@ export {
25497
26004
  tcpReachabilityProbe,
25498
26005
  autoDetectDeviceType
25499
26006
  };
25500
- //# sourceMappingURL=chunk-UL34MR4L.js.map
26007
+ //# sourceMappingURL=chunk-WQ2TQCYP.js.map