@apocaliss92/nodelink-js 0.1.20 → 0.2.2

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.
@@ -10,6 +10,8 @@ import {
10
10
  BC_CMD_ID_CMD_209,
11
11
  BC_CMD_ID_CMD_265,
12
12
  BC_CMD_ID_CMD_440,
13
+ BC_CMD_ID_DING_DONG_CTRL,
14
+ BC_CMD_ID_DING_DONG_OPT,
13
15
  BC_CMD_ID_FILE_INFO_LIST_CLOSE,
14
16
  BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO,
15
17
  BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD,
@@ -33,6 +35,9 @@ import {
33
35
  BC_CMD_ID_GET_BATTERY_INFO_LIST,
34
36
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
35
37
  BC_CMD_ID_GET_DAY_RECORDS,
38
+ BC_CMD_ID_GET_DING_DONG_CFG,
39
+ BC_CMD_ID_GET_DING_DONG_LIST,
40
+ BC_CMD_ID_GET_DING_DONG_SILENT,
36
41
  BC_CMD_ID_GET_EMAIL_TASK,
37
42
  BC_CMD_ID_GET_FTP_TASK,
38
43
  BC_CMD_ID_GET_HDD_INFO_LIST,
@@ -68,9 +73,12 @@ import {
68
73
  BC_CMD_ID_PUSH_SERIAL,
69
74
  BC_CMD_ID_PUSH_SLEEP_STATUS,
70
75
  BC_CMD_ID_PUSH_VIDEO_INPUT,
76
+ BC_CMD_ID_QUICK_REPLY_PLAY,
71
77
  BC_CMD_ID_SET_AI_ALARM,
72
78
  BC_CMD_ID_SET_AI_CFG,
73
79
  BC_CMD_ID_SET_AUDIO_TASK,
80
+ BC_CMD_ID_SET_DING_DONG_CFG,
81
+ BC_CMD_ID_SET_DING_DONG_SILENT,
74
82
  BC_CMD_ID_SET_MOTION_ALARM,
75
83
  BC_CMD_ID_SET_PIR_INFO,
76
84
  BC_CMD_ID_SET_WHITE_LED_STATE,
@@ -136,7 +144,7 @@ import {
136
144
  talkTraceLog,
137
145
  traceLog,
138
146
  xmlEscape
139
- } from "./chunk-YPU7RAEY.js";
147
+ } from "./chunk-NLTB7GTA.js";
140
148
 
141
149
  // src/protocol/framing.ts
142
150
  function encodeHeader(h) {
@@ -1807,6 +1815,13 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1807
1815
  * and may leave device-side sessions in a bad state.
1808
1816
  */
1809
1817
  static coverPreviewQueueTail = /* @__PURE__ */ new Map();
1818
+ /**
1819
+ * Global CoverPreview backoff – increases on 400 rejection, resets on success.
1820
+ * Prevents flooding the camera when it's overwhelmed.
1821
+ */
1822
+ static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
1823
+ static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
1824
+ static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
1810
1825
  opts;
1811
1826
  debugCfg;
1812
1827
  logger;
@@ -1954,7 +1969,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1954
1969
  if (!this.isIdleDisconnectEnabled()) return false;
1955
1970
  if (!this.isSocketConnected()) return false;
1956
1971
  if (this.pending.size > 0) return false;
1957
- if (this.hasActiveVideoSubscriptionsInternal()) return false;
1972
+ if (this.isDeviceStreamingActive()) return false;
1958
1973
  if (this.permits.size > 0) return false;
1959
1974
  return true;
1960
1975
  }
@@ -1968,7 +1983,18 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1968
1983
  const delayMs = Math.max(0, timeoutMs - elapsedMs);
1969
1984
  this.idleDisconnectTimer = setTimeout(() => {
1970
1985
  try {
1971
- if (!this.isIdleDisconnectEligibleNow()) return;
1986
+ if (!this.isIdleDisconnectEligibleNow()) {
1987
+ this.logDebug("idle_disconnect_blocked", {
1988
+ reason: "not eligible",
1989
+ socketConnected: this.isSocketConnected(),
1990
+ pending: this.pending.size,
1991
+ deviceStreamingActive: this.isDeviceStreamingActive(),
1992
+ localVideoSubs: this.hasActiveVideoSubscriptionsInternal(),
1993
+ permits: this.permits.size,
1994
+ host: this.opts.host
1995
+ });
1996
+ return;
1997
+ }
1972
1998
  if (this.lastUserActivityAtMs == null) return;
1973
1999
  const elapsed2 = Date.now() - this.lastUserActivityAtMs;
1974
2000
  if (elapsed2 < timeoutMs) {
@@ -2082,7 +2108,34 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2082
2108
  async withSerializedCoverPreview(fn) {
2083
2109
  const key = this.getCoverPreviewQueueKey();
2084
2110
  const prevTail = _BaichuanClient.coverPreviewQueueTail.get(key) ?? Promise.resolve();
2085
- const run = prevTail.catch(() => void 0).then(fn);
2111
+ const run = prevTail.catch(() => void 0).then(async () => {
2112
+ const backoffMs = _BaichuanClient.coverPreviewBackoffMs.get(key) ?? 0;
2113
+ if (backoffMs > 0) {
2114
+ this.logDebug("coverpreview_backoff_wait", { backoffMs });
2115
+ await new Promise((r) => setTimeout(r, backoffMs));
2116
+ }
2117
+ try {
2118
+ const result = await fn();
2119
+ _BaichuanClient.coverPreviewBackoffMs.delete(key);
2120
+ return result;
2121
+ } catch (e) {
2122
+ const msg = e instanceof Error ? e.message : String(e);
2123
+ const is400 = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
2124
+ if (is400) {
2125
+ const current = _BaichuanClient.coverPreviewBackoffMs.get(key) ?? 0;
2126
+ const next = current === 0 ? _BaichuanClient.COVER_PREVIEW_INITIAL_BACKOFF_MS : Math.min(
2127
+ current * 2,
2128
+ _BaichuanClient.COVER_PREVIEW_MAX_BACKOFF_MS
2129
+ );
2130
+ _BaichuanClient.coverPreviewBackoffMs.set(key, next);
2131
+ this.logDebug("coverpreview_backoff_increased", {
2132
+ previous: current,
2133
+ next
2134
+ });
2135
+ }
2136
+ throw e;
2137
+ }
2138
+ });
2086
2139
  const tail = run.then(
2087
2140
  () => void 0,
2088
2141
  () => void 0
@@ -4404,32 +4457,36 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
4404
4457
  * Send CoverPreview command (cmd_id=298) to get an I-frame from a past recording.
4405
4458
  * Similar to sendBinarySnapshot109 but handles the stream header + frame format
4406
4459
  * instead of JPEG.
4460
+ *
4461
+ * Retry is minimal (2 attempts) – the global backoff in `withSerializedCoverPreview`
4462
+ * throttles subsequent requests when the camera is overwhelmed.
4463
+ * PCAP analysis shows the camera routinely rejects the first request with 400.
4407
4464
  */
4408
4465
  async sendBinaryCoverPreview(params) {
4409
4466
  return await this.withSerializedCoverPreview(async () => {
4410
- const maxRetries = params.maxRetries ?? 5;
4411
- const retryDelayMs = params.retryDelayMs ?? 1e3;
4467
+ const maxAttempts = 5;
4468
+ const retryDelay = 1500;
4412
4469
  let lastError;
4413
- for (let attempt = 0; attempt < maxRetries; attempt++) {
4470
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4414
4471
  try {
4415
4472
  return await this._sendBinaryCoverPreviewOnce(params);
4416
4473
  } catch (e) {
4417
4474
  const msg = e instanceof Error ? e.message : String(e);
4418
4475
  lastError = e instanceof Error ? e : new Error(msg);
4419
- const is400Rejection = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
4420
- if (is400Rejection && attempt < maxRetries - 1) {
4476
+ const is400 = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
4477
+ if (is400 && attempt < maxAttempts - 1) {
4421
4478
  this.logDebug("coverpreview_retry_400", {
4422
4479
  attempt: attempt + 1,
4423
- maxRetries,
4424
- retryDelayMs
4480
+ maxAttempts,
4481
+ retryDelay
4425
4482
  });
4426
- await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
4483
+ await new Promise((r) => setTimeout(r, retryDelay));
4427
4484
  continue;
4428
4485
  }
4429
4486
  throw lastError;
4430
4487
  }
4431
4488
  }
4432
- throw lastError ?? new Error("CoverPreview failed after all retries");
4489
+ throw lastError ?? new Error("CoverPreview failed after all attempts");
4433
4490
  });
4434
4491
  }
4435
4492
  /**
@@ -5343,6 +5400,8 @@ var NativeStreamFanout = class {
5343
5400
  } finally {
5344
5401
  for (const q of this.queues.values()) q.close();
5345
5402
  this.queues.clear();
5403
+ this.running = false;
5404
+ this.opts.onEnd?.();
5346
5405
  }
5347
5406
  })();
5348
5407
  }
@@ -5668,7 +5727,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5668
5727
  this.logger.warn(
5669
5728
  `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5670
5729
  );
5671
- this.streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
5730
+ this.streamMetadata = { frameRate: 25 };
5672
5731
  this.setFlowVideoType("H264", "metadata unavailable");
5673
5732
  }
5674
5733
  this.clientConnectionServer = net2.createServer((socket) => {
@@ -5700,7 +5759,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5700
5759
  */
5701
5760
  handleRtspConnection(socket) {
5702
5761
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
5703
- this.logger.info(`[BaichuanRtspServer] RTSP client connected: ${clientId}`);
5762
+ const connectTime = Date.now();
5763
+ this.logger.info(
5764
+ `[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
5765
+ );
5704
5766
  let sessionId = "";
5705
5767
  let buffer = Buffer.alloc(0);
5706
5768
  let clientFfmpeg;
@@ -5708,6 +5770,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5708
5770
  let clientUdpSocket = null;
5709
5771
  let clientUdpSocketAudio = null;
5710
5772
  const cleanup = () => {
5773
+ const sessionDurationMs = Date.now() - connectTime;
5774
+ const res = this.clientResources.get(clientId);
5775
+ const framesSent = res?.framesSent ?? 0;
5776
+ this.logger.info(
5777
+ `[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
5778
+ );
5711
5779
  this.removeClient(clientId);
5712
5780
  this.authNonces.delete(clientId);
5713
5781
  const resources = this.clientResources.get(clientId);
@@ -5849,7 +5917,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5849
5917
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
5850
5918
  });
5851
5919
  } else if (method === "DESCRIBE") {
5852
- if (!this.firstFrameReceived && this.connectedClients.size === 0) {
5920
+ if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
5853
5921
  try {
5854
5922
  if (!this.nativeStreamActive) {
5855
5923
  await this.startNativeStream();
@@ -5931,7 +5999,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5931
5999
  seenFirstVideoKeyframe: false,
5932
6000
  setupTrack0: false,
5933
6001
  setupTrack1: false,
5934
- isPlaying: false
6002
+ isPlaying: false,
6003
+ connectTime
5935
6004
  });
5936
6005
  } else {
5937
6006
  existing.rtspSocket = socket;
@@ -5978,8 +6047,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5978
6047
  if (resources) {
5979
6048
  if (isTrack1) resources.setupTrack1 = true;
5980
6049
  else resources.setupTrack0 = true;
5981
- this.rtspDebugLog(
5982
- `SETUP done for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
6050
+ const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
6051
+ const track = isTrack1 ? "track1(audio)" : "track0(video)";
6052
+ this.logger.info(
6053
+ `[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
5983
6054
  );
5984
6055
  }
5985
6056
  }
@@ -6004,8 +6075,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6004
6075
  const resources = this.clientResources.get(clientId);
6005
6076
  if (resources) {
6006
6077
  resources.isPlaying = true;
6007
- this.rtspDebugLog(
6008
- `PLAY for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
6078
+ const hasAudio = !!resources.setupTrack1;
6079
+ this.logger.info(
6080
+ `[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
6009
6081
  );
6010
6082
  }
6011
6083
  }
@@ -6014,6 +6086,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6014
6086
  Range: "npt=0.000-"
6015
6087
  });
6016
6088
  } else if (method === "TEARDOWN") {
6089
+ this.logger.info(
6090
+ `[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
6091
+ );
6017
6092
  cleanup();
6018
6093
  sendResponse(200, "OK", {
6019
6094
  Session: sessionId
@@ -6108,7 +6183,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6108
6183
  this.logger.warn(
6109
6184
  `[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
6110
6185
  );
6111
- streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
6186
+ streamMetadata = { frameRate: 25 };
6112
6187
  }
6113
6188
  }
6114
6189
  const ffmpegFormat = this.flow.ffmpegFormat;
@@ -6696,15 +6771,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6696
6771
  `Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
6697
6772
  );
6698
6773
  }
6699
- const now = Date.now();
6700
- const timeSinceLastFrame = now - lastFrameTime;
6701
- const waitTime = targetFrameInterval - timeSinceLastFrame;
6702
- if (waitTime > 0) {
6703
- await new Promise(
6704
- (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
6705
- );
6774
+ if (!useDirectRtp) {
6775
+ const now = Date.now();
6776
+ const timeSinceLastFrame = now - lastFrameTime;
6777
+ const waitTime = targetFrameInterval - timeSinceLastFrame;
6778
+ if (waitTime > 0) {
6779
+ await new Promise(
6780
+ (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
6781
+ );
6782
+ }
6783
+ lastFrameTime = Date.now();
6706
6784
  }
6707
- lastFrameTime = Date.now();
6708
6785
  if (useDirectRtp) {
6709
6786
  const videoType = frame.videoType ?? this.flow.videoType;
6710
6787
  const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
@@ -6777,6 +6854,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6777
6854
  }
6778
6855
  if (!firstVideoWriteLogged) {
6779
6856
  firstVideoWriteLogged = true;
6857
+ const clientConnectTime = resources?.connectTime ?? Date.now();
6858
+ const ttffMs = Date.now() - clientConnectTime;
6859
+ this.logger.info(
6860
+ `[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
6861
+ );
6780
6862
  if (rtspDebug) {
6781
6863
  const headHex = frame.data.subarray(0, 16).toString("hex");
6782
6864
  rtspDebugLog(
@@ -6784,6 +6866,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6784
6866
  );
6785
6867
  }
6786
6868
  }
6869
+ if (resources) {
6870
+ resources.framesSent = (resources.framesSent ?? 0) + 1;
6871
+ }
6787
6872
  sendVideoAccessUnit(videoType, normalizedVideoData, true);
6788
6873
  } else {
6789
6874
  try {
@@ -6868,8 +6953,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6868
6953
  this.firstAudioPromise = new Promise((resolve) => {
6869
6954
  this.firstAudioResolve = resolve;
6870
6955
  });
6871
- this.rtspDebugLog(
6872
- `Starting native stream for profile ${this.profile} (waiting for camera to start transmitting...)`
6956
+ this.logger.info(
6957
+ `[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
6873
6958
  );
6874
6959
  await this.flow.startKeepAlive(this.api);
6875
6960
  this.nativeFanout = new NativeStreamFanout({
@@ -6912,6 +6997,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6912
6997
  this.logger.warn(
6913
6998
  `[BaichuanRtspServer] Shared native stream error: ${error}`
6914
6999
  );
7000
+ },
7001
+ onEnd: () => {
7002
+ if (!this.nativeStreamActive) return;
7003
+ this.nativeStreamActive = false;
7004
+ this.firstFrameReceived = false;
7005
+ this.firstFramePromise = null;
7006
+ this.firstFrameResolve = null;
7007
+ this.nativeFanout = null;
7008
+ this.logger.info(
7009
+ `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
7010
+ );
7011
+ if (this.connectedClients.size > 0) {
7012
+ this.logger.info(
7013
+ `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
7014
+ );
7015
+ setImmediate(() => void this.startNativeStream());
7016
+ }
6915
7017
  }
6916
7018
  });
6917
7019
  this.nativeFanout.start();
@@ -6950,7 +7052,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6950
7052
  if (!this.nativeStreamActive) {
6951
7053
  return;
6952
7054
  }
6953
- this.rtspDebugLog(`Stopping native stream`);
7055
+ this.logger.info(
7056
+ `[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
7057
+ );
6954
7058
  this.flow.stopKeepAlive();
6955
7059
  this.clearNoClientAutoStopTimer();
6956
7060
  this.nativeStreamActive = false;
@@ -6984,9 +7088,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6984
7088
  if (this.connectedClients.has(clientId)) {
6985
7089
  this.connectedClients.delete(clientId);
6986
7090
  this.emit("clientDisconnected", clientId);
6987
- this.logger.info(
6988
- `[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
6989
- );
6990
7091
  if (this.connectedClients.size === 0) {
6991
7092
  void this.stopNativeStream();
6992
7093
  }
@@ -7219,10 +7320,12 @@ function parseSupportXml(xml) {
7219
7320
  }
7220
7321
  function getSupportItemForChannel(support, channel) {
7221
7322
  if (!support?.items?.length) return void 0;
7222
- const scoreSupportItem = (item) => {
7323
+ const candidates = support.items.filter((i) => i.chnID === channel);
7324
+ if (!candidates.length) return void 0;
7325
+ const score = (item) => {
7223
7326
  const anyItem = item;
7224
- let score = 0;
7225
- if (anyItem.name == null) score += 2;
7327
+ let result = 0;
7328
+ if (anyItem.name == null) result += 100;
7226
7329
  const capabilityKeys = [
7227
7330
  "ptzType",
7228
7331
  "ptzControl",
@@ -7234,20 +7337,17 @@ function getSupportItemForChannel(support, channel) {
7234
7337
  "motion",
7235
7338
  "encCtrl",
7236
7339
  "newIspCfg",
7237
- "remoteAbility"
7340
+ "remoteAbility",
7341
+ "aitype",
7342
+ "videoClip",
7343
+ "snap"
7238
7344
  ];
7239
7345
  for (const k of capabilityKeys) {
7240
- if (anyItem[k] !== void 0) score += 3;
7346
+ if (anyItem[k] !== void 0) result += 3;
7241
7347
  }
7242
- score += Math.min(10, Math.max(0, Object.keys(anyItem).length - 1));
7243
- return score;
7244
- };
7245
- const pickBest = (chnId) => {
7246
- const candidates = support.items.filter((i) => i.chnID === chnId);
7247
- if (!candidates.length) return void 0;
7248
- return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
7348
+ return result;
7249
7349
  };
7250
- return pickBest(channel);
7350
+ return candidates.sort((a, b) => score(b) - score(a))[0];
7251
7351
  }
7252
7352
  function computeDeviceCapabilities(params) {
7253
7353
  const { channel } = params;
@@ -7279,6 +7379,7 @@ function computeDeviceCapabilities(params) {
7279
7379
  flat,
7280
7380
  /white\s*led|whiteLed|flood\s*light|floodlight/i
7281
7381
  );
7382
+ const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
7282
7383
  const hasSirenFromAbilities = abilitiesHasAny(
7283
7384
  flat,
7284
7385
  /audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
@@ -7291,6 +7392,9 @@ function computeDeviceCapabilities(params) {
7291
7392
  const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
7292
7393
  const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
7293
7394
  const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
7395
+ const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
7396
+ const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
7397
+ const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
7294
7398
  const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
7295
7399
  const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
7296
7400
  const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
@@ -7306,14 +7410,15 @@ function computeDeviceCapabilities(params) {
7306
7410
  hasZoom: finalHasZoom,
7307
7411
  hasPresets: finalHasPresets,
7308
7412
  hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
7309
- hasBattery: hasBatteryFromSupport || hasBatteryFromAbilities,
7413
+ hasBattery,
7310
7414
  hasIntercom: hasIntercomFromSupport,
7311
- hasSiren: hasSirenFromAbilities,
7415
+ hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
7312
7416
  // lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
7313
7417
  hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
7314
7418
  hasPir: hasPirFromAbilities || hasPirFromSupport,
7315
- isDoorbell: isDoorbellFromSupport || isDoorbellFromModel,
7316
- hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
7419
+ isDoorbell,
7420
+ hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
7421
+ hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
7317
7422
  };
7318
7423
  if (ptzMode !== void 0) result.ptzMode = ptzMode;
7319
7424
  return result;
@@ -9188,6 +9293,161 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
9188
9293
  return extractReolinkUidLike(p2pXml);
9189
9294
  };
9190
9295
 
9296
+ // src/reolink/baichuan/utils/chime.ts
9297
+ var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
9298
+ <body>
9299
+ <dingdongDeviceOpt version="1.1">
9300
+ <id>${chimeId}</id>
9301
+ <opt>getParam</opt>
9302
+ </dingdongDeviceOpt>
9303
+ </body>`;
9304
+ var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
9305
+ <body>
9306
+ <dingdongDeviceOpt version="1.1">
9307
+ <opt>setParam</opt>
9308
+ <id>${chimeId}</id>
9309
+ ${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
9310
+ ${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
9311
+ ${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
9312
+ </dingdongDeviceOpt>
9313
+ </body>`;
9314
+ var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
9315
+ <body>
9316
+ <dingdongDeviceOpt version="1.1">
9317
+ <id>${chimeId}</id>
9318
+ <opt>ringWithMusic</opt>
9319
+ <musicId>${musicId}</musicId>
9320
+ </dingdongDeviceOpt>
9321
+ </body>`;
9322
+ var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
9323
+ <body>
9324
+ <dingdongCfg version="1.1">
9325
+ <deviceCfg>
9326
+ <id>${chimeId}</id>
9327
+ <alarminCfg>
9328
+ <valid>${state}</valid>
9329
+ <musicId>${musicId}</musicId>
9330
+ <type>${eventType}</type>
9331
+ </alarminCfg>
9332
+ </deviceCfg>
9333
+ </dingdongCfg>
9334
+ </body>`;
9335
+ var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
9336
+ <body>
9337
+ <dingdongCtrl version="1.1">
9338
+ <opt>machineStateGet</opt>
9339
+ </dingdongCtrl>
9340
+ </body>`;
9341
+ var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
9342
+ <body>
9343
+ <dingdongCtrl version="1.1">
9344
+ <opt>machineStateSet</opt>
9345
+ <type>${chimeType}</type>
9346
+ <bopen>${enabled}</bopen>
9347
+ <bsave>1</bsave>
9348
+ <time>${time}</time>
9349
+ </dingdongCtrl>
9350
+ </body>`;
9351
+ var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
9352
+ <body>
9353
+ <audioFileInfo version="1.1">
9354
+ <channelId>${channel}</channelId>
9355
+ <id>${fileId}</id>
9356
+ <timeout>0</timeout>
9357
+ </audioFileInfo>
9358
+ </body>`;
9359
+ var parseDingDongListFromXml = (xml) => {
9360
+ const devices = [];
9361
+ const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
9362
+ for (const block of blocks) {
9363
+ const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
9364
+ const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
9365
+ const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
9366
+ if (idText === void 0) continue;
9367
+ const id = Number(idText);
9368
+ if (!Number.isFinite(id)) continue;
9369
+ devices.push({
9370
+ id,
9371
+ name,
9372
+ netState: netStateText !== void 0 ? Number(netStateText) : 0
9373
+ });
9374
+ }
9375
+ return devices;
9376
+ };
9377
+ var parseDingDongParamsFromXml = (xml) => {
9378
+ const name = getXmlText(xml, "name");
9379
+ const volLevelText = getXmlText(xml, "volLevel");
9380
+ const ledStateText = getXmlText(xml, "ledState");
9381
+ const result = {};
9382
+ if (name !== void 0) result.name = name;
9383
+ if (volLevelText !== void 0) {
9384
+ const n = Number(volLevelText);
9385
+ if (Number.isFinite(n)) result.volLevel = n;
9386
+ }
9387
+ if (ledStateText !== void 0) {
9388
+ const n = Number(ledStateText);
9389
+ if (Number.isFinite(n)) result.ledState = n;
9390
+ }
9391
+ return result;
9392
+ };
9393
+ var parseDingDongCfgFromXml = (xml) => {
9394
+ const configs = [];
9395
+ const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
9396
+ for (const deviceBlock of deviceBlocks) {
9397
+ const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
9398
+ if (idText === void 0) continue;
9399
+ const id = Number(idText);
9400
+ if (!Number.isFinite(id)) continue;
9401
+ const typeMap = {};
9402
+ const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
9403
+ for (const alarmBlock of alarmBlocks) {
9404
+ const type = getXmlText(alarmBlock, "type");
9405
+ if (!type) continue;
9406
+ const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
9407
+ const musicIdText = getXmlText(alarmBlock, "musicId");
9408
+ typeMap[type] = {
9409
+ valid: validText !== void 0 ? Number(validText) : 0,
9410
+ musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
9411
+ };
9412
+ }
9413
+ configs.push({ id, type: typeMap });
9414
+ }
9415
+ return configs;
9416
+ };
9417
+ var parseHardwiredChimeFromXml = (xml) => {
9418
+ const type = getXmlText(xml, "type") ?? "";
9419
+ const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
9420
+ const timeText = getXmlText(xml, "time");
9421
+ return {
9422
+ type,
9423
+ enabled: bopenText === "1",
9424
+ time: timeText !== void 0 ? Number(timeText) : 0
9425
+ };
9426
+ };
9427
+ var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
9428
+ <body>
9429
+ <dingdongSilentMode version="1.1">
9430
+ <id>${chimeId}</id>
9431
+ </dingdongSilentMode>
9432
+ </body>`;
9433
+ var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
9434
+ <body>
9435
+ <dingdongSilentMode version="1.1">
9436
+ <id>${chimeId}</id>
9437
+ <time>${time}</time>
9438
+ <type>63</type>
9439
+ </dingdongSilentMode>
9440
+ </body>`;
9441
+ var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
9442
+ const timeText = getXmlText(xml, "time");
9443
+ const time = timeText !== void 0 ? Number(timeText) : 0;
9444
+ return {
9445
+ id: chimeId,
9446
+ time,
9447
+ active: time === 0
9448
+ };
9449
+ };
9450
+
9191
9451
  // src/reolink/baichuan/utils/eventsGetEvents.ts
9192
9452
  var parseAiTypeToken = (aiTypeRaw) => {
9193
9453
  const raw = (aiTypeRaw ?? "").trim();
@@ -9497,6 +9757,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9497
9757
  host;
9498
9758
  username;
9499
9759
  password;
9760
+ /**
9761
+ * Set to `true` after `close()` is called.
9762
+ * Once closed, the API instance should not be reused.
9763
+ */
9764
+ _closed = false;
9500
9765
  // ─────────────────────────────────────────────────────────────────────────────
9501
9766
  // SOCKET POOL - Tag-based socket management
9502
9767
  // ─────────────────────────────────────────────────────────────────────────────
@@ -9526,10 +9791,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9526
9791
  get client() {
9527
9792
  const entry = this.socketPool.get("general");
9528
9793
  if (!entry) {
9794
+ if (this._closed) {
9795
+ throw new Error(
9796
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
9797
+ );
9798
+ }
9529
9799
  throw new Error("[ReolinkBaichuanApi] General socket not initialized");
9530
9800
  }
9531
9801
  return entry.client;
9532
9802
  }
9803
+ /**
9804
+ * `true` after `close()` has been called. A closed API should not be reused;
9805
+ * the consumer should create a new instance.
9806
+ */
9807
+ get isClosed() {
9808
+ return this._closed;
9809
+ }
9810
+ /**
9811
+ * `true` when the API is usable: not closed, general socket exists, socket
9812
+ * is connected and the client is logged in.
9813
+ *
9814
+ * This is the recommended way for consumers to check whether the API is
9815
+ * still valid before issuing commands, instead of directly accessing
9816
+ * `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
9817
+ * if the socket pool was already destroyed).
9818
+ */
9819
+ get isReady() {
9820
+ if (this._closed) return false;
9821
+ const entry = this.socketPool.get("general");
9822
+ if (!entry) return false;
9823
+ try {
9824
+ return entry.client.isSocketConnected() && entry.client.loggedIn;
9825
+ } catch {
9826
+ return false;
9827
+ }
9828
+ }
9829
+ /** Promise tracking an in-flight reconnection from `ensureConnected()`. */
9830
+ _ensureConnectedPromise;
9831
+ /**
9832
+ * Ensure the "general" socket is connected and logged in.
9833
+ * If the socket is disconnected or the pool entry was destroyed, a new
9834
+ * general socket is created, logged in, and all event/push/guard listeners
9835
+ * are re-attached automatically.
9836
+ *
9837
+ * This is a **no-op** when the API is already {@link isReady}.
9838
+ *
9839
+ * @throws If `close()` was called — the API is permanently closed and a new
9840
+ * instance must be created.
9841
+ */
9842
+ async ensureConnected() {
9843
+ if (this._closed) {
9844
+ throw new Error(
9845
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
9846
+ );
9847
+ }
9848
+ if (this.isReady) return;
9849
+ if (this._ensureConnectedPromise) {
9850
+ return this._ensureConnectedPromise;
9851
+ }
9852
+ this._ensureConnectedPromise = this.reconnectGeneralSocket();
9853
+ try {
9854
+ await this._ensureConnectedPromise;
9855
+ } finally {
9856
+ this._ensureConnectedPromise = void 0;
9857
+ }
9858
+ }
9859
+ /**
9860
+ * Internal: destroy the current general socket (if any), create a new one,
9861
+ * login, and re-attach all listeners.
9862
+ */
9863
+ async reconnectGeneralSocket() {
9864
+ const oldEntry = this.socketPool.get("general");
9865
+ if (oldEntry) {
9866
+ oldEntry.client.removeAllListeners();
9867
+ if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
9868
+ if (oldEntry.generalPermitRelease) {
9869
+ try {
9870
+ oldEntry.generalPermitRelease();
9871
+ } catch {
9872
+ }
9873
+ }
9874
+ this.socketPool.delete("general");
9875
+ try {
9876
+ await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
9877
+ } catch {
9878
+ }
9879
+ }
9880
+ const newClient = new BaichuanClient(this.clientOptions);
9881
+ this.socketPool.set("general", {
9882
+ client: newClient,
9883
+ refCount: 1,
9884
+ // general socket is always "in use"
9885
+ createdAt: Date.now(),
9886
+ lastUsedAt: Date.now(),
9887
+ idleCloseTimer: void 0,
9888
+ generalPermitRelease: void 0
9889
+ });
9890
+ this.setupGeneralClientListeners();
9891
+ await this.client.login();
9892
+ this.logger.log?.(
9893
+ "[ReolinkBaichuanApi] General socket reconnected successfully"
9894
+ );
9895
+ if (this.simpleEventListeners.size > 0) {
9896
+ this.simpleEventSubscribed = false;
9897
+ this.simpleEventWatchdogRecoveryAttempts = 0;
9898
+ this.simpleEventWatchdogLastRecoveryAt = 0;
9899
+ try {
9900
+ await this.ensureSimpleEventSubscribed();
9901
+ this.simpleEventLastReceivedAt = Date.now();
9902
+ this.logger.log?.(
9903
+ `[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
9904
+ );
9905
+ } catch (e) {
9906
+ (this.logger.debug ?? this.logger.log).call(
9907
+ this.logger,
9908
+ `[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
9909
+ formatErrorForLog(e)
9910
+ );
9911
+ }
9912
+ }
9913
+ }
9914
+ /**
9915
+ * Attach event, push, channelInfo, and guard listeners to the current
9916
+ * "general" client. Called from the constructor and from
9917
+ * {@link reconnectGeneralSocket}.
9918
+ */
9919
+ setupGeneralClientListeners() {
9920
+ const client = this.client;
9921
+ client.on("event", (event) => {
9922
+ const mapped = mapToSimpleEvent(event);
9923
+ if (!mapped) return;
9924
+ this.dispatchSimpleEvent(mapped);
9925
+ });
9926
+ client.on("channelInfo", (xml) => {
9927
+ try {
9928
+ this.parseAndStoreChannelInfo(xml);
9929
+ } catch (e) {
9930
+ this.logger.warn?.(
9931
+ "[ReolinkBaichuanApi] Error parsing channel info from push",
9932
+ formatErrorForLog(e)
9933
+ );
9934
+ }
9935
+ });
9936
+ client.on("push", (frame) => {
9937
+ const cmdId = frame.header.cmdId;
9938
+ if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
9939
+ return;
9940
+ }
9941
+ try {
9942
+ if (frame.body.length === 0) return;
9943
+ const xml = client.tryDecryptXml(
9944
+ frame.body,
9945
+ frame.header.channelId,
9946
+ client.enc
9947
+ );
9948
+ if (!xml || !xml.startsWith("<?xml")) return;
9949
+ this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
9950
+ } catch (e) {
9951
+ this.logger.debug?.(
9952
+ "[ReolinkBaichuanApi] Error parsing settings push",
9953
+ formatErrorForLog(e)
9954
+ );
9955
+ }
9956
+ });
9957
+ if (this.rebootAfterDisconnectionsPerMinute > 0) {
9958
+ client.on("close", () => {
9959
+ try {
9960
+ void this.maybeRebootOnDisconnectStorm();
9961
+ } catch {
9962
+ }
9963
+ });
9964
+ }
9965
+ if (this.rebootAfterConsecutiveEconnreset > 0) {
9966
+ client.on("close", () => {
9967
+ try {
9968
+ void this.maybeRebootOnEconnresetStorm();
9969
+ } catch {
9970
+ }
9971
+ });
9972
+ }
9973
+ if (!this.sessionGuardIntervalTimer) {
9974
+ client.once("push", () => {
9975
+ void this.logActiveSessionsOnStartup();
9976
+ this.sessionGuardIntervalTimer = setInterval(() => {
9977
+ void this.maybeRebootOnTooManySessions();
9978
+ }, 6e4);
9979
+ });
9980
+ }
9981
+ }
9533
9982
  /**
9534
9983
  * Cached camera UID. May be initially undefined if not provided in the constructor.
9535
9984
  * Will be lazily populated on demand when needed (e.g. for recordings).
@@ -10104,6 +10553,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10104
10553
  `[SocketPool] Closing existing socket for tag=${tag} (recreating)`
10105
10554
  );
10106
10555
  this.socketPool.delete(tag);
10556
+ if (existing.generalPermitRelease) {
10557
+ try {
10558
+ existing.generalPermitRelease();
10559
+ } catch {
10560
+ }
10561
+ existing.generalPermitRelease = void 0;
10562
+ }
10107
10563
  try {
10108
10564
  await existing.client.close({
10109
10565
  reason: "socket pool recreation",
@@ -10122,7 +10578,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10122
10578
  refCount: 0,
10123
10579
  createdAt: now,
10124
10580
  lastUsedAt: now,
10125
- idleCloseTimer: void 0
10581
+ idleCloseTimer: void 0,
10582
+ generalPermitRelease: void 0
10126
10583
  };
10127
10584
  entry.pendingPromise = (async () => {
10128
10585
  try {
@@ -10140,6 +10597,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10140
10597
  entry.lastUsedAt = Date.now();
10141
10598
  delete entry.pendingPromise;
10142
10599
  log?.log?.(`[SocketPool] Socket connected for tag=${tag}`);
10600
+ if (tag !== "general") {
10601
+ try {
10602
+ const generalEntry = this.socketPool.get("general");
10603
+ if (generalEntry?.client) {
10604
+ entry.generalPermitRelease = generalEntry.client.acquirePermit(
10605
+ 0,
10606
+ // indefinite — released when the streaming socket closes
10607
+ `streaming-peer:${tag}`
10608
+ );
10609
+ }
10610
+ } catch {
10611
+ }
10612
+ }
10143
10613
  void this.maybeRebootOnTooManySessions();
10144
10614
  return newClient;
10145
10615
  } catch (loginError) {
@@ -10208,6 +10678,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10208
10678
  if (!current) return;
10209
10679
  if (current.refCount > 0) return;
10210
10680
  this.socketPool.delete(tag);
10681
+ if (current.generalPermitRelease) {
10682
+ try {
10683
+ current.generalPermitRelease();
10684
+ } catch {
10685
+ }
10686
+ current.generalPermitRelease = void 0;
10687
+ }
10211
10688
  log?.log?.(`[SocketPool] Closing idle streaming socket for tag=${tag}`);
10212
10689
  try {
10213
10690
  await current.client.close({
@@ -10262,6 +10739,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10262
10739
  clearTimeout(entry.idleCloseTimer);
10263
10740
  entry.idleCloseTimer = void 0;
10264
10741
  }
10742
+ if (entry.generalPermitRelease) {
10743
+ try {
10744
+ entry.generalPermitRelease();
10745
+ } catch {
10746
+ }
10747
+ entry.generalPermitRelease = void 0;
10748
+ }
10265
10749
  log?.debug?.(`[SocketPool] Force-closing socket for tag=${tag}`);
10266
10750
  this.socketPool.delete(tag);
10267
10751
  try {
@@ -10287,6 +10771,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10287
10771
  if (entry.idleCloseTimer) {
10288
10772
  clearTimeout(entry.idleCloseTimer);
10289
10773
  }
10774
+ if (entry.generalPermitRelease) {
10775
+ try {
10776
+ entry.generalPermitRelease();
10777
+ } catch {
10778
+ }
10779
+ entry.generalPermitRelease = void 0;
10780
+ }
10290
10781
  this.logger?.debug?.(`[SocketPool] Cleanup: closing tag=${tag}`);
10291
10782
  await entry.client.close({ reason: "API cleanup", skipLogout: true });
10292
10783
  } catch {
@@ -10392,7 +10883,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10392
10883
  password: opts.password,
10393
10884
  ...opts.logger ? { logger: opts.logger } : {},
10394
10885
  ...opts.debugOptions ? { debugOptions: opts.debugOptions } : {},
10395
- ...opts.uid ? { uid: opts.uid } : {}
10886
+ ...opts.uid ? { uid: opts.uid } : {},
10887
+ ...opts.transport ? { transport: opts.transport } : {},
10888
+ ...opts.port !== void 0 ? { port: opts.port } : {},
10889
+ ...opts.udpDiscoveryMethod ? { udpDiscoveryMethod: opts.udpDiscoveryMethod } : {},
10890
+ ...opts.idleDisconnect !== void 0 ? { idleDisconnect: opts.idleDisconnect } : {},
10891
+ ...opts.idleDisconnectTimeoutMs !== void 0 ? { idleDisconnectTimeoutMs: opts.idleDisconnectTimeoutMs } : {},
10892
+ ...opts.channel !== void 0 ? { channel: opts.channel } : {}
10396
10893
  };
10397
10894
  const generalClient = new BaichuanClient(opts);
10398
10895
  this.socketPool.set("general", {
@@ -10401,7 +10898,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10401
10898
  // Always keep general socket "in use"
10402
10899
  createdAt: Date.now(),
10403
10900
  lastUsedAt: Date.now(),
10404
- idleCloseTimer: void 0
10901
+ idleCloseTimer: void 0,
10902
+ generalPermitRelease: void 0
10405
10903
  });
10406
10904
  this.host = opts.host;
10407
10905
  this.username = opts.username;
@@ -10421,42 +10919,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10421
10919
  logger: this.logger,
10422
10920
  debugConfig: generalClient.getDebugConfig?.()
10423
10921
  });
10424
- this.client.on("event", (event) => {
10425
- const mapped = mapToSimpleEvent(event);
10426
- if (!mapped) return;
10427
- this.dispatchSimpleEvent(mapped);
10428
- });
10429
- this.client.on("channelInfo", (xml) => {
10430
- try {
10431
- this.parseAndStoreChannelInfo(xml);
10432
- } catch (e) {
10433
- this.logger.warn?.(
10434
- "[ReolinkBaichuanApi] Error parsing channel info from push",
10435
- formatErrorForLog(e)
10436
- );
10437
- }
10438
- });
10439
- this.client.on("push", (frame) => {
10440
- const cmdId = frame.header.cmdId;
10441
- if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
10442
- return;
10443
- }
10444
- try {
10445
- if (frame.body.length === 0) return;
10446
- const xml = this.client.tryDecryptXml(
10447
- frame.body,
10448
- frame.header.channelId,
10449
- this.client.enc
10450
- );
10451
- if (!xml || !xml.startsWith("<?xml")) return;
10452
- this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
10453
- } catch (e) {
10454
- this.logger.debug?.(
10455
- "[ReolinkBaichuanApi] Error parsing settings push",
10456
- formatErrorForLog(e)
10457
- );
10458
- }
10459
- });
10460
10922
  const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
10461
10923
  if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
10462
10924
  this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
@@ -10465,32 +10927,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10465
10927
  if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
10466
10928
  this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
10467
10929
  }
10468
- if (this.rebootAfterDisconnectionsPerMinute > 0) {
10469
- this.client.on("close", () => {
10470
- try {
10471
- void this.maybeRebootOnDisconnectStorm();
10472
- } catch {
10473
- }
10474
- });
10475
- }
10476
10930
  const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
10477
10931
  if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
10478
10932
  this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
10479
10933
  }
10480
- if (this.rebootAfterConsecutiveEconnreset > 0) {
10481
- this.client.on("close", () => {
10482
- try {
10483
- void this.maybeRebootOnEconnresetStorm();
10484
- } catch {
10485
- }
10486
- });
10487
- }
10488
- this.client.once("push", () => {
10489
- void this.logActiveSessionsOnStartup();
10490
- this.sessionGuardIntervalTimer = setInterval(() => {
10491
- void this.maybeRebootOnTooManySessions();
10492
- }, 6e4);
10493
- });
10934
+ this.setupGeneralClientListeners();
10494
10935
  }
10495
10936
  /**
10496
10937
  * CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
@@ -10904,7 +11345,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10904
11345
  */
10905
11346
  async onSimpleEvent(callback) {
10906
11347
  this.simpleEventListeners.add(callback);
10907
- await this.ensureSimpleEventSubscribed();
11348
+ this.logger.debug?.(
11349
+ `[ReolinkBaichuanApi] onSimpleEvent: registering listener (total=${this.simpleEventListeners.size})`
11350
+ );
11351
+ try {
11352
+ await this.ensureSimpleEventSubscribed();
11353
+ this.logger.debug?.(
11354
+ `[ReolinkBaichuanApi] onSimpleEvent: initial subscribe succeeded, simpleEventSubscribed=${this.simpleEventSubscribed}`
11355
+ );
11356
+ } catch (e) {
11357
+ (this.logger.debug ?? this.logger.log).call(
11358
+ this.logger,
11359
+ `[ReolinkBaichuanApi] onSimpleEvent: initial subscribe failed, simpleEventSubscribed=${this.simpleEventSubscribed}, watchdog will retry`,
11360
+ formatErrorForLog(e)
11361
+ );
11362
+ }
10908
11363
  this.simpleEventLastReceivedAt = Date.now();
10909
11364
  this.startSimpleEventResubscribeTimer();
10910
11365
  this.startSimpleEventWatchdog();
@@ -10925,11 +11380,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10925
11380
  this.stopUdpSleepInference();
10926
11381
  await this.ensureSimpleEventUnsubscribed();
10927
11382
  } else {
10928
- const isUdp = this.client.getTransport?.() === "udp";
10929
- if (isUdp) {
10930
- this.startUdpSleepInference();
10931
- } else if (this.client.isStatePollingEnabled?.()) {
10932
- this.startStatePolling();
11383
+ const generalEntry = this.socketPool.get("general");
11384
+ if (generalEntry) {
11385
+ const isUdp = generalEntry.client.getTransport?.() === "udp";
11386
+ if (isUdp) {
11387
+ this.startUdpSleepInference();
11388
+ } else if (generalEntry.client.isStatePollingEnabled?.()) {
11389
+ this.startStatePolling();
11390
+ }
10933
11391
  }
10934
11392
  }
10935
11393
  }
@@ -10971,8 +11429,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10971
11429
  }
10972
11430
  async simpleEventWatchdogTick() {
10973
11431
  if (this.simpleEventListeners.size === 0) return;
10974
- if (!this.client.isSocketConnected?.() || !this.client.loggedIn) return;
11432
+ const generalEntry = this.socketPool.get("general");
11433
+ if (!generalEntry) return;
11434
+ if (!generalEntry.client.isSocketConnected?.() || !generalEntry.client.loggedIn) {
11435
+ this.logger.debug?.(
11436
+ `[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
11437
+ );
11438
+ return;
11439
+ }
10975
11440
  const now = Date.now();
11441
+ const sinceLastEvent = this.simpleEventLastReceivedAt > 0 ? now - this.simpleEventLastReceivedAt : -1;
11442
+ this.logger.debug?.(
11443
+ `[ReolinkBaichuanApi] event watchdog tick: subscribed=${this.simpleEventSubscribed} clientSubscribed=${generalEntry.client.subscribed} lastEventAgoMs=${sinceLastEvent} recoveryAttempts=${this.simpleEventWatchdogRecoveryAttempts} listeners=${this.simpleEventListeners.size}`
11444
+ );
10976
11445
  if (this.simpleEventSubscribed && this.simpleEventLastReceivedAt > 0) {
10977
11446
  const silence = now - this.simpleEventLastReceivedAt;
10978
11447
  if (silence < this.simpleEventWatchdogSilenceThresholdMs) return;
@@ -10983,7 +11452,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10983
11452
  );
10984
11453
  try {
10985
11454
  this.simpleEventSubscribed = false;
10986
- this.client.subscribed = false;
11455
+ generalEntry.client.subscribed = false;
10987
11456
  await this.ensureSimpleEventSubscribed();
10988
11457
  this.simpleEventLastReceivedAt = Date.now();
10989
11458
  this.simpleEventWatchdogRecoveryAttempts = 0;
@@ -11001,6 +11470,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11001
11470
  return;
11002
11471
  }
11003
11472
  if (!this.simpleEventSubscribed) {
11473
+ if (this.simpleEventLastReceivedAt > 0) {
11474
+ const sinceLastEvent2 = now - this.simpleEventLastReceivedAt;
11475
+ if (sinceLastEvent2 < this.simpleEventWatchdogSilenceThresholdMs) {
11476
+ this.simpleEventSubscribed = true;
11477
+ this.logger.debug?.(
11478
+ `[ReolinkBaichuanApi] event watchdog: events flowing (lastEventAgo=${Math.round(sinceLastEvent2 / 1e3)}s) despite simpleEventSubscribed=false, marking subscription as active (recoveryAttempts=${this.simpleEventWatchdogRecoveryAttempts})`
11479
+ );
11480
+ if (this.simpleEventWatchdogRecoveryAttempts > 0) {
11481
+ (this.logger.info ?? this.logger.log).call(
11482
+ this.logger,
11483
+ `[ReolinkBaichuanApi] event watchdog: events flowing despite failed subscribe, marking subscription active`
11484
+ );
11485
+ this.simpleEventWatchdogRecoveryAttempts = 0;
11486
+ }
11487
+ return;
11488
+ } else {
11489
+ this.logger.debug?.(
11490
+ `[ReolinkBaichuanApi] event watchdog: events stale (lastEventAgo=${Math.round(sinceLastEvent2 / 1e3)}s, threshold=${Math.round(this.simpleEventWatchdogSilenceThresholdMs / 1e3)}s), proceeding with recovery`
11491
+ );
11492
+ }
11493
+ } else {
11494
+ this.logger.debug?.(
11495
+ `[ReolinkBaichuanApi] event watchdog: no events ever received (simpleEventLastReceivedAt=0), proceeding with recovery`
11496
+ );
11497
+ }
11004
11498
  const backoffMs = Math.min(
11005
11499
  3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
11006
11500
  this.simpleEventWatchdogSilenceThresholdMs
@@ -11064,20 +11558,51 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11064
11558
  return await this.simpleEventResubscribeInFlight;
11065
11559
  }
11066
11560
  async ensureSimpleEventSubscribed() {
11067
- if (this.simpleEventListeners.size === 0) return;
11068
- if (this.simpleEventSubscribed) return;
11069
- if (this.simpleEventSubscribeInFlight)
11561
+ if (this.simpleEventListeners.size === 0) {
11562
+ this.logger.debug?.(
11563
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: no listeners, skipping`
11564
+ );
11565
+ return;
11566
+ }
11567
+ if (this.simpleEventSubscribed) {
11568
+ this.logger.debug?.(
11569
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: already subscribed, skipping`
11570
+ );
11571
+ return;
11572
+ }
11573
+ if (this.simpleEventSubscribeInFlight) {
11574
+ this.logger.debug?.(
11575
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: subscribe already in-flight, awaiting`
11576
+ );
11070
11577
  return await this.simpleEventSubscribeInFlight;
11578
+ }
11579
+ this.logger.debug?.(
11580
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: starting subscribe (clientSubscribed=${this.socketPool.get("general")?.client.subscribed})`
11581
+ );
11071
11582
  this.simpleEventSubscribeInFlight = (async () => {
11072
- if (!this.client.subscribed) {
11583
+ const entry = this.socketPool.get("general");
11584
+ if (!entry) {
11585
+ this.logger.debug?.(
11586
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: no general socket, bailing out`
11587
+ );
11588
+ return;
11589
+ }
11590
+ if (!entry.client.subscribed) {
11591
+ this.logger.debug?.(
11592
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: client.subscribed=false, calling subscribeEvents()`
11593
+ );
11073
11594
  await this.subscribeEvents();
11595
+ } else {
11596
+ this.logger.debug?.(
11597
+ `[ReolinkBaichuanApi] ensureSimpleEventSubscribed: client already subscribed, skipping subscribeEvents()`
11598
+ );
11074
11599
  }
11075
11600
  this.simpleEventSubscribed = true;
11076
- const isUdp = this.client.getTransport?.() === "udp";
11601
+ const isUdp = entry.client.getTransport?.() === "udp";
11077
11602
  if (isUdp) {
11078
11603
  this.startUdpSleepInference();
11079
- } else if (this.client.isStatePollingEnabled?.()) {
11080
- const channel = this.client.getConfiguredChannel?.() ?? 0;
11604
+ } else if (entry.client.isStatePollingEnabled?.()) {
11605
+ const channel = entry.client.getConfiguredChannel?.() ?? 0;
11081
11606
  await this.checkAndDispatchCurrentState(channel);
11082
11607
  this.startStatePolling();
11083
11608
  }
@@ -11087,7 +11612,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11087
11612
  return await this.simpleEventSubscribeInFlight;
11088
11613
  }
11089
11614
  async ensureSimpleEventUnsubscribed() {
11090
- if (!this.simpleEventSubscribed && !this.client.subscribed) return;
11615
+ const generalEntry = this.socketPool.get("general");
11616
+ if (!generalEntry) {
11617
+ this.simpleEventSubscribed = false;
11618
+ this.stopSimpleEventResubscribeTimer();
11619
+ this.stopStatePolling();
11620
+ this.stopUdpSleepInference();
11621
+ return;
11622
+ }
11623
+ if (!this.simpleEventSubscribed && !generalEntry.client.subscribed) return;
11091
11624
  if (this.simpleEventUnsubscribeInFlight)
11092
11625
  return await this.simpleEventUnsubscribeInFlight;
11093
11626
  if (this.simpleEventSubscribeInFlight) {
@@ -11229,6 +11762,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11229
11762
  );
11230
11763
  }
11231
11764
  async close(options) {
11765
+ if (this._closed) return;
11766
+ this._closed = true;
11232
11767
  if (this.sessionGuardIntervalTimer) {
11233
11768
  clearInterval(this.sessionGuardIntervalTimer);
11234
11769
  this.sessionGuardIntervalTimer = void 0;
@@ -11291,7 +11826,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11291
11826
  }
11292
11827
  async handleSendXml400(params, frame, retry) {
11293
11828
  const emptyBody = frame.body.length === 0;
11294
- const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: camera sleeping/waking (battery), expired session, invalid username/password, or unsupported command on NVR/Hub.";
11829
+ const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
11295
11830
  if (this.isSendXmlFailFast400(params, frame.body.length)) {
11296
11831
  throw new Error(emptyBody400Msg);
11297
11832
  }
@@ -11807,11 +12342,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11807
12342
  * Minimal per-channel inventory for NVR-connected devices.
11808
12343
  *
11809
12344
  * Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
12345
+ *
12346
+ * @param options.source - Data source for the channel list (default: `"cgi"`):
12347
+ * - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
12348
+ * no dependency on async push messages. Recommended for first-call discovery.
12349
+ * - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
12350
+ * info after login + event subscription. This push is *asynchronous*: if it has not
12351
+ * arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
12352
+ * with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
11810
12353
  */
11811
12354
  async getNvrChannelsSummary(options) {
11812
- const source = options?.source ?? "baichuan";
11813
- const pushInfo = this.getChannelInfoFromPushCache();
11814
- const channels = (options?.channels?.length ? options.channels : Array.from(pushInfo.keys())).map((c) => Number(c)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
12355
+ const source = options?.source ?? "cgi";
12356
+ let channels;
12357
+ const cgiStatusByChannel = /* @__PURE__ */ new Map();
12358
+ if (options?.channels?.length) {
12359
+ channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
12360
+ } else if (source === "cgi") {
12361
+ try {
12362
+ const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
12363
+ const status = channelsResponse?.[0]?.value?.status ?? [];
12364
+ for (const s of status) {
12365
+ const ch = Number(s?.channel);
12366
+ if (!Number.isFinite(ch)) continue;
12367
+ cgiStatusByChannel.set(ch, {
12368
+ ...s.name != null ? { name: s.name } : {},
12369
+ ...s.uid != null ? { uid: s.uid } : {},
12370
+ sleeping: s.sleep === 1
12371
+ });
12372
+ }
12373
+ channels = cgiChannels;
12374
+ this.logger.debug?.(
12375
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
12376
+ );
12377
+ } catch (e) {
12378
+ const msg = e instanceof Error ? e.message : String(e);
12379
+ this.logger.warn?.(
12380
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
12381
+ );
12382
+ channels = [];
12383
+ }
12384
+ } else {
12385
+ const pushInfo2 = this.getChannelInfoFromPushCache();
12386
+ channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
12387
+ }
12388
+ channels = channels.sort((a, b) => a - b);
11815
12389
  const support = await this.getSupportInfo().catch(() => {
11816
12390
  this.logger.error?.(
11817
12391
  "[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
@@ -11841,7 +12415,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11841
12415
  );
11842
12416
  }
11843
12417
  }
11844
- const cacheKey = `baichuan:${channels.join(",")}`;
12418
+ const cacheKey = `${source}:${channels.join(",")}`;
11845
12419
  const cached = this.nvrChannelsSummaryCache.get(cacheKey);
11846
12420
  if (cached) {
11847
12421
  return {
@@ -11862,8 +12436,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11862
12436
  } catch {
11863
12437
  }
11864
12438
  }
12439
+ const pushInfo = this.getChannelInfoFromPushCache();
11865
12440
  const devices = channels.map((channel) => {
11866
- const cached2 = pushInfo.get(channel);
12441
+ const pushCached = pushInfo.get(channel);
12442
+ const cgiStatus = cgiStatusByChannel.get(channel);
11867
12443
  const info = infoPerChannel.get(channel);
11868
12444
  const networkInfo = networkInfoPerChannel.get(channel);
11869
12445
  const isBattery = isBatteryByChannel.get(channel) ?? false;
@@ -11871,6 +12447,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11871
12447
  const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
11872
12448
  const normalizedModel = model ? model.trim() : void 0;
11873
12449
  const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
12450
+ const name = pushCached?.name || cgiStatus?.name || "";
12451
+ const uid = pushCached?.uid || cgiStatus?.uid || "";
12452
+ const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
11874
12453
  return {
11875
12454
  channel,
11876
12455
  isBattery,
@@ -11880,19 +12459,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11880
12459
  ...networkInfo?.ip ? { ip: networkInfo.ip } : {},
11881
12460
  ...networkInfo?.mac ? { mac: networkInfo.mac } : {},
11882
12461
  ...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
11883
- ...cached2?.name ? { name: cached2.name } : {},
11884
- ...cached2?.uid ? { uid: cached2.uid } : {},
11885
- ...cached2?.state ? { state: cached2.state } : {},
11886
- ...typeof cached2?.index === "number" ? { index: cached2.index } : {},
11887
- ...cached2?.streamSupport?.length ? { streamSupport: cached2.streamSupport } : {},
11888
- ...cached2?.wifiState ? { wifiState: cached2.wifiState } : {},
11889
- ...cached2?.networkSegment ? { networkSegment: cached2.networkSegment } : {},
11890
- ...typeof cached2?.changed === "boolean" ? { changed: cached2.changed } : {},
11891
- ...typeof cached2?.abilityChanged === "boolean" ? { abilityChanged: cached2.abilityChanged } : {},
11892
- ...typeof cached2?.online === "boolean" ? { online: cached2.online } : {},
11893
- ...typeof cached2?.sleeping === "boolean" ? { sleeping: cached2.sleeping } : {},
11894
- ...cached2?.loginState ? { loginState: cached2.loginState } : {},
11895
- ...typeof cached2?.updatedAtMs === "number" ? { updatedAtMs: cached2.updatedAtMs } : {}
12462
+ ...name ? { name } : {},
12463
+ ...uid ? { uid } : {},
12464
+ ...pushCached?.state ? { state: pushCached.state } : {},
12465
+ ...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
12466
+ ...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
12467
+ ...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
12468
+ ...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
12469
+ ...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
12470
+ ...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
12471
+ ...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
12472
+ ...typeof sleeping === "boolean" ? { sleeping } : {},
12473
+ ...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
12474
+ ...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
11896
12475
  };
11897
12476
  });
11898
12477
  const result = { channels, devices };
@@ -13072,6 +13651,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
13072
13651
  this._processVideoclipThumbnailQueue();
13073
13652
  }
13074
13653
  }
13654
+ if (this.videoclipThumbnailQueue.length >= 50) {
13655
+ throw new Error(
13656
+ `Thumbnail queue full (${this.videoclipThumbnailQueue.length}/50) \u2013 request rejected to protect camera stability`
13657
+ );
13658
+ }
13075
13659
  return new Promise((resolve, reject) => {
13076
13660
  this.videoclipThumbnailQueue.push({ params, resolve, reject });
13077
13661
  });
@@ -13172,10 +13756,7 @@ ${xml}`);
13172
13756
  messageClass: BC_CLASS_MODERN_24,
13173
13757
  streamType: 0,
13174
13758
  payloadXml: xml,
13175
- timeoutMs,
13176
- // Retry parameters - camera often rejects first few requests
13177
- maxRetries: 8,
13178
- retryDelayMs: 1500
13759
+ timeoutMs
13179
13760
  });
13180
13761
  trace(`CoverPreview succeeded`);
13181
13762
  } catch (e) {
@@ -14568,8 +15149,10 @@ ${stderr}`)
14568
15149
  * Unsubscribe from events.
14569
15150
  */
14570
15151
  async unsubscribeEvents() {
14571
- this.client.subscribed = false;
14572
- this.client.refreshKeepAlive?.();
15152
+ const generalEntry = this.socketPool.get("general");
15153
+ if (!generalEntry) return;
15154
+ generalEntry.client.subscribed = false;
15155
+ generalEntry.client.refreshKeepAlive?.();
14573
15156
  }
14574
15157
  /**
14575
15158
  * Check current motion and AI state and dispatch events if state changed.
@@ -16156,13 +16739,12 @@ ${xml}`
16156
16739
  ]);
16157
16740
  const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
16158
16741
  const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
16159
- const supportItem = this.pickBestSupportItem(support, ch);
16160
- const capabilities = this.parseCapabilitiesFromSupport(
16161
- ch,
16162
- supportItem,
16163
- support,
16164
- abilities
16165
- );
16742
+ const supportItem = getSupportItemForChannel(support, ch);
16743
+ const capabilities = computeDeviceCapabilities({
16744
+ channel: ch,
16745
+ ...support != null && { support },
16746
+ ...abilities != null && { abilities }
16747
+ });
16166
16748
  const item = supportItem;
16167
16749
  const lightType = item?.lightType;
16168
16750
  const ledCtrl = item?.ledCtrl;
@@ -16178,6 +16760,25 @@ ${xml}`
16178
16760
  });
16179
16761
  capabilities.hasFloodlight = probed;
16180
16762
  }
16763
+ let dingDongListIds;
16764
+ let dingDongCfgIds;
16765
+ let wirelessChimeError;
16766
+ if (capabilities.hasWirelessChime) {
16767
+ try {
16768
+ const list = await this.getDingDongList(ch);
16769
+ dingDongListIds = list.map((d) => d.id);
16770
+ const first = list[0];
16771
+ const fromList = first !== void 0 && first.id >= 0;
16772
+ if (!fromList) {
16773
+ const configs = await this.getDingDongCfg(ch);
16774
+ dingDongCfgIds = configs.map((c) => c.id);
16775
+ capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
16776
+ }
16777
+ } catch (e) {
16778
+ capabilities.hasWirelessChime = false;
16779
+ wirelessChimeError = e instanceof Error ? e.message : String(e);
16780
+ }
16781
+ }
16181
16782
  const features = this.parseFeaturesFromSupport(support);
16182
16783
  const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
16183
16784
  const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
@@ -16214,7 +16815,10 @@ ${xml}`
16214
16815
  ...abilities && {
16215
16816
  abilityMergedKeyCount: Object.keys(abilities).length
16216
16817
  },
16217
- ...support?.items && { supportItemCount: support.items.length }
16818
+ ...support?.items && { supportItemCount: support.items.length },
16819
+ ...dingDongListIds !== void 0 && { dingDongListIds },
16820
+ ...dingDongCfgIds !== void 0 && { dingDongCfgIds },
16821
+ ...wirelessChimeError !== void 0 && { wirelessChimeError }
16218
16822
  };
16219
16823
  const result = {
16220
16824
  capabilities,
@@ -16241,90 +16845,6 @@ ${xml}`
16241
16845
  this.deviceCapabilitiesCache.clear();
16242
16846
  }
16243
16847
  }
16244
- /**
16245
- * Pick the best SupportItem for a channel.
16246
- * Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
16247
- */
16248
- pickBestSupportItem(support, channel) {
16249
- if (!support?.items?.length) return void 0;
16250
- const candidates = support.items.filter((i) => i.chnID === channel);
16251
- if (!candidates.length) return void 0;
16252
- const score = (item) => {
16253
- const anyItem = item;
16254
- let result = 0;
16255
- if (anyItem.name == null) result += 100;
16256
- const capabilityKeys = [
16257
- "ptzType",
16258
- "ptzControl",
16259
- "ptzPreset",
16260
- "ledCtrl",
16261
- "lightType",
16262
- "battery",
16263
- "audioVersion",
16264
- "motion",
16265
- "encCtrl",
16266
- "newIspCfg",
16267
- "remoteAbility",
16268
- "aitype",
16269
- "videoClip",
16270
- "snap"
16271
- ];
16272
- for (const k of capabilityKeys) {
16273
- if (anyItem[k] !== void 0) result += 3;
16274
- }
16275
- return result;
16276
- };
16277
- return candidates.sort((a, b) => score(b) - score(a))[0];
16278
- }
16279
- /**
16280
- * Parse device capabilities from SupportInfo.
16281
- * Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
16282
- */
16283
- parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
16284
- const truthy = (v) => {
16285
- if (typeof v === "number") return v > 0;
16286
- if (typeof v === "string") {
16287
- const n = Number(v);
16288
- return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
16289
- }
16290
- return Boolean(v);
16291
- };
16292
- const item = supportItem;
16293
- const ptzMode = support?.ptzMode?.toLowerCase();
16294
- const ptzType = item ? truthy(item.ptzType) : false;
16295
- const ptzControl = item ? truthy(item.ptzControl) : false;
16296
- const hasPtzFromItem = ptzType || ptzControl;
16297
- const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
16298
- const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
16299
- const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
16300
- const hasPresets = item ? truthy(item.ptzPreset) : false;
16301
- const hasBattery = item ? truthy(item.battery) : false;
16302
- const hasSiren = item ? truthy(item.audioVersion) : false;
16303
- const lightType = item?.lightType;
16304
- const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
16305
- const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
16306
- const isDoorbell = item ? truthy(item.doorbellVersion) : false;
16307
- const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
16308
- return {
16309
- channel,
16310
- ...ptzMode && { ptzMode },
16311
- hasPan: hasPanTilt,
16312
- hasTilt: hasPanTilt,
16313
- hasZoom,
16314
- hasPresets,
16315
- hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
16316
- hasBattery,
16317
- hasIntercom,
16318
- hasSiren,
16319
- hasFloodlight,
16320
- hasPir,
16321
- isDoorbell,
16322
- // Autotracking: explicit flags only (autoPt or smartAI)
16323
- // Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
16324
- // on cameras that have PTZ and AI detection but NOT autotracking capability.
16325
- hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
16326
- };
16327
- }
16328
16848
  /**
16329
16849
  * Parse support features from SupportInfo.
16330
16850
  */
@@ -17093,7 +17613,7 @@ ${xml}`
17093
17613
  * @returns Test results for all stream types and profiles
17094
17614
  */
17095
17615
  async testChannelStreams(channel, logger) {
17096
- const { testChannelStreams } = await import("./DiagnosticsTools-NUMCYEKQ.js");
17616
+ const { testChannelStreams } = await import("./DiagnosticsTools-FNLGCOVA.js");
17097
17617
  return await testChannelStreams({
17098
17618
  api: this,
17099
17619
  channel: this.normalizeChannel(channel),
@@ -17109,7 +17629,7 @@ ${xml}`
17109
17629
  * @returns Complete diagnostics for all channels and streams
17110
17630
  */
17111
17631
  async collectMultifocalDiagnostics(logger) {
17112
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-NUMCYEKQ.js");
17632
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-FNLGCOVA.js");
17113
17633
  return await collectMultifocalDiagnostics({
17114
17634
  api: this,
17115
17635
  logger
@@ -19197,6 +19717,216 @@ ${scheduleItems}
19197
19717
  const channel = 0;
19198
19718
  return await this.getSnapshot(channel);
19199
19719
  }
19720
+ // --------------------
19721
+ // Chime / DingDong APIs
19722
+ // --------------------
19723
+ /**
19724
+ * Get the list of paired wireless chime devices.
19725
+ * cmd_id: 484 (GetDingDongList)
19726
+ *
19727
+ * @param channel - Channel number (0-based, default 0)
19728
+ * @returns Array of paired chime devices
19729
+ */
19730
+ async getDingDongList(channel) {
19731
+ const ch = this.normalizeChannel(channel);
19732
+ const xml = await this.sendXml({
19733
+ cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
19734
+ channel: ch
19735
+ });
19736
+ return parseDingDongListFromXml(xml);
19737
+ }
19738
+ /**
19739
+ * Get parameters (name, volume, LED state) for a specific wireless chime.
19740
+ * cmd_id: 485 (DingDongOpt, option getParam)
19741
+ *
19742
+ * @param chimeId - The chime device ID
19743
+ * @param channel - Channel number (0-based, default 0)
19744
+ * @returns Chime parameters
19745
+ */
19746
+ async getDingDongParams(chimeId, channel) {
19747
+ const ch = this.normalizeChannel(channel);
19748
+ const payloadXml = buildDingDongGetParamsXml(chimeId);
19749
+ const xml = await this.sendXml({
19750
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19751
+ channel: ch,
19752
+ payloadXml
19753
+ });
19754
+ return parseDingDongParamsFromXml(xml);
19755
+ }
19756
+ /**
19757
+ * Set parameters (name, volume, LED state) for a specific wireless chime.
19758
+ * cmd_id: 485 (DingDongOpt, option setParam)
19759
+ *
19760
+ * @param chimeId - The chime device ID
19761
+ * @param params - Parameters to set (volLevel, ledState, name)
19762
+ * @param channel - Channel number (0-based, default 0)
19763
+ */
19764
+ async setDingDongParams(chimeId, params, channel) {
19765
+ const ch = this.normalizeChannel(channel);
19766
+ const payloadXml = buildDingDongSetParamsXml(chimeId, params);
19767
+ await this.sendXml({
19768
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19769
+ channel: ch,
19770
+ payloadXml
19771
+ });
19772
+ }
19773
+ /**
19774
+ * Trigger a wireless chime to ring with a specific ringtone.
19775
+ * cmd_id: 485 (DingDongOpt, option ringWithMusic)
19776
+ *
19777
+ * @param chimeId - The chime device ID
19778
+ * @param musicId - The ringtone/music ID to play
19779
+ * @param channel - Channel number (0-based, default 0)
19780
+ */
19781
+ async ringDingDong(chimeId, musicId, channel) {
19782
+ const ch = this.normalizeChannel(channel);
19783
+ const payloadXml = buildDingDongRingXml(chimeId, musicId);
19784
+ await this.sendXml({
19785
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19786
+ channel: ch,
19787
+ payloadXml
19788
+ });
19789
+ }
19790
+ /**
19791
+ * Get the per-event alarm configuration for paired wireless chimes.
19792
+ * cmd_id: 486 (GetDingDongCfg)
19793
+ *
19794
+ * @param channel - Channel number (0-based, default 0)
19795
+ * @returns Array of chime configurations (one per paired chime)
19796
+ */
19797
+ async getDingDongCfg(channel) {
19798
+ const ch = this.normalizeChannel(channel);
19799
+ const xml = await this.sendXml({
19800
+ cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
19801
+ channel: ch
19802
+ });
19803
+ return parseDingDongCfgFromXml(xml);
19804
+ }
19805
+ /**
19806
+ * Set the per-event alarm configuration for a specific wireless chime.
19807
+ * cmd_id: 487 (SetDingDongCfg)
19808
+ *
19809
+ * @param chimeId - The chime ring/device ID
19810
+ * @param eventType - Event type string (e.g. "doorbell", "package", "people")
19811
+ * @param state - 0 = disabled, 1 = enabled
19812
+ * @param musicId - Ringtone ID to use for this event type
19813
+ * @param channel - Channel number (0-based, default 0)
19814
+ */
19815
+ async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
19816
+ const ch = this.normalizeChannel(channel);
19817
+ const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
19818
+ await this.sendXml({
19819
+ cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
19820
+ channel: ch,
19821
+ payloadXml
19822
+ });
19823
+ }
19824
+ /** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
19825
+ _hardwiredChimeCache = /* @__PURE__ */ new Map();
19826
+ /**
19827
+ * Get the hardwired (wired-in) chime state.
19828
+ * cmd_id: 483 (GetDingDongCtrl)
19829
+ *
19830
+ * Note: calling this may briefly trigger the physical chime to rattle.
19831
+ *
19832
+ * @param channel - Channel number (0-based, default 0)
19833
+ * @returns Hardwired chime state (type, enabled, time)
19834
+ */
19835
+ async getHardwiredChime(channel) {
19836
+ const ch = this.normalizeChannel(channel);
19837
+ const payloadXml = buildGetDingDongCtrlXml();
19838
+ const xml = await this.sendXml({
19839
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
19840
+ channel: ch,
19841
+ payloadXml
19842
+ });
19843
+ const state = parseHardwiredChimeFromXml(xml);
19844
+ this._hardwiredChimeCache.set(ch, state);
19845
+ return state;
19846
+ }
19847
+ /**
19848
+ * Set the hardwired (wired-in) chime state.
19849
+ * cmd_id: 483 (SetDingDongCtrl)
19850
+ *
19851
+ * Uses the cached state from a previous getHardwiredChime call to fill in
19852
+ * missing type/time fields, avoiding a double round-trip on every set.
19853
+ * Falls back to fetching if no cache is available.
19854
+ *
19855
+ * @param params - Chime configuration (type, enabled, time)
19856
+ * @param channel - Channel number (0-based, default 0)
19857
+ */
19858
+ async setHardwiredChime(params, channel) {
19859
+ const ch = this.normalizeChannel(channel);
19860
+ let current = this._hardwiredChimeCache.get(ch);
19861
+ if (!current) {
19862
+ current = await this.getHardwiredChime(ch);
19863
+ }
19864
+ const chimeType = params.type ?? current.type;
19865
+ const enabled = params.enabled ? 1 : 0;
19866
+ const time = params.time ?? current.time;
19867
+ const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
19868
+ const xml = await this.sendXml({
19869
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
19870
+ channel: ch,
19871
+ payloadXml
19872
+ });
19873
+ const newState = parseHardwiredChimeFromXml(xml);
19874
+ this._hardwiredChimeCache.set(ch, newState);
19875
+ return newState;
19876
+ }
19877
+ /**
19878
+ * Play an audio file on the doorbell / chime device.
19879
+ * cmd_id: 349 (QuickReplyPlay)
19880
+ *
19881
+ * @param fileId - The audio file ID to play
19882
+ * @param channel - Channel number (0-based, default 0)
19883
+ */
19884
+ async quickReplyPlay(fileId, channel) {
19885
+ const ch = this.normalizeChannel(channel);
19886
+ const payloadXml = buildQuickReplyPlayXml(ch, fileId);
19887
+ await this.sendXml({
19888
+ cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
19889
+ channel: ch,
19890
+ payloadXml
19891
+ });
19892
+ }
19893
+ /**
19894
+ * Get the silent mode state of a paired wireless chime.
19895
+ * cmd_id: 609 (GetDingDongSilent)
19896
+ *
19897
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
19898
+ * @param channel - Channel number (0-based, default 0)
19899
+ * @returns Wireless chime silent state (time=0 means active/not silenced)
19900
+ */
19901
+ async getDingDongSilent(chimeId, channel) {
19902
+ const ch = this.normalizeChannel(channel);
19903
+ const payloadXml = buildGetDingDongSilentXml(chimeId);
19904
+ const xml = await this.sendXml({
19905
+ cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
19906
+ channel: ch,
19907
+ payloadXml
19908
+ });
19909
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
19910
+ }
19911
+ /**
19912
+ * Set the silent mode of a paired wireless chime.
19913
+ * cmd_id: 610 (SetDingDongSilent)
19914
+ *
19915
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
19916
+ * @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
19917
+ * @param channel - Channel number (0-based, default 0)
19918
+ * @returns Updated wireless chime silent state
19919
+ */
19920
+ async setDingDongSilent(chimeId, time, channel) {
19921
+ const ch = this.normalizeChannel(channel);
19922
+ const payloadXml = buildSetDingDongSilentXml(chimeId, time);
19923
+ const xml = await this.sendXml({
19924
+ cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
19925
+ channel: ch,
19926
+ payloadXml
19927
+ });
19928
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
19929
+ }
19200
19930
  };
19201
19931
 
19202
19932
  // src/reolink/discovery.ts
@@ -20194,6 +20924,7 @@ export {
20194
20924
  flattenAbilitiesForChannel,
20195
20925
  abilitiesHasAny,
20196
20926
  parseSupportXml,
20927
+ getSupportItemForChannel,
20197
20928
  computeDeviceCapabilities,
20198
20929
  DUAL_LENS_DUAL_MOTION_MODELS,
20199
20930
  DUAL_LENS_SINGLE_MOTION_MODELS,
@@ -20212,4 +20943,4 @@ export {
20212
20943
  isTcpFailureThatShouldFallbackToUdp,
20213
20944
  autoDetectDeviceType
20214
20945
  };
20215
- //# sourceMappingURL=chunk-EHWVA3SG.js.map
20946
+ //# sourceMappingURL=chunk-MN7GUZT7.js.map