@apocaliss92/nodelink-js 0.4.7 → 0.4.10

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.
@@ -31,6 +31,7 @@ import {
31
31
  BC_CMD_ID_GET_AUDIO_ALARM,
32
32
  BC_CMD_ID_GET_AUDIO_CFG,
33
33
  BC_CMD_ID_GET_AUDIO_TASK,
34
+ BC_CMD_ID_GET_AUTO_FOCUS,
34
35
  BC_CMD_ID_GET_BATTERY_INFO,
35
36
  BC_CMD_ID_GET_BATTERY_INFO_LIST,
36
37
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
@@ -39,6 +40,7 @@ import {
39
40
  BC_CMD_ID_GET_DING_DONG_LIST,
40
41
  BC_CMD_ID_GET_DING_DONG_SILENT,
41
42
  BC_CMD_ID_GET_EMAIL_TASK,
43
+ BC_CMD_ID_GET_ENC,
42
44
  BC_CMD_ID_GET_FTP_TASK,
43
45
  BC_CMD_ID_GET_HDD_INFO_LIST,
44
46
  BC_CMD_ID_GET_KIT_AP_CFG,
@@ -47,6 +49,7 @@ import {
47
49
  BC_CMD_ID_GET_ONLINE_USER_LIST,
48
50
  BC_CMD_ID_GET_OSD_DATETIME,
49
51
  BC_CMD_ID_GET_PIR_INFO,
52
+ BC_CMD_ID_GET_PRIVACY_MASK,
50
53
  BC_CMD_ID_GET_PTZ_POSITION,
51
54
  BC_CMD_ID_GET_PTZ_PRESET,
52
55
  BC_CMD_ID_GET_RECORD,
@@ -76,11 +79,19 @@ import {
76
79
  BC_CMD_ID_QUICK_REPLY_PLAY,
77
80
  BC_CMD_ID_SET_AI_ALARM,
78
81
  BC_CMD_ID_SET_AI_CFG,
82
+ BC_CMD_ID_SET_AI_DENOISE,
83
+ BC_CMD_ID_SET_AUDIO_CFG,
79
84
  BC_CMD_ID_SET_AUDIO_TASK,
85
+ BC_CMD_ID_SET_AUTO_FOCUS,
86
+ BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
80
87
  BC_CMD_ID_SET_DING_DONG_CFG,
81
88
  BC_CMD_ID_SET_DING_DONG_SILENT,
89
+ BC_CMD_ID_SET_ENC,
90
+ BC_CMD_ID_SET_LED_STATE,
82
91
  BC_CMD_ID_SET_MOTION_ALARM,
83
92
  BC_CMD_ID_SET_PIR_INFO,
93
+ BC_CMD_ID_SET_PRIVACY_MASK,
94
+ BC_CMD_ID_SET_VIDEO_INPUT,
84
95
  BC_CMD_ID_SET_WHITE_LED_STATE,
85
96
  BC_CMD_ID_SET_WHITE_LED_TASK,
86
97
  BC_CMD_ID_SET_ZOOM_FOCUS,
@@ -102,6 +113,8 @@ import {
102
113
  __require,
103
114
  aesDecrypt,
104
115
  aesEncrypt,
116
+ applyStreamPatch,
117
+ applyXmlTagPatch,
105
118
  bcDecrypt,
106
119
  bcEncrypt,
107
120
  bcHeaderHasPayloadOffset,
@@ -127,6 +140,7 @@ import {
127
140
  convertToAnnexB2,
128
141
  debugLog,
129
142
  deriveAesKey,
143
+ ensureXmlHeader,
130
144
  eventTraceLog,
131
145
  extractPpsFromAnnexB,
132
146
  extractSpsFromAnnexB,
@@ -134,8 +148,11 @@ import {
134
148
  getXmlText,
135
149
  isH265Irap,
136
150
  md5StrModern,
151
+ normalizeDayNightMode,
137
152
  normalizeDebugOptions,
153
+ normalizeOpenClose,
138
154
  parseRecordingFileName,
155
+ patchNestedTag,
139
156
  recordingsTraceLog,
140
157
  runAllDiagnosticsConsecutively,
141
158
  runMultifocalDiagnosticsConsecutively,
@@ -144,7 +161,7 @@ import {
144
161
  talkTraceLog,
145
162
  traceLog,
146
163
  xmlEscape
147
- } from "./chunk-TR3V5FTO.js";
164
+ } from "./chunk-EDLMKBG2.js";
148
165
 
149
166
  // src/protocol/framing.ts
150
167
  function encodeHeader(h) {
@@ -5325,19 +5342,34 @@ async function* createNativeStream(api, channel, profile, options) {
5325
5342
  }
5326
5343
  });
5327
5344
  streamStarted = true;
5328
- while (!closed) {
5345
+ const signal = options?.signal;
5346
+ while (!closed && !signal?.aborted) {
5329
5347
  if (frameQueue.length > 0) {
5330
5348
  const frame = frameQueue.shift();
5331
5349
  yield frame;
5332
5350
  } else {
5333
5351
  await new Promise((resolve) => {
5334
5352
  frameResolve = resolve;
5335
- setTimeout(() => {
5353
+ const timer = setTimeout(() => {
5336
5354
  if (frameResolve === resolve) {
5337
5355
  frameResolve = null;
5338
5356
  resolve();
5339
5357
  }
5340
5358
  }, 1e3);
5359
+ if (signal) {
5360
+ const onAbort = () => {
5361
+ clearTimeout(timer);
5362
+ if (frameResolve === resolve) frameResolve = null;
5363
+ resolve();
5364
+ };
5365
+ if (signal.aborted) {
5366
+ clearTimeout(timer);
5367
+ frameResolve = null;
5368
+ resolve();
5369
+ } else {
5370
+ signal.addEventListener("abort", onAbort, { once: true });
5371
+ }
5372
+ }
5341
5373
  });
5342
5374
  }
5343
5375
  }
@@ -5513,13 +5545,14 @@ var NativeStreamFanout = class {
5513
5545
  source = null;
5514
5546
  running = false;
5515
5547
  pumpPromise = null;
5548
+ abort = new AbortController();
5516
5549
  constructor(opts) {
5517
5550
  this.opts = opts;
5518
5551
  }
5519
5552
  start() {
5520
5553
  if (this.running) return;
5521
5554
  this.running = true;
5522
- this.source = this.opts.createSource();
5555
+ this.source = this.opts.createSource(this.abort.signal);
5523
5556
  this.pumpPromise = (async () => {
5524
5557
  try {
5525
5558
  for await (const frame of this.source) {
@@ -5565,6 +5598,7 @@ var NativeStreamFanout = class {
5565
5598
  this.source = null;
5566
5599
  for (const q of this.queues.values()) q.close();
5567
5600
  this.queues.clear();
5601
+ this.abort.abort();
5568
5602
  try {
5569
5603
  await src?.return(void 0);
5570
5604
  } catch {
@@ -5603,9 +5637,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5603
5637
  requireAuth;
5604
5638
  authNonces = /* @__PURE__ */ new Map();
5605
5639
  // Track nonces per client
5606
- AUTH_REALM = "BaichuanRtspServer";
5640
+ AUTH_REALM;
5607
5641
  NONCE_TIMEOUT_MS = 3e5;
5608
5642
  // 5 minutes
5643
+ lazyMetadata;
5609
5644
  // Client tracking
5610
5645
  connectedClients = /* @__PURE__ */ new Set();
5611
5646
  // Set of client IDs (IP:port)
@@ -5617,8 +5652,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5617
5652
  // Track all client resources for cleanup
5618
5653
  clientResources = /* @__PURE__ */ new Map();
5619
5654
  isRtspDebugEnabled() {
5620
- const dbg = this.api.client.getDebugConfig();
5621
- return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5655
+ try {
5656
+ if (this.api.isClosed) {
5657
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5658
+ }
5659
+ const dbg = this.api.client.getDebugConfig();
5660
+ return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5661
+ } catch {
5662
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5663
+ }
5622
5664
  }
5623
5665
  rtspDebugLog(message) {
5624
5666
  if (!this.isRtspDebugEnabled()) return;
@@ -5640,10 +5682,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5640
5682
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
5641
5683
  nativeFanout = null;
5642
5684
  noClientAutoStopTimer;
5685
+ /** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
5686
+ noFrameDeadlineTimer;
5643
5687
  /** After last RTSP client; 0 = never auto-stop native stream. */
5644
5688
  nativeStreamIdleStopMs;
5645
5689
  /** Primed-but-no-PLAY timeout; 0 = disabled. */
5646
5690
  nativeStreamPrimeIdleStopMs;
5691
+ /**
5692
+ * Max time to wait for the first camera frame after stream start.
5693
+ * If no frames arrive within this window, the native stream is stopped
5694
+ * (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
5695
+ * firing and waking the camera when no real viewer is watching.
5696
+ * 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
5697
+ */
5698
+ nativeStreamNoFrameDeadlineMs;
5647
5699
  // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
5648
5700
  // When a new client connects while the stream is already running it does not need
5649
5701
  // to wait up to one full GOP interval for the next keyframe — we replay frames
@@ -5769,14 +5821,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5769
5821
  this.logger = options.logger ?? console;
5770
5822
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
5771
5823
  this.deviceId = options.deviceId;
5772
- this.externalListener = options.externalListener ?? false;
5824
+ this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
5773
5825
  this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
5774
5826
  this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
5775
- this.authCredentials = options.credentials ?? [];
5827
+ this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
5828
+ this.authCredentials = (options.credentials ?? []).map((c) => ({
5829
+ username: c.username,
5830
+ ...c.password !== void 0 ? { password: c.password } : {},
5831
+ ...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
5832
+ }));
5776
5833
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
5834
+ this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
5835
+ this.lazyMetadata = options.lazyMetadata ?? false;
5777
5836
  const transport = this.api.client.getTransport();
5778
5837
  this.flow = createRtspFlow(transport, "H264");
5779
5838
  }
5839
+ /** Number of currently connected RTSP clients. */
5840
+ get clientCount() {
5841
+ return this.connectedClients.size;
5842
+ }
5780
5843
  // --- Authentication helpers ---
5781
5844
  /**
5782
5845
  * Generate a new nonce for Digest authentication
@@ -5837,9 +5900,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5837
5900
  this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
5838
5901
  return false;
5839
5902
  }
5903
+ if (realm !== this.AUTH_REALM) {
5904
+ this.rtspDebugLog(
5905
+ `Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
5906
+ );
5907
+ return false;
5908
+ }
5840
5909
  for (const cred of this.authCredentials) {
5841
5910
  if (username !== cred.username) continue;
5842
- const ha1 = this.md5(`${cred.username}:${realm}:${cred.password}`);
5911
+ const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
5912
+ if (!ha1) continue;
5843
5913
  const ha2 = this.md5(`${method}:${authUri || uri}`);
5844
5914
  const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
5845
5915
  if (response === expectedResponse) {
@@ -5868,6 +5938,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5868
5938
  this.noClientAutoStopTimer = void 0;
5869
5939
  }
5870
5940
  }
5941
+ clearNoFrameDeadlineTimer() {
5942
+ if (this.noFrameDeadlineTimer) {
5943
+ clearTimeout(this.noFrameDeadlineTimer);
5944
+ this.noFrameDeadlineTimer = void 0;
5945
+ }
5946
+ }
5871
5947
  setFlowVideoType(videoType, reason) {
5872
5948
  if (this.flow.videoType === videoType) return;
5873
5949
  const transport = this.api.client.getTransport();
@@ -5882,25 +5958,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5882
5958
  if (this.active) {
5883
5959
  throw new Error("RTSP server is already active");
5884
5960
  }
5885
- try {
5886
- const metadata = await this.api.getStreamMetadata(this.channel);
5887
- const stream = metadata.streams.find((s) => s.profile === this.profile);
5888
- if (stream) {
5889
- this.streamMetadata = {
5890
- frameRate: stream.frameRate || 25,
5891
- width: stream.width,
5892
- height: stream.height
5893
- };
5894
- const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
5895
- const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
5896
- this.setFlowVideoType(metaVideoType, "metadata");
5897
- }
5898
- } catch (error) {
5899
- this.logger.warn(
5900
- `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5961
+ if (this.lazyMetadata) {
5962
+ this.logger.info(
5963
+ `[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
5901
5964
  );
5902
- this.streamMetadata = { frameRate: 25 };
5903
- this.setFlowVideoType("H264", "metadata unavailable");
5965
+ } else {
5966
+ try {
5967
+ const metadata = await this.api.getStreamMetadata(this.channel);
5968
+ const stream = metadata.streams.find((s) => s.profile === this.profile);
5969
+ if (stream) {
5970
+ this.streamMetadata = {
5971
+ frameRate: stream.frameRate || 25,
5972
+ width: stream.width,
5973
+ height: stream.height
5974
+ };
5975
+ const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
5976
+ const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
5977
+ this.setFlowVideoType(metaVideoType, "metadata");
5978
+ }
5979
+ } catch (error) {
5980
+ this.logger.warn(
5981
+ `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5982
+ );
5983
+ this.streamMetadata = { frameRate: 25 };
5984
+ this.setFlowVideoType("H264", "metadata unavailable");
5985
+ }
5904
5986
  }
5905
5987
  if (!this.externalListener) {
5906
5988
  this.clientConnectionServer = net2.createServer((socket) => {
@@ -5941,6 +6023,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5941
6023
  }
5942
6024
  this.handleRtspConnection(socket, initialBuffer);
5943
6025
  }
6026
+ /**
6027
+ * Inject an already-accepted client socket from a multiplexer
6028
+ * (e.g. `LocalRtspMux`) that owns the listening port.
6029
+ *
6030
+ * The mux reads the first RTSP request line to determine the target path,
6031
+ * then hands the socket over. Any bytes already consumed during routing
6032
+ * are replayed back onto the socket via `unshift()` so the RTSP parser in
6033
+ * `handleRtspConnection` sees the complete original request.
6034
+ *
6035
+ * @param socket - Client TCP socket, already accepted by the mux.
6036
+ * @param preReadData - Bytes the mux has already pulled off the socket
6037
+ * while parsing the request line. Replayed via `socket.unshift()`
6038
+ * before any further reads.
6039
+ */
6040
+ injectSocket(socket, preReadData) {
6041
+ if (!this.active) {
6042
+ socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
6043
+ return;
6044
+ }
6045
+ if (preReadData && preReadData.length > 0) {
6046
+ socket.unshift(preReadData);
6047
+ }
6048
+ this.handleRtspConnection(socket);
6049
+ }
5944
6050
  /**
5945
6051
  * Handle RTSP connection from a client.
5946
6052
  */
@@ -6105,6 +6211,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6105
6211
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
6106
6212
  });
6107
6213
  } else if (method === "DESCRIBE") {
6214
+ if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
6215
+ void this.api.ensureConnected().catch(() => {
6216
+ });
6217
+ }
6108
6218
  if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
6109
6219
  try {
6110
6220
  if (!this.nativeStreamActive) {
@@ -6142,6 +6252,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6142
6252
  }
6143
6253
  }
6144
6254
  }
6255
+ if (!this.hasAudio && this.firstAudioPromise) {
6256
+ const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
6257
+ const audioPrimingStart = Date.now();
6258
+ try {
6259
+ await Promise.race([
6260
+ this.firstAudioPromise,
6261
+ new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
6262
+ ]);
6263
+ } catch {
6264
+ }
6265
+ const audioPrimingElapsed = Date.now() - audioPrimingStart;
6266
+ if (this.hasAudio) {
6267
+ this.logger.info(
6268
+ `[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
6269
+ );
6270
+ } else {
6271
+ this.logger.info(
6272
+ `[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
6273
+ );
6274
+ }
6275
+ }
6145
6276
  {
6146
6277
  const { fmtp, hasParamSets } = this.flow.getFmtp();
6147
6278
  const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
@@ -6150,12 +6281,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6150
6281
  );
6151
6282
  }
6152
6283
  const sdp = this.generateSdp();
6284
+ const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
6153
6285
  sendResponse(
6154
6286
  200,
6155
6287
  "OK",
6156
6288
  {
6157
6289
  "Content-Type": "application/sdp",
6158
- "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
6290
+ "Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
6159
6291
  },
6160
6292
  sdp
6161
6293
  );
@@ -6178,7 +6310,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6178
6310
  this.emit("client", clientId);
6179
6311
  this.clearNoClientAutoStopTimer();
6180
6312
  if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
6181
- await this.startNativeStream();
6313
+ void this.startNativeStream();
6182
6314
  }
6183
6315
  const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
6184
6316
  const transport = (transportMatch?.[1] ?? "").trim();
@@ -6312,12 +6444,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6312
6444
  }
6313
6445
  }
6314
6446
  };
6447
+ const runProcessBuffer = () => {
6448
+ processBuffer().catch((err) => {
6449
+ this.logger.debug(
6450
+ `[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
6451
+ );
6452
+ try {
6453
+ socket.destroy();
6454
+ } catch {
6455
+ }
6456
+ });
6457
+ };
6315
6458
  socket.on("data", (data) => {
6316
6459
  buffer = Buffer.concat([buffer, data]);
6317
- void processBuffer();
6460
+ runProcessBuffer();
6318
6461
  });
6319
6462
  if (buffer.includes("\r\n\r\n")) {
6320
- void processBuffer();
6463
+ runProcessBuffer();
6321
6464
  }
6322
6465
  }
6323
6466
  /**
@@ -7185,6 +7328,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7185
7328
  if (this.nativeStreamActive) {
7186
7329
  return;
7187
7330
  }
7331
+ if (!this.api.isReady) {
7332
+ if (this.api.isClosed) {
7333
+ this.logger.warn?.(
7334
+ `[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
7335
+ );
7336
+ return;
7337
+ }
7338
+ try {
7339
+ this.logger.info?.(
7340
+ `[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
7341
+ );
7342
+ await this.api.ensureConnected();
7343
+ } catch (e) {
7344
+ this.logger.warn?.(
7345
+ `[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
7346
+ );
7347
+ return;
7348
+ }
7349
+ }
7188
7350
  this.nativeStreamActive = true;
7189
7351
  this.firstFrameReceived = false;
7190
7352
  this.firstAudioDetected = false;
@@ -7219,13 +7381,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7219
7381
  await this.flow.startKeepAlive(this.api);
7220
7382
  this.nativeFanout = new NativeStreamFanout({
7221
7383
  maxQueueItems: 200,
7222
- createSource: () => createNativeStream(this.api, this.channel, this.profile, {
7384
+ createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
7223
7385
  variant: this.variant,
7224
- ...dedicatedClient ? { client: dedicatedClient } : {}
7386
+ ...dedicatedClient ? { client: dedicatedClient } : {},
7387
+ signal
7225
7388
  }),
7226
7389
  onFrame: (frame) => {
7227
7390
  if (frame.audio) {
7228
- if (!this.hasAudio && this.api.client.getTransport() === "tcp" && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
7391
+ if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
7229
7392
  const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
7230
7393
  if (info) {
7231
7394
  this.hasAudio = true;
@@ -7274,6 +7437,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7274
7437
  onEnd: () => {
7275
7438
  if (!this.nativeStreamActive) return;
7276
7439
  this.nativeStreamActive = false;
7440
+ this.clearNoFrameDeadlineTimer();
7441
+ const hadFrames = this.firstFrameReceived;
7277
7442
  this.firstFrameReceived = false;
7278
7443
  this.firstFramePromise = null;
7279
7444
  this.firstFrameResolve = null;
@@ -7298,7 +7463,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7298
7463
  } catch {
7299
7464
  }
7300
7465
  }
7301
- if (this.connectedClients.size > 0) {
7466
+ if (this.connectedClients.size > 0 && hadFrames) {
7302
7467
  this.logger.info(
7303
7468
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
7304
7469
  );
@@ -7310,6 +7475,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7310
7475
  }
7311
7476
  });
7312
7477
  this.nativeFanout.start();
7478
+ this.clearNoFrameDeadlineTimer();
7479
+ if (this.nativeStreamNoFrameDeadlineMs > 0) {
7480
+ this.noFrameDeadlineTimer = setTimeout(() => {
7481
+ this.noFrameDeadlineTimer = void 0;
7482
+ if (!this.firstFrameReceived && this.nativeStreamActive) {
7483
+ this.logger.info(
7484
+ `[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
7485
+ );
7486
+ void this.stopNativeStream();
7487
+ }
7488
+ }, this.nativeStreamNoFrameDeadlineMs);
7489
+ this.noFrameDeadlineTimer?.unref?.();
7490
+ }
7313
7491
  this.clearNoClientAutoStopTimer();
7314
7492
  if (this.nativeStreamPrimeIdleStopMs > 0) {
7315
7493
  this.noClientAutoStopTimer = setTimeout(() => {
@@ -7326,6 +7504,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7326
7504
  markFirstFrameReceived() {
7327
7505
  if (!this.firstFrameReceived && this.firstFrameResolve) {
7328
7506
  this.firstFrameReceived = true;
7507
+ this.clearNoFrameDeadlineTimer();
7329
7508
  this.rtspDebugLog(
7330
7509
  `First frame received from camera for profile ${this.profile}`
7331
7510
  );
@@ -7352,6 +7531,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7352
7531
  );
7353
7532
  this.flow.stopKeepAlive();
7354
7533
  this.clearNoClientAutoStopTimer();
7534
+ this.clearNoFrameDeadlineTimer();
7355
7535
  this.nativeStreamActive = false;
7356
7536
  this.firstFrameReceived = false;
7357
7537
  this.firstFramePromise = null;
@@ -7561,6 +7741,249 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7561
7741
  }
7562
7742
  };
7563
7743
 
7744
+ // src/baichuan/stream/MpegTsMuxer.ts
7745
+ var TS_PACKET_SIZE = 188;
7746
+ var TS_SYNC_BYTE = 71;
7747
+ var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
7748
+ var PID_PAT = 0;
7749
+ var PID_PMT = 4096;
7750
+ var PID_VIDEO = 256;
7751
+ var PID_AUDIO = 257;
7752
+ var STREAM_TYPE_H264 = 27;
7753
+ var STREAM_TYPE_H265 = 36;
7754
+ var STREAM_TYPE_AAC = 15;
7755
+ var PES_STREAM_ID_VIDEO = 224;
7756
+ var PES_STREAM_ID_AUDIO = 192;
7757
+ var PAT_PMT_INTERVAL = 40;
7758
+ function crc32Mpeg(data) {
7759
+ let crc = 4294967295;
7760
+ for (let i = 0; i < data.length; i++) {
7761
+ crc ^= data[i] << 24;
7762
+ for (let j = 0; j < 8; j++) {
7763
+ if (crc & 2147483648) {
7764
+ crc = (crc << 1 ^ 79764919) >>> 0;
7765
+ } else {
7766
+ crc = crc << 1 >>> 0;
7767
+ }
7768
+ }
7769
+ }
7770
+ return crc >>> 0;
7771
+ }
7772
+ function usToPts(us) {
7773
+ return Math.floor(us * 90 / 1e3) & 8589934591;
7774
+ }
7775
+ function encodePts(buf, offset, pts, prefix) {
7776
+ buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
7777
+ buf[offset + 1] = pts >>> 22 & 255;
7778
+ buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
7779
+ buf[offset + 3] = pts >>> 7 & 255;
7780
+ buf[offset + 4] = (pts & 127) << 1 | 1;
7781
+ }
7782
+ function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
7783
+ buf[0] = TS_SYNC_BYTE;
7784
+ buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
7785
+ buf[2] = pid & 255;
7786
+ buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
7787
+ }
7788
+ function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
7789
+ const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
7790
+ const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
7791
+ let pesOffset = 0;
7792
+ let outOffset = 0;
7793
+ let isFirst = true;
7794
+ while (pesOffset < pesData.length) {
7795
+ const remaining = pesData.length - pesOffset;
7796
+ const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
7797
+ outOffset += TS_PACKET_SIZE;
7798
+ if (remaining >= TS_PAYLOAD_SIZE) {
7799
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
7800
+ ccRef.cc = ccRef.cc + 1 & 15;
7801
+ pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
7802
+ pesOffset += TS_PAYLOAD_SIZE;
7803
+ } else {
7804
+ const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
7805
+ if (paddingNeeded === 1) {
7806
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
7807
+ ccRef.cc = ccRef.cc + 1 & 15;
7808
+ packet[4] = 0;
7809
+ pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
7810
+ } else {
7811
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
7812
+ ccRef.cc = ccRef.cc + 1 & 15;
7813
+ const adaptLen = paddingNeeded - 1;
7814
+ packet[4] = adaptLen;
7815
+ packet[5] = isFirst && isKeyframe ? 64 : 0;
7816
+ packet.fill(255, 6, 4 + paddingNeeded);
7817
+ pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
7818
+ }
7819
+ pesOffset += remaining;
7820
+ }
7821
+ isFirst = false;
7822
+ }
7823
+ return out;
7824
+ }
7825
+ function buildPat(cc) {
7826
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
7827
+ pkt[0] = TS_SYNC_BYTE;
7828
+ pkt[1] = 64 | PID_PAT >> 8 & 31;
7829
+ pkt[2] = PID_PAT & 255;
7830
+ pkt[3] = 16 | cc & 15;
7831
+ pkt[4] = 0;
7832
+ const sectionStart = 5;
7833
+ let i = sectionStart;
7834
+ pkt[i++] = 0;
7835
+ pkt[i++] = 176;
7836
+ pkt[i++] = 13;
7837
+ pkt[i++] = 0;
7838
+ pkt[i++] = 1;
7839
+ pkt[i++] = 193;
7840
+ pkt[i++] = 0;
7841
+ pkt[i++] = 0;
7842
+ pkt[i++] = 0;
7843
+ pkt[i++] = 1;
7844
+ pkt[i++] = 224 | PID_PMT >> 8 & 31;
7845
+ pkt[i++] = PID_PMT & 255;
7846
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
7847
+ pkt.writeUInt32BE(crc, i);
7848
+ return pkt;
7849
+ }
7850
+ function buildPmt(videoStreamType, includeAudio, cc) {
7851
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
7852
+ pkt[0] = TS_SYNC_BYTE;
7853
+ pkt[1] = 64 | PID_PMT >> 8 & 31;
7854
+ pkt[2] = PID_PMT & 255;
7855
+ pkt[3] = 16 | cc & 15;
7856
+ pkt[4] = 0;
7857
+ const sectionStart = 5;
7858
+ let i = sectionStart;
7859
+ pkt[i++] = 2;
7860
+ pkt[i++] = 176;
7861
+ const sectionLenPos = i;
7862
+ i += 1;
7863
+ pkt[i++] = 0;
7864
+ pkt[i++] = 1;
7865
+ pkt[i++] = 193;
7866
+ pkt[i++] = 0;
7867
+ pkt[i++] = 0;
7868
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
7869
+ pkt[i++] = PID_VIDEO & 255;
7870
+ pkt[i++] = 240;
7871
+ pkt[i++] = 0;
7872
+ pkt[i++] = videoStreamType;
7873
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
7874
+ pkt[i++] = PID_VIDEO & 255;
7875
+ pkt[i++] = 240;
7876
+ pkt[i++] = 0;
7877
+ if (includeAudio) {
7878
+ pkt[i++] = STREAM_TYPE_AAC;
7879
+ pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
7880
+ pkt[i++] = PID_AUDIO & 255;
7881
+ pkt[i++] = 240;
7882
+ pkt[i++] = 0;
7883
+ }
7884
+ const sectionLen = i - sectionStart - 3 + 4;
7885
+ pkt[sectionLenPos] = sectionLen;
7886
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
7887
+ pkt.writeUInt32BE(crc, i);
7888
+ return pkt;
7889
+ }
7890
+ function buildVideoPes(annexBData, ptsUs, isKeyframe) {
7891
+ const pts = usToPts(ptsUs);
7892
+ const pesHeader = Buffer.allocUnsafe(14);
7893
+ pesHeader[0] = 0;
7894
+ pesHeader[1] = 0;
7895
+ pesHeader[2] = 1;
7896
+ pesHeader[3] = PES_STREAM_ID_VIDEO;
7897
+ pesHeader[4] = 0;
7898
+ pesHeader[5] = 0;
7899
+ pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
7900
+ pesHeader[7] = 128;
7901
+ pesHeader[8] = 5;
7902
+ encodePts(pesHeader, 9, pts, 2);
7903
+ return Buffer.concat([pesHeader, annexBData]);
7904
+ }
7905
+ function buildAudioPes(adtsData, ptsUs) {
7906
+ const pts = usToPts(ptsUs);
7907
+ const pesPayloadLen = 8 + adtsData.length;
7908
+ const pesHeader = Buffer.allocUnsafe(14);
7909
+ pesHeader[0] = 0;
7910
+ pesHeader[1] = 0;
7911
+ pesHeader[2] = 1;
7912
+ pesHeader[3] = PES_STREAM_ID_AUDIO;
7913
+ pesHeader[4] = pesPayloadLen >> 8 & 255;
7914
+ pesHeader[5] = pesPayloadLen & 255;
7915
+ pesHeader[6] = 128;
7916
+ pesHeader[7] = 128;
7917
+ pesHeader[8] = 5;
7918
+ encodePts(pesHeader, 9, pts, 2);
7919
+ return Buffer.concat([pesHeader, adtsData]);
7920
+ }
7921
+ var MpegTsMuxer = class {
7922
+ videoStreamType;
7923
+ includeAudio;
7924
+ // Per-instance continuity counters (4-bit, wrap at 16)
7925
+ patCc = 0;
7926
+ pmtCc = 0;
7927
+ videoCc = 0;
7928
+ audioCc = 0;
7929
+ framesSinceTableSend = 0;
7930
+ tablesSent = false;
7931
+ constructor(options) {
7932
+ this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
7933
+ this.includeAudio = options.includeAudio ?? true;
7934
+ }
7935
+ /**
7936
+ * Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
7937
+ * PAT and PMT are emitted before keyframes and periodically.
7938
+ *
7939
+ * @param annexBData - Annex-B video data (with start codes)
7940
+ * @param ptsUs - Presentation timestamp in microseconds
7941
+ * @param isKeyframe - Whether this is an IDR / IRAP frame
7942
+ */
7943
+ muxVideo(annexBData, ptsUs, isKeyframe) {
7944
+ const chunks = [];
7945
+ const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
7946
+ if (needTables) {
7947
+ chunks.push(buildPat(this.patCc));
7948
+ this.patCc = this.patCc + 1 & 15;
7949
+ chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
7950
+ this.pmtCc = this.pmtCc + 1 & 15;
7951
+ this.tablesSent = true;
7952
+ this.framesSinceTableSend = 0;
7953
+ }
7954
+ this.framesSinceTableSend++;
7955
+ const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
7956
+ const ccRef = { cc: this.videoCc };
7957
+ chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
7958
+ this.videoCc = ccRef.cc;
7959
+ return Buffer.concat(chunks);
7960
+ }
7961
+ /**
7962
+ * Mux an audio frame (ADTS AAC) into MPEG-TS packets.
7963
+ * Returns an empty Buffer when includeAudio is false.
7964
+ *
7965
+ * @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
7966
+ * @param ptsUs - Presentation timestamp in microseconds
7967
+ */
7968
+ muxAudio(adtsData, ptsUs) {
7969
+ if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
7970
+ const pes = buildAudioPes(adtsData, ptsUs);
7971
+ const ccRef = { cc: this.audioCc };
7972
+ const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
7973
+ this.audioCc = ccRef.cc;
7974
+ return result;
7975
+ }
7976
+ /** Reset all continuity counters and table state (e.g. after stream restart). */
7977
+ reset() {
7978
+ this.patCc = 0;
7979
+ this.pmtCc = 0;
7980
+ this.videoCc = 0;
7981
+ this.audioCc = 0;
7982
+ this.framesSinceTableSend = 0;
7983
+ this.tablesSent = false;
7984
+ }
7985
+ };
7986
+
7564
7987
  // src/reolink/baichuan/capabilities.ts
7565
7988
  function toNumberOrUndefined(value) {
7566
7989
  if (value == null) return void 0;
@@ -7774,214 +8197,59 @@ function xmlIndicatesFloodlight(xml) {
7774
8197
  return false;
7775
8198
  }
7776
8199
 
8200
+ // src/reolink/baichuan/utils/sleepInference.ts
8201
+ function decideSleepInferenceTransition(input) {
8202
+ const { inferred, committed, pending, hysteresisPolls } = input;
8203
+ if (committed === void 0) {
8204
+ return {
8205
+ emit: inferred === "sleeping" ? "sleeping" : null,
8206
+ nextCommitted: inferred,
8207
+ nextPending: void 0
8208
+ };
8209
+ }
8210
+ if (inferred === committed) {
8211
+ return {
8212
+ emit: null,
8213
+ nextCommitted: committed,
8214
+ nextPending: void 0
8215
+ };
8216
+ }
8217
+ const effectivePolls = Math.max(1, hysteresisPolls);
8218
+ if (!pending || pending.state !== inferred) {
8219
+ if (effectivePolls <= 1) {
8220
+ return {
8221
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
8222
+ nextCommitted: inferred,
8223
+ nextPending: void 0
8224
+ };
8225
+ }
8226
+ return {
8227
+ emit: null,
8228
+ nextCommitted: committed,
8229
+ nextPending: { state: inferred, count: 1 }
8230
+ };
8231
+ }
8232
+ const nextCount = pending.count + 1;
8233
+ if (nextCount < effectivePolls) {
8234
+ return {
8235
+ emit: null,
8236
+ nextCommitted: committed,
8237
+ nextPending: { state: inferred, count: nextCount }
8238
+ };
8239
+ }
8240
+ return {
8241
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
8242
+ nextCommitted: inferred,
8243
+ nextPending: void 0
8244
+ };
8245
+ }
8246
+
7777
8247
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
7778
8248
  import { spawn as spawn2 } from "child_process";
7779
8249
  import { mkdir } from "fs/promises";
7780
8250
  import { dirname } from "path";
7781
8251
  import { PassThrough } from "stream";
7782
8252
 
7783
- // src/baichuan/stream/MpegTsMuxer.ts
7784
- var TS_PACKET_SIZE = 188;
7785
- var TS_SYNC_BYTE = 71;
7786
- var PAT_PID = 0;
7787
- var PMT_PID = 4096;
7788
- var VIDEO_PID = 256;
7789
- var STREAM_TYPE_H264 = 27;
7790
- var STREAM_TYPE_H265 = 36;
7791
- var patCc = 0;
7792
- var pmtCc = 0;
7793
- var videoCc = 0;
7794
- function createPat() {
7795
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
7796
- packet[0] = TS_SYNC_BYTE;
7797
- packet[1] = 64 | PAT_PID >> 8 & 31;
7798
- packet[2] = PAT_PID & 255;
7799
- packet[3] = 16 | patCc & 15;
7800
- patCc = patCc + 1 & 15;
7801
- packet[4] = 0;
7802
- let idx = 5;
7803
- packet[idx++] = 0;
7804
- packet[idx++] = 176;
7805
- packet[idx++] = 13;
7806
- packet[idx++] = 0;
7807
- packet[idx++] = 1;
7808
- packet[idx++] = 193;
7809
- packet[idx++] = 0;
7810
- packet[idx++] = 0;
7811
- packet[idx++] = 0;
7812
- packet[idx++] = 1;
7813
- packet[idx++] = 224 | PMT_PID >> 8 & 31;
7814
- packet[idx++] = PMT_PID & 255;
7815
- const crc = crc32Mpeg(packet.subarray(5, idx));
7816
- packet.writeUInt32BE(crc, idx);
7817
- return packet;
7818
- }
7819
- function createPmt(streamType) {
7820
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
7821
- packet[0] = TS_SYNC_BYTE;
7822
- packet[1] = 64 | PMT_PID >> 8 & 31;
7823
- packet[2] = PMT_PID & 255;
7824
- packet[3] = 16 | pmtCc & 15;
7825
- pmtCc = pmtCc + 1 & 15;
7826
- packet[4] = 0;
7827
- let idx = 5;
7828
- packet[idx++] = 2;
7829
- packet[idx++] = 176;
7830
- packet[idx++] = 18;
7831
- packet[idx++] = 0;
7832
- packet[idx++] = 1;
7833
- packet[idx++] = 193;
7834
- packet[idx++] = 0;
7835
- packet[idx++] = 0;
7836
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
7837
- packet[idx++] = VIDEO_PID & 255;
7838
- packet[idx++] = 240;
7839
- packet[idx++] = 0;
7840
- packet[idx++] = streamType;
7841
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
7842
- packet[idx++] = VIDEO_PID & 255;
7843
- packet[idx++] = 240;
7844
- packet[idx++] = 0;
7845
- const crc = crc32Mpeg(packet.subarray(5, idx));
7846
- packet.writeUInt32BE(crc, idx);
7847
- return packet;
7848
- }
7849
- function createVideoPes(data, pts, isKeyframe) {
7850
- const packets = [];
7851
- const pts90k = Math.floor(pts * 9e4 / 1e6);
7852
- const pesHeaderLen = 14;
7853
- const pesHeader = Buffer.alloc(pesHeaderLen);
7854
- let idx = 0;
7855
- pesHeader[idx++] = 0;
7856
- pesHeader[idx++] = 0;
7857
- pesHeader[idx++] = 1;
7858
- pesHeader[idx++] = 224;
7859
- pesHeader[idx++] = 0;
7860
- pesHeader[idx++] = 0;
7861
- pesHeader[idx++] = 128;
7862
- pesHeader[idx++] = 128;
7863
- pesHeader[idx++] = 5;
7864
- pesHeader[idx++] = 33 | pts90k >> 29 & 14;
7865
- pesHeader[idx++] = pts90k >> 22 & 255;
7866
- pesHeader[idx++] = 1 | pts90k >> 14 & 254;
7867
- pesHeader[idx++] = pts90k >> 7 & 255;
7868
- pesHeader[idx++] = 1 | pts90k << 1 & 254;
7869
- const pesData = Buffer.concat([pesHeader, data]);
7870
- let pesOffset = 0;
7871
- let isFirst = true;
7872
- while (pesOffset < pesData.length) {
7873
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
7874
- let pktIdx = 0;
7875
- packet[pktIdx++] = TS_SYNC_BYTE;
7876
- packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
7877
- packet[pktIdx++] = VIDEO_PID & 255;
7878
- const remaining = pesData.length - pesOffset;
7879
- const maxPayload = TS_PACKET_SIZE - 4;
7880
- if (remaining >= maxPayload) {
7881
- packet[pktIdx++] = 16 | videoCc & 15;
7882
- videoCc = videoCc + 1 & 15;
7883
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
7884
- pesOffset += maxPayload;
7885
- } else {
7886
- const adaptLen = maxPayload - remaining - 1;
7887
- if (adaptLen < 0) {
7888
- packet[pktIdx++] = 48 | videoCc & 15;
7889
- videoCc = videoCc + 1 & 15;
7890
- packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
7891
- if (isFirst && isKeyframe) {
7892
- packet[pktIdx++] = 64;
7893
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
7894
- packet[i] = 255;
7895
- }
7896
- } else {
7897
- packet[pktIdx++] = 0;
7898
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
7899
- packet[i] = 255;
7900
- }
7901
- }
7902
- pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
7903
- pesOffset += remaining;
7904
- } else {
7905
- packet[pktIdx++] = 48 | videoCc & 15;
7906
- videoCc = videoCc + 1 & 15;
7907
- if (adaptLen === 0) {
7908
- packet[pktIdx++] = 0;
7909
- } else {
7910
- packet[pktIdx++] = adaptLen;
7911
- if (isFirst && isKeyframe) {
7912
- packet[pktIdx++] = 64;
7913
- } else {
7914
- packet[pktIdx++] = 0;
7915
- }
7916
- for (let i = 0; i < adaptLen - 1; i++) {
7917
- packet[pktIdx++] = 255;
7918
- }
7919
- }
7920
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
7921
- pesOffset += remaining;
7922
- }
7923
- }
7924
- packets.push(packet);
7925
- isFirst = false;
7926
- }
7927
- return packets;
7928
- }
7929
- function crc32Mpeg(data) {
7930
- let crc = 4294967295;
7931
- for (let i = 0; i < data.length; i++) {
7932
- crc ^= data[i] << 24;
7933
- for (let j = 0; j < 8; j++) {
7934
- if (crc & 2147483648) {
7935
- crc = (crc << 1 ^ 79764919) >>> 0;
7936
- } else {
7937
- crc = crc << 1 >>> 0;
7938
- }
7939
- }
7940
- }
7941
- return crc >>> 0;
7942
- }
7943
- var MpegTsMuxer = class {
7944
- streamType;
7945
- patSent = false;
7946
- pmtSent = false;
7947
- patPmtInterval = 0;
7948
- patPmtIntervalMax = 40;
7949
- // Send PAT/PMT every ~40 frames
7950
- constructor(options) {
7951
- this.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
7952
- }
7953
- /**
7954
- * Reset continuity counters (call when starting a new stream).
7955
- */
7956
- static resetCounters() {
7957
- patCc = 0;
7958
- pmtCc = 0;
7959
- videoCc = 0;
7960
- }
7961
- /**
7962
- * Mux a video frame into MPEG-TS packets.
7963
- *
7964
- * @param data - Annex-B video data (with start codes)
7965
- * @param microseconds - Frame timestamp in microseconds
7966
- * @param isKeyframe - Whether this is a keyframe
7967
- * @returns Buffer containing all TS packets for this frame
7968
- */
7969
- mux(data, microseconds, isKeyframe) {
7970
- const packets = [];
7971
- if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
7972
- packets.push(createPat());
7973
- packets.push(createPmt(this.streamType));
7974
- this.patSent = true;
7975
- this.pmtSent = true;
7976
- this.patPmtInterval = 0;
7977
- }
7978
- this.patPmtInterval++;
7979
- const videoPackets = createVideoPes(data, microseconds, isKeyframe);
7980
- packets.push(...videoPackets);
7981
- return Buffer.concat(packets);
7982
- }
7983
- };
7984
-
7985
8253
  // src/reolink/baichuan/utils/xml.ts
7986
8254
  import { XMLParser } from "fast-xml-parser";
7987
8255
  var parser = new XMLParser({
@@ -10437,7 +10705,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10437
10705
  statePollingInterval;
10438
10706
  udpSleepInferenceInterval;
10439
10707
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
10708
+ /**
10709
+ * Per-channel pending sleep-state candidate for hysteresis.
10710
+ * When the inference flips to a new state we require N consecutive polls
10711
+ * of that same state before committing it — this filters out transient
10712
+ * flapping caused by non-waking traffic drifting in/out of the 10 s
10713
+ * getSleepStatus() observation window during stream teardown.
10714
+ */
10715
+ udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
10440
10716
  udpSleepInferenceIntervalMs = 2e3;
10717
+ /** Consecutive inference polls required to commit a new sleeping/awake state. */
10718
+ udpSleepInferenceHysteresisPolls = 2;
10441
10719
  lastMotionState;
10442
10720
  lastAiState;
10443
10721
  aiStatePollingDisabled = false;
@@ -10904,6 +11182,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10904
11182
  */
10905
11183
  attachD2cDiscListener(client) {
10906
11184
  client.on("d2c_disc", () => this.notifyD2cDisc());
11185
+ client.on("error", () => {
11186
+ });
10907
11187
  }
10908
11188
  /**
10909
11189
  * Acquire a socket from the pool by tag.
@@ -11022,6 +11302,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11022
11302
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
11023
11303
  const newClient = new BaichuanClient(clientOpts);
11024
11304
  this.attachD2cDiscListener(newClient);
11305
+ newClient.on("error", (err) => {
11306
+ log?.debug?.(
11307
+ `[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
11308
+ );
11309
+ });
11025
11310
  await newClient.login();
11026
11311
  const existingCooldown = this.socketPoolCooldowns.get(this.host);
11027
11312
  if (existingCooldown) {
@@ -11537,6 +11822,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11537
11822
  * Only counts sessions from our own IP address.
11538
11823
  */
11539
11824
  async maybeRebootOnTooManySessions() {
11825
+ if (!this.client.isSocketConnected?.()) return;
11540
11826
  const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
11541
11827
  if (this.sessionGuardRebootInFlight) return;
11542
11828
  const cooldownMs = 10 * 6e4;
@@ -11978,6 +12264,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11978
12264
  }
11979
12265
  async renewSimpleEventSubscription() {
11980
12266
  if (this.simpleEventListeners.size === 0) return;
12267
+ if (!this.client.isSocketConnected?.()) return;
11981
12268
  if (this.simpleEventResubscribeInFlight)
11982
12269
  return await this.simpleEventResubscribeInFlight;
11983
12270
  this.simpleEventResubscribeInFlight = (async () => {
@@ -15745,23 +16032,32 @@ ${stderr}`)
15745
16032
  return;
15746
16033
  }
15747
16034
  const channel = this.client.getConfiguredChannel?.() ?? 0;
16035
+ if (!this.client.isSocketConnected?.()) {
16036
+ this.udpPendingSleepStateByChannel.delete(channel);
16037
+ return;
16038
+ }
15748
16039
  const status = this.getSleepStatus({ channel });
15749
16040
  if (status.state === "unknown") return;
15750
- const prev = this.udpLastInferredSleepStateByChannel.get(channel);
15751
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
15752
- if (prev === void 0) {
15753
- if (status.state === "sleeping") {
15754
- this.dispatchSimpleEvent({
15755
- type: "sleeping",
15756
- channel,
15757
- timestamp: Date.now()
15758
- });
15759
- }
15760
- return;
16041
+ const committed = this.udpLastInferredSleepStateByChannel.get(channel);
16042
+ const pending = this.udpPendingSleepStateByChannel.get(channel);
16043
+ const decision = decideSleepInferenceTransition({
16044
+ inferred: status.state,
16045
+ committed,
16046
+ pending,
16047
+ hysteresisPolls: this.udpSleepInferenceHysteresisPolls
16048
+ });
16049
+ this.udpLastInferredSleepStateByChannel.set(
16050
+ channel,
16051
+ decision.nextCommitted
16052
+ );
16053
+ if (decision.nextPending === void 0) {
16054
+ this.udpPendingSleepStateByChannel.delete(channel);
16055
+ } else {
16056
+ this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
15761
16057
  }
15762
- if (prev !== status.state) {
16058
+ if (decision.emit) {
15763
16059
  this.dispatchSimpleEvent({
15764
- type: status.state === "sleeping" ? "sleeping" : "awake",
16060
+ type: decision.emit,
15765
16061
  channel,
15766
16062
  timestamp: Date.now()
15767
16063
  });
@@ -15784,6 +16080,7 @@ ${stderr}`)
15784
16080
  this.udpSleepInferenceInterval = void 0;
15785
16081
  }
15786
16082
  this.udpLastInferredSleepStateByChannel.clear();
16083
+ this.udpPendingSleepStateByChannel.clear();
15787
16084
  }
15788
16085
  /**
15789
16086
  * GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
@@ -18180,7 +18477,7 @@ ${xml}`
18180
18477
  * @returns Test results for all stream types and profiles
18181
18478
  */
18182
18479
  async testChannelStreams(channel, logger) {
18183
- const { testChannelStreams } = await import("./DiagnosticsTools-UMN4C7SY.js");
18480
+ const { testChannelStreams } = await import("./DiagnosticsTools-RNIDFEJK.js");
18184
18481
  return await testChannelStreams({
18185
18482
  api: this,
18186
18483
  channel: this.normalizeChannel(channel),
@@ -18196,7 +18493,7 @@ ${xml}`
18196
18493
  * @returns Complete diagnostics for all channels and streams
18197
18494
  */
18198
18495
  async collectMultifocalDiagnostics(logger) {
18199
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-UMN4C7SY.js");
18496
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-RNIDFEJK.js");
18200
18497
  return await collectMultifocalDiagnostics({
18201
18498
  api: this,
18202
18499
  logger
@@ -18214,6 +18511,373 @@ ${xml}`
18214
18511
  await this.cgiApi.login();
18215
18512
  return await this.cgiApi.getAllChannelsEvents(options);
18216
18513
  }
18514
+ // ====================================================================
18515
+ // Native Baichuan tunable-settings setters
18516
+ //
18517
+ // Replace the CGI passthroughs above with on-wire Baichuan binary
18518
+ // calls. Mirrors the @http_cmd-decorated methods in reolink_aio's
18519
+ // baichuan.py — every command has a documented `cmd_id` (read) and
18520
+ // `cmd_id` (write) pair. The pattern is:
18521
+ //
18522
+ // 1. read XML via `sendXml({ cmdId: GET, channel })`
18523
+ // 2. patch fields via regex (camera firmware is XML-strict; using
18524
+ // the parser would force us to rebuild the document and risk
18525
+ // losing unmodified attributes / element order).
18526
+ // 3. write back via `sendXml({ cmdId: SET, channel, payloadXml })`
18527
+ //
18528
+ // All getters parse via `parseXmlFragmentToJson` so the consumer gets
18529
+ // a clean JSON object instead of XML.
18530
+ // ====================================================================
18531
+ /**
18532
+ * GetEnc via Baichuan (cmdId=56). Returns the `<Compression>` block:
18533
+ * per-stream `mainStream` / `subStream` / `thirdStream` with `audio`
18534
+ * flag, `width`, `height`, `frame` (NOT `frameRate`), `bitRate`,
18535
+ * `videoEncType` (0=h264, 1=h265), `encoderProfile`, `gop`. Mirrors
18536
+ * reolink_aio's `GetEnc` — note the wire payload wraps everything
18537
+ * in `Compression`, not `Enc`.
18538
+ */
18539
+ async getEnc(channel, options) {
18540
+ const xml = await this.sendPcapDerivedSettingsGetXml({
18541
+ cmdId: BC_CMD_ID_GET_ENC,
18542
+ ...channel != null ? { channel } : {},
18543
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18544
+ });
18545
+ return parseXmlFragmentToJson(xml);
18546
+ }
18547
+ /**
18548
+ * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
18549
+ * unspecified fields. Mirrors reolink_aio's `SetEnc`.
18550
+ *
18551
+ * @param channel - Channel number (0-based)
18552
+ * @param patch - Fields to update on `mainStream` and/or `subStream`,
18553
+ * plus a top-level `audio` toggle (0/1). Pass only what you want
18554
+ * to change.
18555
+ */
18556
+ async setEnc(channel, patch, options) {
18557
+ const ch = this.normalizeChannel(channel);
18558
+ let xml = await this.sendXml({
18559
+ cmdId: BC_CMD_ID_GET_ENC,
18560
+ channel: ch,
18561
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18562
+ });
18563
+ if (patch.audio !== void 0) {
18564
+ xml = xml.replace(
18565
+ /<audio>[^<]*<\/audio>/g,
18566
+ `<audio>${patch.audio}</audio>`
18567
+ );
18568
+ }
18569
+ xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
18570
+ xml = applyStreamPatch(xml, "subStream", patch.subStream);
18571
+ await this.sendXml({
18572
+ cmdId: BC_CMD_ID_SET_ENC,
18573
+ channel: ch,
18574
+ payloadXml: ensureXmlHeader(xml),
18575
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18576
+ });
18577
+ }
18578
+ /**
18579
+ * SetImage via Baichuan (cmdId=25, read via cmdId=26). Patches the
18580
+ * `<VideoInput>` block: bright / contrast / saturation / hue /
18581
+ * sharpen. Mirrors reolink_aio's `SetImage`.
18582
+ */
18583
+ async setImage(channel, patch, options) {
18584
+ const ch = this.normalizeChannel(channel);
18585
+ let xml = await this.sendXml({
18586
+ cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
18587
+ channel: ch,
18588
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18589
+ });
18590
+ xml = applyXmlTagPatch(xml, "bright", patch.bright);
18591
+ xml = applyXmlTagPatch(xml, "contrast", patch.contrast);
18592
+ xml = applyXmlTagPatch(xml, "saturation", patch.saturation);
18593
+ xml = applyXmlTagPatch(xml, "hue", patch.hue);
18594
+ xml = applyXmlTagPatch(xml, "sharpen", patch.sharpen);
18595
+ await this.sendXml({
18596
+ cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
18597
+ channel: ch,
18598
+ payloadXml: ensureXmlHeader(xml),
18599
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18600
+ });
18601
+ }
18602
+ /**
18603
+ * SetIsp via Baichuan (cmdId=25 for image side, cmdId=297 for
18604
+ * dayNightThreshold). Patches the `<InputAdvanceCfg>` block:
18605
+ * `DayNight/mode`, `Exposure/mode`, `binning_mode`, `hdrSwitch`.
18606
+ * Mirrors reolink_aio's `SetIsp`.
18607
+ *
18608
+ * @param channel - Channel number (0-based)
18609
+ * @param patch - Fields to update. `dayNight` accepts the camera's
18610
+ * raw enum (`color`, `auto`, `blackAndWhite`, …) — pass it as the
18611
+ * camera reports it (PascalCase / dotted forms get normalized
18612
+ * server-side).
18613
+ */
18614
+ async setIsp(channel, patch, options) {
18615
+ const ch = this.normalizeChannel(channel);
18616
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
18617
+ const wantsImageWrite = patch.dayNight !== void 0 || patch.exposure !== void 0 || patch.binningMode !== void 0 || patch.hdr !== void 0;
18618
+ if (wantsImageWrite) {
18619
+ let xml = await this.sendXml({
18620
+ cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
18621
+ channel: ch,
18622
+ ...timeoutOpts
18623
+ });
18624
+ if (patch.dayNight !== void 0) {
18625
+ const normalized = normalizeDayNightMode(patch.dayNight);
18626
+ xml = patchNestedTag(xml, "DayNight", "mode", normalized);
18627
+ }
18628
+ if (patch.exposure !== void 0) {
18629
+ xml = patchNestedTag(
18630
+ xml,
18631
+ "Exposure",
18632
+ "mode",
18633
+ patch.exposure.toLowerCase()
18634
+ );
18635
+ }
18636
+ if (patch.binningMode !== void 0) {
18637
+ xml = applyXmlTagPatch(xml, "binning_mode", patch.binningMode);
18638
+ }
18639
+ if (patch.hdr !== void 0) {
18640
+ xml = applyXmlTagPatch(xml, "hdrSwitch", patch.hdr);
18641
+ }
18642
+ await this.sendXml({
18643
+ cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
18644
+ channel: ch,
18645
+ payloadXml: ensureXmlHeader(xml),
18646
+ ...timeoutOpts
18647
+ });
18648
+ }
18649
+ if (patch.dayNightThreshold !== void 0) {
18650
+ let xml = await this.sendXml({
18651
+ cmdId: BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
18652
+ channel: ch,
18653
+ ...timeoutOpts
18654
+ });
18655
+ xml = applyXmlTagPatch(xml, "cur", patch.dayNightThreshold);
18656
+ await this.sendXml({
18657
+ cmdId: BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
18658
+ channel: ch,
18659
+ payloadXml: ensureXmlHeader(xml),
18660
+ ...timeoutOpts
18661
+ });
18662
+ }
18663
+ }
18664
+ /**
18665
+ * GetIsp via Baichuan (cmdId=26). Convenience alias of
18666
+ * `getVideoInput()` so callers that switched from CGI keep the
18667
+ * familiar name. Both return the merged VideoInput +
18668
+ * InputAdvanceCfg blob.
18669
+ */
18670
+ async getIsp(channel, options) {
18671
+ return this.getVideoInput(channel, options);
18672
+ }
18673
+ /** GetImage via Baichuan (cmdId=26). Same payload as `getIsp` —
18674
+ * Reolink merged VideoInput + InputAdvanceCfg under one cmdId. */
18675
+ async getImage(channel, options) {
18676
+ return this.getVideoInput(channel, options);
18677
+ }
18678
+ /**
18679
+ * GetIrLights via Baichuan (cmdId=208). Returns LedState block:
18680
+ * `IRLedBrightness`, `state` (ir on/off), `lightState` (status LED
18681
+ * open/close), `doorbellLightState`. Mirrors reolink_aio's
18682
+ * `get_status_led`.
18683
+ */
18684
+ async getIrLights(channel, options) {
18685
+ const xml = await this.sendPcapDerivedSettingsGetXml({
18686
+ cmdId: BC_CMD_ID_GET_LED_STATE,
18687
+ ...channel != null ? { channel } : {},
18688
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18689
+ });
18690
+ return parseXmlFragmentToJson(xml);
18691
+ }
18692
+ /**
18693
+ * SetIrLights via Baichuan (cmdId=209, read via cmdId=208). Patches
18694
+ * IR LED + status LED + doorbell LED + IR brightness. Mirrors
18695
+ * reolink_aio's `set_status_led`.
18696
+ *
18697
+ * @param channel - Channel number (0-based)
18698
+ * @param patch - `irState` ("On" | "Off" | "Auto"), `lightState`
18699
+ * (status LED), `doorbellLightState`, `irBrightness` (0..255).
18700
+ * Camera-side accepts lowercase strings (`open`/`close`); the
18701
+ * helper normalizes from the friendly variants.
18702
+ */
18703
+ async setIrLights(channel, patch, options) {
18704
+ const ch = this.normalizeChannel(channel);
18705
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
18706
+ let xml = await this.sendXml({
18707
+ cmdId: BC_CMD_ID_GET_LED_STATE,
18708
+ channel: ch,
18709
+ ...timeoutOpts
18710
+ });
18711
+ if (patch.lightState !== void 0) {
18712
+ xml = applyXmlTagPatch(
18713
+ xml,
18714
+ "lightState",
18715
+ patch.lightState === "On" ? "open" : "close"
18716
+ );
18717
+ }
18718
+ if (patch.doorbellLightState !== void 0) {
18719
+ xml = applyXmlTagPatch(
18720
+ xml,
18721
+ "doorbellLightState",
18722
+ normalizeOpenClose(patch.doorbellLightState)
18723
+ );
18724
+ }
18725
+ if (patch.irState !== void 0) {
18726
+ const v = String(patch.irState);
18727
+ const out = v === "Off" ? "close" : v.toLowerCase();
18728
+ xml = applyXmlTagPatch(xml, "state", out);
18729
+ }
18730
+ if (patch.irBrightness !== void 0) {
18731
+ xml = applyXmlTagPatch(xml, "IRLedBrightness", patch.irBrightness);
18732
+ }
18733
+ await this.sendXml({
18734
+ cmdId: BC_CMD_ID_SET_LED_STATE,
18735
+ channel: ch,
18736
+ payloadXml: ensureXmlHeader(xml),
18737
+ ...timeoutOpts
18738
+ });
18739
+ }
18740
+ /**
18741
+ * SetAudioCfg via Baichuan (cmdId=265, read via cmdId=264). Patches
18742
+ * volume / talk-and-reply / visitor settings. Mirrors reolink_aio's
18743
+ * `SetAudioCfg`.
18744
+ */
18745
+ async setAudioCfg(channel, patch, options) {
18746
+ const ch = this.normalizeChannel(channel);
18747
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
18748
+ let xml = await this.sendXml({
18749
+ cmdId: BC_CMD_ID_GET_AUDIO_CFG,
18750
+ channel: ch,
18751
+ ...timeoutOpts
18752
+ });
18753
+ xml = applyXmlTagPatch(xml, "volume", patch.volume);
18754
+ xml = applyXmlTagPatch(
18755
+ xml,
18756
+ "talkAndReplyVolume",
18757
+ patch.talkAndReplyVolume
18758
+ );
18759
+ xml = applyXmlTagPatch(xml, "visitorVolume", patch.visitorVolume);
18760
+ xml = applyXmlTagPatch(xml, "visitorLoudspeaker", patch.visitorLoudspeaker);
18761
+ await this.sendXml({
18762
+ cmdId: BC_CMD_ID_SET_AUDIO_CFG,
18763
+ channel: ch,
18764
+ payloadXml: ensureXmlHeader(xml),
18765
+ ...timeoutOpts
18766
+ });
18767
+ }
18768
+ /**
18769
+ * GetMask (privacy mask) via Baichuan (cmdId=52). Returns the
18770
+ * `<Shelter>` block — `enable` flag + `shelterList`. Mirrors
18771
+ * reolink_aio's `GetMask`.
18772
+ */
18773
+ async getMask(channel, options) {
18774
+ const xml = await this.sendPcapDerivedSettingsGetXml({
18775
+ cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
18776
+ ...channel != null ? { channel } : {},
18777
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18778
+ });
18779
+ return parseXmlFragmentToJson(xml);
18780
+ }
18781
+ /**
18782
+ * SetMask (privacy mask) via Baichuan (cmdId=53, read via cmdId=52).
18783
+ * Toggles the `<Shelter><enable>` flag. Mirrors reolink_aio's
18784
+ * `SetMask` (which only touches enable too — shelter zone editing
18785
+ * goes through a separate flow).
18786
+ */
18787
+ async setMask(channel, patch, options) {
18788
+ const ch = this.normalizeChannel(channel);
18789
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
18790
+ let xml = await this.sendXml({
18791
+ cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
18792
+ channel: ch,
18793
+ ...timeoutOpts
18794
+ });
18795
+ if (patch.enable !== void 0) {
18796
+ xml = applyXmlTagPatch(xml, "enable", patch.enable ? 1 : 0);
18797
+ }
18798
+ await this.sendXml({
18799
+ cmdId: BC_CMD_ID_SET_PRIVACY_MASK,
18800
+ channel: ch,
18801
+ payloadXml: ensureXmlHeader(xml),
18802
+ ...timeoutOpts
18803
+ });
18804
+ }
18805
+ /**
18806
+ * GetAudioNoise via Baichuan (cmdId=439). Reads `enable` + `level`
18807
+ * from the aiDenoise block. Mirrors reolink_aio's `GetAudioNoise`.
18808
+ *
18809
+ * Note: `getAiDenoise` already returns the same payload typed as
18810
+ * `AiDenoiseConfig`. This getter exists for naming parity with
18811
+ * reolink_aio + the reolink CGI.
18812
+ */
18813
+ async getAudioNoise(channel, options) {
18814
+ const xml = await this.sendPcapDerivedSettingsGetXml({
18815
+ cmdId: BC_CMD_ID_GET_AI_DENOISE,
18816
+ ...channel != null ? { channel } : {},
18817
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18818
+ });
18819
+ return parseXmlFragmentToJson(xml);
18820
+ }
18821
+ /**
18822
+ * SetAudioNoise via Baichuan (cmdId=440, read via cmdId=439).
18823
+ * Mirrors reolink_aio's `SetAudioNoise` — `level <= 0` flips the
18824
+ * enable flag off; positive values turn it on and update the level.
18825
+ */
18826
+ async setAudioNoise(channel, level, options) {
18827
+ const ch = this.normalizeChannel(channel);
18828
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
18829
+ let xml = await this.sendXml({
18830
+ cmdId: BC_CMD_ID_GET_AI_DENOISE,
18831
+ channel: ch,
18832
+ ...timeoutOpts
18833
+ });
18834
+ xml = applyXmlTagPatch(xml, "enable", level > 0 ? 1 : 0);
18835
+ if (level > 0) {
18836
+ xml = applyXmlTagPatch(xml, "level", level);
18837
+ }
18838
+ await this.sendXml({
18839
+ cmdId: BC_CMD_ID_SET_AI_DENOISE,
18840
+ channel: ch,
18841
+ payloadXml: ensureXmlHeader(xml),
18842
+ ...timeoutOpts
18843
+ });
18844
+ }
18845
+ /**
18846
+ * GetAutoFocus via Baichuan (cmdId=224). Returns the `<AutoFocus>`
18847
+ * block — only `disable` (0 = AF on, 1 = AF off). Mirrors
18848
+ * reolink_aio's `GetAutoFocus`.
18849
+ */
18850
+ async getAutoFocus(channel, options) {
18851
+ const ch = this.normalizeChannel(channel);
18852
+ const xml = await this.sendPcapDerivedSettingsGetXml({
18853
+ cmdId: BC_CMD_ID_GET_AUTO_FOCUS,
18854
+ channel: ch,
18855
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18856
+ });
18857
+ return parseXmlFragmentToJson(xml);
18858
+ }
18859
+ /**
18860
+ * SetAutoFocus via Baichuan (cmdId=225). Mirrors reolink_aio's
18861
+ * `SetAutoFocus`. Note: write-only command — the payload is built
18862
+ * from scratch (no read-modify-write needed).
18863
+ */
18864
+ async setAutoFocus(channel, disable, options) {
18865
+ const ch = this.normalizeChannel(channel);
18866
+ const disableVal = disable ? 1 : 0;
18867
+ const payloadXml = `<?xml version="1.0" encoding="UTF-8" ?>
18868
+ <body>
18869
+ <AutoFocus version="1.1">
18870
+ <channelId>${ch}</channelId>
18871
+ <disable>${disableVal}</disable>
18872
+ </AutoFocus>
18873
+ </body>`;
18874
+ await this.sendXml({
18875
+ cmdId: BC_CMD_ID_SET_AUTO_FOCUS,
18876
+ channel: ch,
18877
+ payloadXml,
18878
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
18879
+ });
18880
+ }
18217
18881
  /**
18218
18882
  * Passthrough to ReolinkCgiApi.getAllChannelsBatteryInfo.
18219
18883
  * Fetches battery info for all channels via CGI (merged with channel status sleep flag).
@@ -19382,8 +20046,8 @@ ${scheduleItems}
19382
20046
  );
19383
20047
  let args;
19384
20048
  if (useMpegTsMuxer) {
19385
- MpegTsMuxer.resetCounters();
19386
- tsMuxer = new MpegTsMuxer({ videoType });
20049
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
20050
+ tsMuxer.reset();
19387
20051
  args = [
19388
20052
  "-hide_banner",
19389
20053
  "-loglevel",
@@ -19547,7 +20211,7 @@ ${scheduleItems}
19547
20211
  startFfmpeg(videoType);
19548
20212
  frameCount++;
19549
20213
  if (useMpegTsMuxer && tsMuxer) {
19550
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
20214
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
19551
20215
  input.write(tsData);
19552
20216
  } else {
19553
20217
  if (videoType === "H264") input.write(H264_AUD);
@@ -19836,8 +20500,8 @@ ${scheduleItems}
19836
20500
  logger?.log?.(
19837
20501
  `[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
19838
20502
  );
19839
- MpegTsMuxer.resetCounters();
19840
- tsMuxer = new MpegTsMuxer({ videoType });
20503
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
20504
+ tsMuxer.reset();
19841
20505
  const args = [
19842
20506
  "-hide_banner",
19843
20507
  "-loglevel",
@@ -20002,7 +20666,7 @@ ${scheduleItems}
20002
20666
  startFfmpeg(videoType);
20003
20667
  frameCount++;
20004
20668
  if (tsMuxer) {
20005
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
20669
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
20006
20670
  input.write(tsData);
20007
20671
  }
20008
20672
  if (frameCount === 1) {
@@ -21414,9 +22078,10 @@ async function autoDetectDeviceType(inputs) {
21414
22078
  const msg = fmtErr(e);
21415
22079
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
21416
22080
  };
21417
- const withRetries = async (label, max, op, shouldRetry) => {
22081
+ const withRetries = async (label, max, op, shouldRetry, isAborted) => {
21418
22082
  let lastErr;
21419
22083
  for (let attempt = 1; attempt <= max; attempt++) {
22084
+ if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
21420
22085
  try {
21421
22086
  if (attempt > 1) {
21422
22087
  logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
@@ -21425,7 +22090,7 @@ async function autoDetectDeviceType(inputs) {
21425
22090
  } catch (e) {
21426
22091
  lastErr = e;
21427
22092
  const msg = fmtErr(e);
21428
- const retryable = attempt < max && shouldRetry(e);
22093
+ const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
21429
22094
  logger?.log?.(
21430
22095
  `[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
21431
22096
  );
@@ -21435,6 +22100,31 @@ async function autoDetectDeviceType(inputs) {
21435
22100
  }
21436
22101
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
21437
22102
  };
22103
+ const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
22104
+ let raceWon = false;
22105
+ const methodErrors = /* @__PURE__ */ new Map();
22106
+ try {
22107
+ return await Promise.any(
22108
+ methods.map(async (m) => {
22109
+ try {
22110
+ const result = await loginAndDetect(m, () => raceWon);
22111
+ raceWon = true;
22112
+ return result;
22113
+ } catch (e) {
22114
+ if (!raceWon) methodErrors.set(m, fmtErr(e));
22115
+ logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
22116
+ throw e;
22117
+ }
22118
+ })
22119
+ );
22120
+ } catch (e) {
22121
+ if (e instanceof AggregateError) {
22122
+ const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
22123
+ throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
22124
+ }
22125
+ throw e;
22126
+ }
22127
+ };
21438
22128
  const effectiveUid = normalizeUid(uid);
21439
22129
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
21440
22130
  const isReachable = await pingHost(host);
@@ -21464,9 +22154,9 @@ async function autoDetectDeviceType(inputs) {
21464
22154
  normalizedUid = normalizedDiscovered;
21465
22155
  }
21466
22156
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
21467
- const udpErrors = [];
21468
- for (const m of methodsToTry) {
21469
- try {
22157
+ return await runUdpMethodsParallel(
22158
+ methodsToTry,
22159
+ async (m, isAborted) => {
21470
22160
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
21471
22161
  const udpApi = await withRetries(
21472
22162
  `UDP(${m})`,
@@ -21489,11 +22179,14 @@ async function autoDetectDeviceType(inputs) {
21489
22179
  throw e;
21490
22180
  }
21491
22181
  },
21492
- shouldRetryUdp
22182
+ shouldRetryUdp,
22183
+ isAborted
21493
22184
  );
21494
- const deviceInfo = await udpApi.getInfo();
21495
- const capabilities = await udpApi.getDeviceCapabilities();
21496
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
22185
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
22186
+ udpApi.getInfo(),
22187
+ udpApi.getDeviceCapabilities(),
22188
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
22189
+ ]);
21497
22190
  const channelNum = capabilities?.support?.channelNum ?? 1;
21498
22191
  const model = deviceInfo.type?.trim();
21499
22192
  const normalizedModel = model ? model.trim() : void 0;
@@ -21532,14 +22225,8 @@ async function autoDetectDeviceType(inputs) {
21532
22225
  channelNum: 1,
21533
22226
  api: udpApi
21534
22227
  };
21535
- } catch (e) {
21536
- const msg = fmtErr(e);
21537
- udpErrors.push(`${m}: ${msg}`);
21538
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
21539
- }
21540
- }
21541
- throw new Error(
21542
- `Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
22228
+ },
22229
+ "Forced UDP autodetect failed for all methods."
21543
22230
  );
21544
22231
  }
21545
22232
  let tcpApi;
@@ -21592,54 +22279,57 @@ async function autoDetectDeviceType(inputs) {
21592
22279
  }
21593
22280
  return void 0;
21594
22281
  };
21595
- const infoProbe = await runProbeVariants(
21596
- "getInfo",
21597
- [
22282
+ const [infoProbe, supportProbe] = await Promise.all([
22283
+ runProbeVariants(
22284
+ "getInfo",
22285
+ [
22286
+ {
22287
+ variant: "cmd80 class=0x6414",
22288
+ op: () => api.getInfo(void 0, {
22289
+ timeoutMs: 2500,
22290
+ messageClass: BC_CLASS_MODERN_24
22291
+ })
22292
+ },
22293
+ {
22294
+ variant: "cmd80 class=0x6614",
22295
+ op: () => api.getInfo(void 0, {
22296
+ timeoutMs: 3e3,
22297
+ messageClass: BC_CLASS_MODERN_20
22298
+ })
22299
+ },
22300
+ {
22301
+ variant: "cmd318(ch0) class=0x6414",
22302
+ op: () => api.getInfo(0, {
22303
+ timeoutMs: 3e3,
22304
+ messageClass: BC_CLASS_MODERN_24
22305
+ })
22306
+ },
22307
+ {
22308
+ variant: "cmd318(ch0) class=0x6614",
22309
+ op: () => api.getInfo(0, {
22310
+ timeoutMs: 3500,
22311
+ messageClass: BC_CLASS_MODERN_20
22312
+ })
22313
+ }
22314
+ ]
22315
+ ),
22316
+ // Support probes (cmd 199). Some firmwares may not support it or are slow.
22317
+ runProbeVariants("getSupportInfo", [
21598
22318
  {
21599
- variant: "cmd80 class=0x6414",
21600
- op: () => api.getInfo(void 0, {
22319
+ variant: "cmd199 class=0x6414",
22320
+ op: () => api.getSupportInfo({
21601
22321
  timeoutMs: 2500,
21602
22322
  messageClass: BC_CLASS_MODERN_24
21603
22323
  })
21604
22324
  },
21605
22325
  {
21606
- variant: "cmd80 class=0x6614",
21607
- op: () => api.getInfo(void 0, {
21608
- timeoutMs: 3e3,
21609
- messageClass: BC_CLASS_MODERN_20
21610
- })
21611
- },
21612
- {
21613
- variant: "cmd318(ch0) class=0x6414",
21614
- op: () => api.getInfo(0, {
21615
- timeoutMs: 3e3,
21616
- messageClass: BC_CLASS_MODERN_24
21617
- })
21618
- },
21619
- {
21620
- variant: "cmd318(ch0) class=0x6614",
21621
- op: () => api.getInfo(0, {
22326
+ variant: "cmd199 class=0x6614",
22327
+ op: () => api.getSupportInfo({
21622
22328
  timeoutMs: 3500,
21623
22329
  messageClass: BC_CLASS_MODERN_20
21624
22330
  })
21625
22331
  }
21626
- ]
21627
- );
21628
- const supportProbe = await runProbeVariants("getSupportInfo", [
21629
- {
21630
- variant: "cmd199 class=0x6414",
21631
- op: () => api.getSupportInfo({
21632
- timeoutMs: 2500,
21633
- messageClass: BC_CLASS_MODERN_24
21634
- })
21635
- },
21636
- {
21637
- variant: "cmd199 class=0x6614",
21638
- op: () => api.getSupportInfo({
21639
- timeoutMs: 3500,
21640
- messageClass: BC_CLASS_MODERN_20
21641
- })
21642
- }
22332
+ ])
21643
22333
  ]);
21644
22334
  const deviceInfo = infoProbe?.value;
21645
22335
  const support = supportProbe?.value;
@@ -21731,9 +22421,11 @@ async function autoDetectDeviceType(inputs) {
21731
22421
  }
21732
22422
  try {
21733
22423
  const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
21734
- const deviceInfo = await udpApi.getInfo();
21735
- const capabilities = await udpApi.getDeviceCapabilities();
21736
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
22424
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
22425
+ udpApi.getInfo(),
22426
+ udpApi.getDeviceCapabilities(),
22427
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
22428
+ ]);
21737
22429
  const channelNum = capabilities?.support?.channelNum ?? 1;
21738
22430
  const model = deviceInfo.type?.trim();
21739
22431
  const normalizedModel = model ? model.trim() : void 0;
@@ -21777,21 +22469,17 @@ async function autoDetectDeviceType(inputs) {
21777
22469
  };
21778
22470
  };
21779
22471
  const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
21780
- const udpErrors = [];
21781
- for (const m of methodsToTry) {
21782
- try {
22472
+ const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
22473
+ return await runUdpMethodsParallel(
22474
+ viableMethods,
22475
+ async (m, isAborted) => {
21783
22476
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
21784
22477
  const udpApi = await withRetries(
21785
22478
  `UDP(${m})`,
21786
22479
  maxRetries,
21787
22480
  async (attempt) => {
21788
- const apiInputs = {
21789
- ...inputs,
21790
- udpDiscoveryMethod: m
21791
- };
21792
- if (normalizedUid) {
21793
- apiInputs.uid = normalizedUid;
21794
- }
22481
+ const apiInputs = { ...inputs, udpDiscoveryMethod: m };
22482
+ if (normalizedUid) apiInputs.uid = normalizedUid;
21795
22483
  const api = createBaichuanApi(apiInputs, "udp");
21796
22484
  try {
21797
22485
  await api.login();
@@ -21806,20 +22494,12 @@ async function autoDetectDeviceType(inputs) {
21806
22494
  throw e;
21807
22495
  }
21808
22496
  },
21809
- shouldRetryUdp
22497
+ shouldRetryUdp,
22498
+ isAborted
21810
22499
  );
21811
- return await detectOverUdpApi(udpApi, m);
21812
- } catch (e) {
21813
- const msg = e?.message || e?.toString?.() || String(e);
21814
- udpErrors.push(`${m}: ${msg}`);
21815
- try {
21816
- } catch {
21817
- }
21818
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
21819
- }
21820
- }
21821
- throw new Error(
21822
- `UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
22500
+ return detectOverUdpApi(udpApi, m);
22501
+ },
22502
+ "UDP discovery failed for all methods."
21823
22503
  );
21824
22504
  } catch (udpError) {
21825
22505
  logger?.log?.(
@@ -21851,12 +22531,14 @@ export {
21851
22531
  BaichuanEventEmitter,
21852
22532
  createNativeStream,
21853
22533
  BaichuanRtspServer,
22534
+ MpegTsMuxer,
21854
22535
  flattenAbilitiesForChannel,
21855
22536
  abilitiesHasAny,
21856
22537
  parseSupportXml,
21857
22538
  getSupportItemForChannel,
21858
22539
  computeDeviceCapabilities,
21859
22540
  xmlIndicatesFloodlight,
22541
+ decideSleepInferenceTransition,
21860
22542
  DUAL_LENS_DUAL_MOTION_MODELS,
21861
22543
  DUAL_LENS_SINGLE_MOTION_MODELS,
21862
22544
  DUAL_LENS_MODELS,
@@ -21878,4 +22560,4 @@ export {
21878
22560
  isTcpFailureThatShouldFallbackToUdp,
21879
22561
  autoDetectDeviceType
21880
22562
  };
21881
- //# sourceMappingURL=chunk-GKLOJJ34.js.map
22563
+ //# sourceMappingURL=chunk-HGQ53FB3.js.map