@apocaliss92/nodelink-js 0.2.1 → 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.
@@ -60,7 +60,7 @@ var init_urls = __esm({
60
60
  function bcHeaderHasPayloadOffset(messageClass) {
61
61
  return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
62
62
  }
63
- var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST;
63
+ var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
64
64
  var init_constants = __esm({
65
65
  "src/protocol/constants.ts"() {
66
66
  "use strict";
@@ -164,6 +164,14 @@ var init_constants = __esm({
164
164
  BC_CMD_ID_PUSH_DINGDONG_LIST = 484;
165
165
  BC_CMD_ID_PUSH_SLEEP_STATUS = 623;
166
166
  BC_CMD_ID_PUSH_COORDINATE_POINT_LIST = 723;
167
+ BC_CMD_ID_DING_DONG_CTRL = 483;
168
+ BC_CMD_ID_GET_DING_DONG_LIST = 484;
169
+ BC_CMD_ID_DING_DONG_OPT = 485;
170
+ BC_CMD_ID_GET_DING_DONG_CFG = 486;
171
+ BC_CMD_ID_SET_DING_DONG_CFG = 487;
172
+ BC_CMD_ID_QUICK_REPLY_PLAY = 349;
173
+ BC_CMD_ID_GET_DING_DONG_SILENT = 609;
174
+ BC_CMD_ID_SET_DING_DONG_SILENT = 610;
167
175
  }
168
176
  });
169
177
 
@@ -7542,6 +7550,8 @@ var NativeStreamFanout = class {
7542
7550
  } finally {
7543
7551
  for (const q of this.queues.values()) q.close();
7544
7552
  this.queues.clear();
7553
+ this.running = false;
7554
+ this.opts.onEnd?.();
7545
7555
  }
7546
7556
  })();
7547
7557
  }
@@ -7867,7 +7877,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
7867
7877
  this.logger.warn(
7868
7878
  `[BaichuanRtspServer] Could not get stream metadata: ${error}`
7869
7879
  );
7870
- this.streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
7880
+ this.streamMetadata = { frameRate: 25 };
7871
7881
  this.setFlowVideoType("H264", "metadata unavailable");
7872
7882
  }
7873
7883
  this.clientConnectionServer = net.createServer((socket) => {
@@ -7899,7 +7909,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
7899
7909
  */
7900
7910
  handleRtspConnection(socket) {
7901
7911
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
7902
- this.logger.info(`[BaichuanRtspServer] RTSP client connected: ${clientId}`);
7912
+ const connectTime = Date.now();
7913
+ this.logger.info(
7914
+ `[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
7915
+ );
7903
7916
  let sessionId = "";
7904
7917
  let buffer = Buffer.alloc(0);
7905
7918
  let clientFfmpeg;
@@ -7907,6 +7920,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
7907
7920
  let clientUdpSocket = null;
7908
7921
  let clientUdpSocketAudio = null;
7909
7922
  const cleanup = () => {
7923
+ const sessionDurationMs = Date.now() - connectTime;
7924
+ const res = this.clientResources.get(clientId);
7925
+ const framesSent = res?.framesSent ?? 0;
7926
+ this.logger.info(
7927
+ `[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
7928
+ );
7910
7929
  this.removeClient(clientId);
7911
7930
  this.authNonces.delete(clientId);
7912
7931
  const resources = this.clientResources.get(clientId);
@@ -8048,7 +8067,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8048
8067
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
8049
8068
  });
8050
8069
  } else if (method === "DESCRIBE") {
8051
- if (!this.firstFrameReceived && this.connectedClients.size === 0) {
8070
+ if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
8052
8071
  try {
8053
8072
  if (!this.nativeStreamActive) {
8054
8073
  await this.startNativeStream();
@@ -8130,7 +8149,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8130
8149
  seenFirstVideoKeyframe: false,
8131
8150
  setupTrack0: false,
8132
8151
  setupTrack1: false,
8133
- isPlaying: false
8152
+ isPlaying: false,
8153
+ connectTime
8134
8154
  });
8135
8155
  } else {
8136
8156
  existing.rtspSocket = socket;
@@ -8177,8 +8197,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8177
8197
  if (resources) {
8178
8198
  if (isTrack1) resources.setupTrack1 = true;
8179
8199
  else resources.setupTrack0 = true;
8180
- this.rtspDebugLog(
8181
- `SETUP done for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
8200
+ const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
8201
+ const track = isTrack1 ? "track1(audio)" : "track0(video)";
8202
+ this.logger.info(
8203
+ `[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
8182
8204
  );
8183
8205
  }
8184
8206
  }
@@ -8203,8 +8225,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8203
8225
  const resources = this.clientResources.get(clientId);
8204
8226
  if (resources) {
8205
8227
  resources.isPlaying = true;
8206
- this.rtspDebugLog(
8207
- `PLAY for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
8228
+ const hasAudio = !!resources.setupTrack1;
8229
+ this.logger.info(
8230
+ `[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
8208
8231
  );
8209
8232
  }
8210
8233
  }
@@ -8213,6 +8236,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8213
8236
  Range: "npt=0.000-"
8214
8237
  });
8215
8238
  } else if (method === "TEARDOWN") {
8239
+ this.logger.info(
8240
+ `[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
8241
+ );
8216
8242
  cleanup();
8217
8243
  sendResponse(200, "OK", {
8218
8244
  Session: sessionId
@@ -8307,7 +8333,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8307
8333
  this.logger.warn(
8308
8334
  `[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
8309
8335
  );
8310
- streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
8336
+ streamMetadata = { frameRate: 25 };
8311
8337
  }
8312
8338
  }
8313
8339
  const ffmpegFormat = this.flow.ffmpegFormat;
@@ -8895,15 +8921,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8895
8921
  `Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
8896
8922
  );
8897
8923
  }
8898
- const now = Date.now();
8899
- const timeSinceLastFrame = now - lastFrameTime;
8900
- const waitTime = targetFrameInterval - timeSinceLastFrame;
8901
- if (waitTime > 0) {
8902
- await new Promise(
8903
- (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
8904
- );
8924
+ if (!useDirectRtp) {
8925
+ const now = Date.now();
8926
+ const timeSinceLastFrame = now - lastFrameTime;
8927
+ const waitTime = targetFrameInterval - timeSinceLastFrame;
8928
+ if (waitTime > 0) {
8929
+ await new Promise(
8930
+ (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
8931
+ );
8932
+ }
8933
+ lastFrameTime = Date.now();
8905
8934
  }
8906
- lastFrameTime = Date.now();
8907
8935
  if (useDirectRtp) {
8908
8936
  const videoType = frame.videoType ?? this.flow.videoType;
8909
8937
  const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
@@ -8976,6 +9004,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8976
9004
  }
8977
9005
  if (!firstVideoWriteLogged) {
8978
9006
  firstVideoWriteLogged = true;
9007
+ const clientConnectTime = resources?.connectTime ?? Date.now();
9008
+ const ttffMs = Date.now() - clientConnectTime;
9009
+ this.logger.info(
9010
+ `[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
9011
+ );
8979
9012
  if (rtspDebug) {
8980
9013
  const headHex = frame.data.subarray(0, 16).toString("hex");
8981
9014
  rtspDebugLog(
@@ -8983,6 +9016,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8983
9016
  );
8984
9017
  }
8985
9018
  }
9019
+ if (resources) {
9020
+ resources.framesSent = (resources.framesSent ?? 0) + 1;
9021
+ }
8986
9022
  sendVideoAccessUnit(videoType, normalizedVideoData, true);
8987
9023
  } else {
8988
9024
  try {
@@ -9067,8 +9103,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9067
9103
  this.firstAudioPromise = new Promise((resolve) => {
9068
9104
  this.firstAudioResolve = resolve;
9069
9105
  });
9070
- this.rtspDebugLog(
9071
- `Starting native stream for profile ${this.profile} (waiting for camera to start transmitting...)`
9106
+ this.logger.info(
9107
+ `[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
9072
9108
  );
9073
9109
  await this.flow.startKeepAlive(this.api);
9074
9110
  this.nativeFanout = new NativeStreamFanout({
@@ -9111,6 +9147,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9111
9147
  this.logger.warn(
9112
9148
  `[BaichuanRtspServer] Shared native stream error: ${error}`
9113
9149
  );
9150
+ },
9151
+ onEnd: () => {
9152
+ if (!this.nativeStreamActive) return;
9153
+ this.nativeStreamActive = false;
9154
+ this.firstFrameReceived = false;
9155
+ this.firstFramePromise = null;
9156
+ this.firstFrameResolve = null;
9157
+ this.nativeFanout = null;
9158
+ this.logger.info(
9159
+ `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
9160
+ );
9161
+ if (this.connectedClients.size > 0) {
9162
+ this.logger.info(
9163
+ `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
9164
+ );
9165
+ setImmediate(() => void this.startNativeStream());
9166
+ }
9114
9167
  }
9115
9168
  });
9116
9169
  this.nativeFanout.start();
@@ -9149,7 +9202,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9149
9202
  if (!this.nativeStreamActive) {
9150
9203
  return;
9151
9204
  }
9152
- this.rtspDebugLog(`Stopping native stream`);
9205
+ this.logger.info(
9206
+ `[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
9207
+ );
9153
9208
  this.flow.stopKeepAlive();
9154
9209
  this.clearNoClientAutoStopTimer();
9155
9210
  this.nativeStreamActive = false;
@@ -9183,9 +9238,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9183
9238
  if (this.connectedClients.has(clientId)) {
9184
9239
  this.connectedClients.delete(clientId);
9185
9240
  this.emit("clientDisconnected", clientId);
9186
- this.logger.info(
9187
- `[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
9188
- );
9189
9241
  if (this.connectedClients.size === 0) {
9190
9242
  void this.stopNativeStream();
9191
9243
  }
@@ -14392,10 +14444,12 @@ function parseSupportXml(xml) {
14392
14444
  }
14393
14445
  function getSupportItemForChannel(support, channel) {
14394
14446
  if (!support?.items?.length) return void 0;
14395
- const scoreSupportItem = (item) => {
14447
+ const candidates = support.items.filter((i) => i.chnID === channel);
14448
+ if (!candidates.length) return void 0;
14449
+ const score = (item) => {
14396
14450
  const anyItem = item;
14397
- let score = 0;
14398
- if (anyItem.name == null) score += 2;
14451
+ let result = 0;
14452
+ if (anyItem.name == null) result += 100;
14399
14453
  const capabilityKeys = [
14400
14454
  "ptzType",
14401
14455
  "ptzControl",
@@ -14407,20 +14461,17 @@ function getSupportItemForChannel(support, channel) {
14407
14461
  "motion",
14408
14462
  "encCtrl",
14409
14463
  "newIspCfg",
14410
- "remoteAbility"
14464
+ "remoteAbility",
14465
+ "aitype",
14466
+ "videoClip",
14467
+ "snap"
14411
14468
  ];
14412
14469
  for (const k of capabilityKeys) {
14413
- if (anyItem[k] !== void 0) score += 3;
14470
+ if (anyItem[k] !== void 0) result += 3;
14414
14471
  }
14415
- score += Math.min(10, Math.max(0, Object.keys(anyItem).length - 1));
14416
- return score;
14417
- };
14418
- const pickBest = (chnId) => {
14419
- const candidates = support.items.filter((i) => i.chnID === chnId);
14420
- if (!candidates.length) return void 0;
14421
- return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
14472
+ return result;
14422
14473
  };
14423
- return pickBest(channel);
14474
+ return candidates.sort((a, b) => score(b) - score(a))[0];
14424
14475
  }
14425
14476
  function computeDeviceCapabilities(params) {
14426
14477
  const { channel } = params;
@@ -14452,6 +14503,7 @@ function computeDeviceCapabilities(params) {
14452
14503
  flat,
14453
14504
  /white\s*led|whiteLed|flood\s*light|floodlight/i
14454
14505
  );
14506
+ const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
14455
14507
  const hasSirenFromAbilities = abilitiesHasAny(
14456
14508
  flat,
14457
14509
  /audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
@@ -14464,6 +14516,9 @@ function computeDeviceCapabilities(params) {
14464
14516
  const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
14465
14517
  const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
14466
14518
  const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
14519
+ const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
14520
+ const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
14521
+ const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
14467
14522
  const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
14468
14523
  const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
14469
14524
  const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
@@ -14479,14 +14534,15 @@ function computeDeviceCapabilities(params) {
14479
14534
  hasZoom: finalHasZoom,
14480
14535
  hasPresets: finalHasPresets,
14481
14536
  hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
14482
- hasBattery: hasBatteryFromSupport || hasBatteryFromAbilities,
14537
+ hasBattery,
14483
14538
  hasIntercom: hasIntercomFromSupport,
14484
- hasSiren: hasSirenFromAbilities,
14539
+ hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
14485
14540
  // lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
14486
14541
  hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
14487
14542
  hasPir: hasPirFromAbilities || hasPirFromSupport,
14488
- isDoorbell: isDoorbellFromSupport || isDoorbellFromModel,
14489
- hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
14543
+ isDoorbell,
14544
+ hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
14545
+ hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
14490
14546
  };
14491
14547
  if (ptzMode !== void 0) result.ptzMode = ptzMode;
14492
14548
  return result;
@@ -16151,6 +16207,162 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
16151
16207
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
16152
16208
  init_recordingFileName();
16153
16209
 
16210
+ // src/reolink/baichuan/utils/chime.ts
16211
+ init_xml();
16212
+ var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
16213
+ <body>
16214
+ <dingdongDeviceOpt version="1.1">
16215
+ <id>${chimeId}</id>
16216
+ <opt>getParam</opt>
16217
+ </dingdongDeviceOpt>
16218
+ </body>`;
16219
+ var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
16220
+ <body>
16221
+ <dingdongDeviceOpt version="1.1">
16222
+ <opt>setParam</opt>
16223
+ <id>${chimeId}</id>
16224
+ ${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
16225
+ ${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
16226
+ ${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
16227
+ </dingdongDeviceOpt>
16228
+ </body>`;
16229
+ var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
16230
+ <body>
16231
+ <dingdongDeviceOpt version="1.1">
16232
+ <id>${chimeId}</id>
16233
+ <opt>ringWithMusic</opt>
16234
+ <musicId>${musicId}</musicId>
16235
+ </dingdongDeviceOpt>
16236
+ </body>`;
16237
+ var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
16238
+ <body>
16239
+ <dingdongCfg version="1.1">
16240
+ <deviceCfg>
16241
+ <id>${chimeId}</id>
16242
+ <alarminCfg>
16243
+ <valid>${state}</valid>
16244
+ <musicId>${musicId}</musicId>
16245
+ <type>${eventType}</type>
16246
+ </alarminCfg>
16247
+ </deviceCfg>
16248
+ </dingdongCfg>
16249
+ </body>`;
16250
+ var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
16251
+ <body>
16252
+ <dingdongCtrl version="1.1">
16253
+ <opt>machineStateGet</opt>
16254
+ </dingdongCtrl>
16255
+ </body>`;
16256
+ var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
16257
+ <body>
16258
+ <dingdongCtrl version="1.1">
16259
+ <opt>machineStateSet</opt>
16260
+ <type>${chimeType}</type>
16261
+ <bopen>${enabled}</bopen>
16262
+ <bsave>1</bsave>
16263
+ <time>${time}</time>
16264
+ </dingdongCtrl>
16265
+ </body>`;
16266
+ var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
16267
+ <body>
16268
+ <audioFileInfo version="1.1">
16269
+ <channelId>${channel}</channelId>
16270
+ <id>${fileId}</id>
16271
+ <timeout>0</timeout>
16272
+ </audioFileInfo>
16273
+ </body>`;
16274
+ var parseDingDongListFromXml = (xml) => {
16275
+ const devices = [];
16276
+ const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
16277
+ for (const block of blocks) {
16278
+ const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
16279
+ const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
16280
+ const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
16281
+ if (idText === void 0) continue;
16282
+ const id = Number(idText);
16283
+ if (!Number.isFinite(id)) continue;
16284
+ devices.push({
16285
+ id,
16286
+ name,
16287
+ netState: netStateText !== void 0 ? Number(netStateText) : 0
16288
+ });
16289
+ }
16290
+ return devices;
16291
+ };
16292
+ var parseDingDongParamsFromXml = (xml) => {
16293
+ const name = getXmlText(xml, "name");
16294
+ const volLevelText = getXmlText(xml, "volLevel");
16295
+ const ledStateText = getXmlText(xml, "ledState");
16296
+ const result = {};
16297
+ if (name !== void 0) result.name = name;
16298
+ if (volLevelText !== void 0) {
16299
+ const n = Number(volLevelText);
16300
+ if (Number.isFinite(n)) result.volLevel = n;
16301
+ }
16302
+ if (ledStateText !== void 0) {
16303
+ const n = Number(ledStateText);
16304
+ if (Number.isFinite(n)) result.ledState = n;
16305
+ }
16306
+ return result;
16307
+ };
16308
+ var parseDingDongCfgFromXml = (xml) => {
16309
+ const configs = [];
16310
+ const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
16311
+ for (const deviceBlock of deviceBlocks) {
16312
+ const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
16313
+ if (idText === void 0) continue;
16314
+ const id = Number(idText);
16315
+ if (!Number.isFinite(id)) continue;
16316
+ const typeMap = {};
16317
+ const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
16318
+ for (const alarmBlock of alarmBlocks) {
16319
+ const type = getXmlText(alarmBlock, "type");
16320
+ if (!type) continue;
16321
+ const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
16322
+ const musicIdText = getXmlText(alarmBlock, "musicId");
16323
+ typeMap[type] = {
16324
+ valid: validText !== void 0 ? Number(validText) : 0,
16325
+ musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
16326
+ };
16327
+ }
16328
+ configs.push({ id, type: typeMap });
16329
+ }
16330
+ return configs;
16331
+ };
16332
+ var parseHardwiredChimeFromXml = (xml) => {
16333
+ const type = getXmlText(xml, "type") ?? "";
16334
+ const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
16335
+ const timeText = getXmlText(xml, "time");
16336
+ return {
16337
+ type,
16338
+ enabled: bopenText === "1",
16339
+ time: timeText !== void 0 ? Number(timeText) : 0
16340
+ };
16341
+ };
16342
+ var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
16343
+ <body>
16344
+ <dingdongSilentMode version="1.1">
16345
+ <id>${chimeId}</id>
16346
+ </dingdongSilentMode>
16347
+ </body>`;
16348
+ var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
16349
+ <body>
16350
+ <dingdongSilentMode version="1.1">
16351
+ <id>${chimeId}</id>
16352
+ <time>${time}</time>
16353
+ <type>63</type>
16354
+ </dingdongSilentMode>
16355
+ </body>`;
16356
+ var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
16357
+ const timeText = getXmlText(xml, "time");
16358
+ const time = timeText !== void 0 ? Number(timeText) : 0;
16359
+ return {
16360
+ id: chimeId,
16361
+ time,
16362
+ active: time === 0
16363
+ };
16364
+ };
16365
+
16154
16366
  // src/reolink/baichuan/utils/eventsGetEvents.ts
16155
16367
  init_xml();
16156
16368
  var parseAiTypeToken = (aiTypeRaw) => {
@@ -16463,6 +16675,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
16463
16675
  host;
16464
16676
  username;
16465
16677
  password;
16678
+ /**
16679
+ * Set to `true` after `close()` is called.
16680
+ * Once closed, the API instance should not be reused.
16681
+ */
16682
+ _closed = false;
16466
16683
  // ─────────────────────────────────────────────────────────────────────────────
16467
16684
  // SOCKET POOL - Tag-based socket management
16468
16685
  // ─────────────────────────────────────────────────────────────────────────────
@@ -16492,10 +16709,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
16492
16709
  get client() {
16493
16710
  const entry = this.socketPool.get("general");
16494
16711
  if (!entry) {
16712
+ if (this._closed) {
16713
+ throw new Error(
16714
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
16715
+ );
16716
+ }
16495
16717
  throw new Error("[ReolinkBaichuanApi] General socket not initialized");
16496
16718
  }
16497
16719
  return entry.client;
16498
16720
  }
16721
+ /**
16722
+ * `true` after `close()` has been called. A closed API should not be reused;
16723
+ * the consumer should create a new instance.
16724
+ */
16725
+ get isClosed() {
16726
+ return this._closed;
16727
+ }
16728
+ /**
16729
+ * `true` when the API is usable: not closed, general socket exists, socket
16730
+ * is connected and the client is logged in.
16731
+ *
16732
+ * This is the recommended way for consumers to check whether the API is
16733
+ * still valid before issuing commands, instead of directly accessing
16734
+ * `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
16735
+ * if the socket pool was already destroyed).
16736
+ */
16737
+ get isReady() {
16738
+ if (this._closed) return false;
16739
+ const entry = this.socketPool.get("general");
16740
+ if (!entry) return false;
16741
+ try {
16742
+ return entry.client.isSocketConnected() && entry.client.loggedIn;
16743
+ } catch {
16744
+ return false;
16745
+ }
16746
+ }
16747
+ /** Promise tracking an in-flight reconnection from `ensureConnected()`. */
16748
+ _ensureConnectedPromise;
16749
+ /**
16750
+ * Ensure the "general" socket is connected and logged in.
16751
+ * If the socket is disconnected or the pool entry was destroyed, a new
16752
+ * general socket is created, logged in, and all event/push/guard listeners
16753
+ * are re-attached automatically.
16754
+ *
16755
+ * This is a **no-op** when the API is already {@link isReady}.
16756
+ *
16757
+ * @throws If `close()` was called — the API is permanently closed and a new
16758
+ * instance must be created.
16759
+ */
16760
+ async ensureConnected() {
16761
+ if (this._closed) {
16762
+ throw new Error(
16763
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
16764
+ );
16765
+ }
16766
+ if (this.isReady) return;
16767
+ if (this._ensureConnectedPromise) {
16768
+ return this._ensureConnectedPromise;
16769
+ }
16770
+ this._ensureConnectedPromise = this.reconnectGeneralSocket();
16771
+ try {
16772
+ await this._ensureConnectedPromise;
16773
+ } finally {
16774
+ this._ensureConnectedPromise = void 0;
16775
+ }
16776
+ }
16777
+ /**
16778
+ * Internal: destroy the current general socket (if any), create a new one,
16779
+ * login, and re-attach all listeners.
16780
+ */
16781
+ async reconnectGeneralSocket() {
16782
+ const oldEntry = this.socketPool.get("general");
16783
+ if (oldEntry) {
16784
+ oldEntry.client.removeAllListeners();
16785
+ if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
16786
+ if (oldEntry.generalPermitRelease) {
16787
+ try {
16788
+ oldEntry.generalPermitRelease();
16789
+ } catch {
16790
+ }
16791
+ }
16792
+ this.socketPool.delete("general");
16793
+ try {
16794
+ await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
16795
+ } catch {
16796
+ }
16797
+ }
16798
+ const newClient = new BaichuanClient(this.clientOptions);
16799
+ this.socketPool.set("general", {
16800
+ client: newClient,
16801
+ refCount: 1,
16802
+ // general socket is always "in use"
16803
+ createdAt: Date.now(),
16804
+ lastUsedAt: Date.now(),
16805
+ idleCloseTimer: void 0,
16806
+ generalPermitRelease: void 0
16807
+ });
16808
+ this.setupGeneralClientListeners();
16809
+ await this.client.login();
16810
+ this.logger.log?.(
16811
+ "[ReolinkBaichuanApi] General socket reconnected successfully"
16812
+ );
16813
+ if (this.simpleEventListeners.size > 0) {
16814
+ this.simpleEventSubscribed = false;
16815
+ this.simpleEventWatchdogRecoveryAttempts = 0;
16816
+ this.simpleEventWatchdogLastRecoveryAt = 0;
16817
+ try {
16818
+ await this.ensureSimpleEventSubscribed();
16819
+ this.simpleEventLastReceivedAt = Date.now();
16820
+ this.logger.log?.(
16821
+ `[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
16822
+ );
16823
+ } catch (e) {
16824
+ (this.logger.debug ?? this.logger.log).call(
16825
+ this.logger,
16826
+ `[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
16827
+ formatErrorForLog(e)
16828
+ );
16829
+ }
16830
+ }
16831
+ }
16832
+ /**
16833
+ * Attach event, push, channelInfo, and guard listeners to the current
16834
+ * "general" client. Called from the constructor and from
16835
+ * {@link reconnectGeneralSocket}.
16836
+ */
16837
+ setupGeneralClientListeners() {
16838
+ const client = this.client;
16839
+ client.on("event", (event) => {
16840
+ const mapped = mapToSimpleEvent(event);
16841
+ if (!mapped) return;
16842
+ this.dispatchSimpleEvent(mapped);
16843
+ });
16844
+ client.on("channelInfo", (xml) => {
16845
+ try {
16846
+ this.parseAndStoreChannelInfo(xml);
16847
+ } catch (e) {
16848
+ this.logger.warn?.(
16849
+ "[ReolinkBaichuanApi] Error parsing channel info from push",
16850
+ formatErrorForLog(e)
16851
+ );
16852
+ }
16853
+ });
16854
+ client.on("push", (frame) => {
16855
+ const cmdId = frame.header.cmdId;
16856
+ 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) {
16857
+ return;
16858
+ }
16859
+ try {
16860
+ if (frame.body.length === 0) return;
16861
+ const xml = client.tryDecryptXml(
16862
+ frame.body,
16863
+ frame.header.channelId,
16864
+ client.enc
16865
+ );
16866
+ if (!xml || !xml.startsWith("<?xml")) return;
16867
+ this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
16868
+ } catch (e) {
16869
+ this.logger.debug?.(
16870
+ "[ReolinkBaichuanApi] Error parsing settings push",
16871
+ formatErrorForLog(e)
16872
+ );
16873
+ }
16874
+ });
16875
+ if (this.rebootAfterDisconnectionsPerMinute > 0) {
16876
+ client.on("close", () => {
16877
+ try {
16878
+ void this.maybeRebootOnDisconnectStorm();
16879
+ } catch {
16880
+ }
16881
+ });
16882
+ }
16883
+ if (this.rebootAfterConsecutiveEconnreset > 0) {
16884
+ client.on("close", () => {
16885
+ try {
16886
+ void this.maybeRebootOnEconnresetStorm();
16887
+ } catch {
16888
+ }
16889
+ });
16890
+ }
16891
+ if (!this.sessionGuardIntervalTimer) {
16892
+ client.once("push", () => {
16893
+ void this.logActiveSessionsOnStartup();
16894
+ this.sessionGuardIntervalTimer = setInterval(() => {
16895
+ void this.maybeRebootOnTooManySessions();
16896
+ }, 6e4);
16897
+ });
16898
+ }
16899
+ }
16499
16900
  /**
16500
16901
  * Cached camera UID. May be initially undefined if not provided in the constructor.
16501
16902
  * Will be lazily populated on demand when needed (e.g. for recordings).
@@ -17436,42 +17837,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17436
17837
  logger: this.logger,
17437
17838
  debugConfig: generalClient.getDebugConfig?.()
17438
17839
  });
17439
- this.client.on("event", (event) => {
17440
- const mapped = mapToSimpleEvent(event);
17441
- if (!mapped) return;
17442
- this.dispatchSimpleEvent(mapped);
17443
- });
17444
- this.client.on("channelInfo", (xml) => {
17445
- try {
17446
- this.parseAndStoreChannelInfo(xml);
17447
- } catch (e) {
17448
- this.logger.warn?.(
17449
- "[ReolinkBaichuanApi] Error parsing channel info from push",
17450
- formatErrorForLog(e)
17451
- );
17452
- }
17453
- });
17454
- this.client.on("push", (frame) => {
17455
- const cmdId = frame.header.cmdId;
17456
- 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) {
17457
- return;
17458
- }
17459
- try {
17460
- if (frame.body.length === 0) return;
17461
- const xml = this.client.tryDecryptXml(
17462
- frame.body,
17463
- frame.header.channelId,
17464
- this.client.enc
17465
- );
17466
- if (!xml || !xml.startsWith("<?xml")) return;
17467
- this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
17468
- } catch (e) {
17469
- this.logger.debug?.(
17470
- "[ReolinkBaichuanApi] Error parsing settings push",
17471
- formatErrorForLog(e)
17472
- );
17473
- }
17474
- });
17475
17840
  const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
17476
17841
  if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
17477
17842
  this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
@@ -17480,32 +17845,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17480
17845
  if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
17481
17846
  this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
17482
17847
  }
17483
- if (this.rebootAfterDisconnectionsPerMinute > 0) {
17484
- this.client.on("close", () => {
17485
- try {
17486
- void this.maybeRebootOnDisconnectStorm();
17487
- } catch {
17488
- }
17489
- });
17490
- }
17491
17848
  const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
17492
17849
  if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
17493
17850
  this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
17494
17851
  }
17495
- if (this.rebootAfterConsecutiveEconnreset > 0) {
17496
- this.client.on("close", () => {
17497
- try {
17498
- void this.maybeRebootOnEconnresetStorm();
17499
- } catch {
17500
- }
17501
- });
17502
- }
17503
- this.client.once("push", () => {
17504
- void this.logActiveSessionsOnStartup();
17505
- this.sessionGuardIntervalTimer = setInterval(() => {
17506
- void this.maybeRebootOnTooManySessions();
17507
- }, 6e4);
17508
- });
17852
+ this.setupGeneralClientListeners();
17509
17853
  }
17510
17854
  /**
17511
17855
  * CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
@@ -18336,6 +18680,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18336
18680
  );
18337
18681
  }
18338
18682
  async close(options) {
18683
+ if (this._closed) return;
18684
+ this._closed = true;
18339
18685
  if (this.sessionGuardIntervalTimer) {
18340
18686
  clearInterval(this.sessionGuardIntervalTimer);
18341
18687
  this.sessionGuardIntervalTimer = void 0;
@@ -18398,7 +18744,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18398
18744
  }
18399
18745
  async handleSendXml400(params, frame, retry) {
18400
18746
  const emptyBody = frame.body.length === 0;
18401
- 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.";
18747
+ const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
18402
18748
  if (this.isSendXmlFailFast400(params, frame.body.length)) {
18403
18749
  throw new Error(emptyBody400Msg);
18404
18750
  }
@@ -18914,11 +19260,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18914
19260
  * Minimal per-channel inventory for NVR-connected devices.
18915
19261
  *
18916
19262
  * Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
19263
+ *
19264
+ * @param options.source - Data source for the channel list (default: `"cgi"`):
19265
+ * - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
19266
+ * no dependency on async push messages. Recommended for first-call discovery.
19267
+ * - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
19268
+ * info after login + event subscription. This push is *asynchronous*: if it has not
19269
+ * arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
19270
+ * with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
18917
19271
  */
18918
19272
  async getNvrChannelsSummary(options) {
18919
- const source = options?.source ?? "baichuan";
18920
- const pushInfo = this.getChannelInfoFromPushCache();
18921
- 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);
19273
+ const source = options?.source ?? "cgi";
19274
+ let channels;
19275
+ const cgiStatusByChannel = /* @__PURE__ */ new Map();
19276
+ if (options?.channels?.length) {
19277
+ channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
19278
+ } else if (source === "cgi") {
19279
+ try {
19280
+ const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
19281
+ const status = channelsResponse?.[0]?.value?.status ?? [];
19282
+ for (const s of status) {
19283
+ const ch = Number(s?.channel);
19284
+ if (!Number.isFinite(ch)) continue;
19285
+ cgiStatusByChannel.set(ch, {
19286
+ ...s.name != null ? { name: s.name } : {},
19287
+ ...s.uid != null ? { uid: s.uid } : {},
19288
+ sleeping: s.sleep === 1
19289
+ });
19290
+ }
19291
+ channels = cgiChannels;
19292
+ this.logger.debug?.(
19293
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
19294
+ );
19295
+ } catch (e) {
19296
+ const msg = e instanceof Error ? e.message : String(e);
19297
+ this.logger.warn?.(
19298
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
19299
+ );
19300
+ channels = [];
19301
+ }
19302
+ } else {
19303
+ const pushInfo2 = this.getChannelInfoFromPushCache();
19304
+ channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
19305
+ }
19306
+ channels = channels.sort((a, b) => a - b);
18922
19307
  const support = await this.getSupportInfo().catch(() => {
18923
19308
  this.logger.error?.(
18924
19309
  "[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
@@ -18948,7 +19333,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18948
19333
  );
18949
19334
  }
18950
19335
  }
18951
- const cacheKey = `baichuan:${channels.join(",")}`;
19336
+ const cacheKey = `${source}:${channels.join(",")}`;
18952
19337
  const cached = this.nvrChannelsSummaryCache.get(cacheKey);
18953
19338
  if (cached) {
18954
19339
  return {
@@ -18969,8 +19354,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18969
19354
  } catch {
18970
19355
  }
18971
19356
  }
19357
+ const pushInfo = this.getChannelInfoFromPushCache();
18972
19358
  const devices = channels.map((channel) => {
18973
- const cached2 = pushInfo.get(channel);
19359
+ const pushCached = pushInfo.get(channel);
19360
+ const cgiStatus = cgiStatusByChannel.get(channel);
18974
19361
  const info = infoPerChannel.get(channel);
18975
19362
  const networkInfo = networkInfoPerChannel.get(channel);
18976
19363
  const isBattery = isBatteryByChannel.get(channel) ?? false;
@@ -18978,6 +19365,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18978
19365
  const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
18979
19366
  const normalizedModel = model ? model.trim() : void 0;
18980
19367
  const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
19368
+ const name = pushCached?.name || cgiStatus?.name || "";
19369
+ const uid = pushCached?.uid || cgiStatus?.uid || "";
19370
+ const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
18981
19371
  return {
18982
19372
  channel,
18983
19373
  isBattery,
@@ -18987,19 +19377,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18987
19377
  ...networkInfo?.ip ? { ip: networkInfo.ip } : {},
18988
19378
  ...networkInfo?.mac ? { mac: networkInfo.mac } : {},
18989
19379
  ...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
18990
- ...cached2?.name ? { name: cached2.name } : {},
18991
- ...cached2?.uid ? { uid: cached2.uid } : {},
18992
- ...cached2?.state ? { state: cached2.state } : {},
18993
- ...typeof cached2?.index === "number" ? { index: cached2.index } : {},
18994
- ...cached2?.streamSupport?.length ? { streamSupport: cached2.streamSupport } : {},
18995
- ...cached2?.wifiState ? { wifiState: cached2.wifiState } : {},
18996
- ...cached2?.networkSegment ? { networkSegment: cached2.networkSegment } : {},
18997
- ...typeof cached2?.changed === "boolean" ? { changed: cached2.changed } : {},
18998
- ...typeof cached2?.abilityChanged === "boolean" ? { abilityChanged: cached2.abilityChanged } : {},
18999
- ...typeof cached2?.online === "boolean" ? { online: cached2.online } : {},
19000
- ...typeof cached2?.sleeping === "boolean" ? { sleeping: cached2.sleeping } : {},
19001
- ...cached2?.loginState ? { loginState: cached2.loginState } : {},
19002
- ...typeof cached2?.updatedAtMs === "number" ? { updatedAtMs: cached2.updatedAtMs } : {}
19380
+ ...name ? { name } : {},
19381
+ ...uid ? { uid } : {},
19382
+ ...pushCached?.state ? { state: pushCached.state } : {},
19383
+ ...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
19384
+ ...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
19385
+ ...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
19386
+ ...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
19387
+ ...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
19388
+ ...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
19389
+ ...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
19390
+ ...typeof sleeping === "boolean" ? { sleeping } : {},
19391
+ ...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
19392
+ ...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
19003
19393
  };
19004
19394
  });
19005
19395
  const result = { channels, devices };
@@ -23267,13 +23657,12 @@ ${xml}`
23267
23657
  ]);
23268
23658
  const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
23269
23659
  const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
23270
- const supportItem = this.pickBestSupportItem(support, ch);
23271
- const capabilities = this.parseCapabilitiesFromSupport(
23272
- ch,
23273
- supportItem,
23274
- support,
23275
- abilities
23276
- );
23660
+ const supportItem = getSupportItemForChannel(support, ch);
23661
+ const capabilities = computeDeviceCapabilities({
23662
+ channel: ch,
23663
+ ...support != null && { support },
23664
+ ...abilities != null && { abilities }
23665
+ });
23277
23666
  const item = supportItem;
23278
23667
  const lightType = item?.lightType;
23279
23668
  const ledCtrl = item?.ledCtrl;
@@ -23289,6 +23678,25 @@ ${xml}`
23289
23678
  });
23290
23679
  capabilities.hasFloodlight = probed;
23291
23680
  }
23681
+ let dingDongListIds;
23682
+ let dingDongCfgIds;
23683
+ let wirelessChimeError;
23684
+ if (capabilities.hasWirelessChime) {
23685
+ try {
23686
+ const list = await this.getDingDongList(ch);
23687
+ dingDongListIds = list.map((d) => d.id);
23688
+ const first = list[0];
23689
+ const fromList = first !== void 0 && first.id >= 0;
23690
+ if (!fromList) {
23691
+ const configs = await this.getDingDongCfg(ch);
23692
+ dingDongCfgIds = configs.map((c) => c.id);
23693
+ capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
23694
+ }
23695
+ } catch (e) {
23696
+ capabilities.hasWirelessChime = false;
23697
+ wirelessChimeError = e instanceof Error ? e.message : String(e);
23698
+ }
23699
+ }
23292
23700
  const features = this.parseFeaturesFromSupport(support);
23293
23701
  const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
23294
23702
  const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
@@ -23325,7 +23733,10 @@ ${xml}`
23325
23733
  ...abilities && {
23326
23734
  abilityMergedKeyCount: Object.keys(abilities).length
23327
23735
  },
23328
- ...support?.items && { supportItemCount: support.items.length }
23736
+ ...support?.items && { supportItemCount: support.items.length },
23737
+ ...dingDongListIds !== void 0 && { dingDongListIds },
23738
+ ...dingDongCfgIds !== void 0 && { dingDongCfgIds },
23739
+ ...wirelessChimeError !== void 0 && { wirelessChimeError }
23329
23740
  };
23330
23741
  const result = {
23331
23742
  capabilities,
@@ -23352,90 +23763,6 @@ ${xml}`
23352
23763
  this.deviceCapabilitiesCache.clear();
23353
23764
  }
23354
23765
  }
23355
- /**
23356
- * Pick the best SupportItem for a channel.
23357
- * Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
23358
- */
23359
- pickBestSupportItem(support, channel) {
23360
- if (!support?.items?.length) return void 0;
23361
- const candidates = support.items.filter((i) => i.chnID === channel);
23362
- if (!candidates.length) return void 0;
23363
- const score = (item) => {
23364
- const anyItem = item;
23365
- let result = 0;
23366
- if (anyItem.name == null) result += 100;
23367
- const capabilityKeys = [
23368
- "ptzType",
23369
- "ptzControl",
23370
- "ptzPreset",
23371
- "ledCtrl",
23372
- "lightType",
23373
- "battery",
23374
- "audioVersion",
23375
- "motion",
23376
- "encCtrl",
23377
- "newIspCfg",
23378
- "remoteAbility",
23379
- "aitype",
23380
- "videoClip",
23381
- "snap"
23382
- ];
23383
- for (const k of capabilityKeys) {
23384
- if (anyItem[k] !== void 0) result += 3;
23385
- }
23386
- return result;
23387
- };
23388
- return candidates.sort((a, b) => score(b) - score(a))[0];
23389
- }
23390
- /**
23391
- * Parse device capabilities from SupportInfo.
23392
- * Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
23393
- */
23394
- parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
23395
- const truthy = (v) => {
23396
- if (typeof v === "number") return v > 0;
23397
- if (typeof v === "string") {
23398
- const n = Number(v);
23399
- return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
23400
- }
23401
- return Boolean(v);
23402
- };
23403
- const item = supportItem;
23404
- const ptzMode = support?.ptzMode?.toLowerCase();
23405
- const ptzType = item ? truthy(item.ptzType) : false;
23406
- const ptzControl = item ? truthy(item.ptzControl) : false;
23407
- const hasPtzFromItem = ptzType || ptzControl;
23408
- const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
23409
- const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
23410
- const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
23411
- const hasPresets = item ? truthy(item.ptzPreset) : false;
23412
- const hasBattery = item ? truthy(item.battery) : false;
23413
- const hasSiren = item ? truthy(item.audioVersion) : false;
23414
- const lightType = item?.lightType;
23415
- const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
23416
- const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
23417
- const isDoorbell = item ? truthy(item.doorbellVersion) : false;
23418
- const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
23419
- return {
23420
- channel,
23421
- ...ptzMode && { ptzMode },
23422
- hasPan: hasPanTilt,
23423
- hasTilt: hasPanTilt,
23424
- hasZoom,
23425
- hasPresets,
23426
- hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
23427
- hasBattery,
23428
- hasIntercom,
23429
- hasSiren,
23430
- hasFloodlight,
23431
- hasPir,
23432
- isDoorbell,
23433
- // Autotracking: explicit flags only (autoPt or smartAI)
23434
- // Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
23435
- // on cameras that have PTZ and AI detection but NOT autotracking capability.
23436
- hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
23437
- };
23438
- }
23439
23766
  /**
23440
23767
  * Parse support features from SupportInfo.
23441
23768
  */
@@ -26308,6 +26635,216 @@ ${scheduleItems}
26308
26635
  const channel = 0;
26309
26636
  return await this.getSnapshot(channel);
26310
26637
  }
26638
+ // --------------------
26639
+ // Chime / DingDong APIs
26640
+ // --------------------
26641
+ /**
26642
+ * Get the list of paired wireless chime devices.
26643
+ * cmd_id: 484 (GetDingDongList)
26644
+ *
26645
+ * @param channel - Channel number (0-based, default 0)
26646
+ * @returns Array of paired chime devices
26647
+ */
26648
+ async getDingDongList(channel) {
26649
+ const ch = this.normalizeChannel(channel);
26650
+ const xml = await this.sendXml({
26651
+ cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
26652
+ channel: ch
26653
+ });
26654
+ return parseDingDongListFromXml(xml);
26655
+ }
26656
+ /**
26657
+ * Get parameters (name, volume, LED state) for a specific wireless chime.
26658
+ * cmd_id: 485 (DingDongOpt, option getParam)
26659
+ *
26660
+ * @param chimeId - The chime device ID
26661
+ * @param channel - Channel number (0-based, default 0)
26662
+ * @returns Chime parameters
26663
+ */
26664
+ async getDingDongParams(chimeId, channel) {
26665
+ const ch = this.normalizeChannel(channel);
26666
+ const payloadXml = buildDingDongGetParamsXml(chimeId);
26667
+ const xml = await this.sendXml({
26668
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
26669
+ channel: ch,
26670
+ payloadXml
26671
+ });
26672
+ return parseDingDongParamsFromXml(xml);
26673
+ }
26674
+ /**
26675
+ * Set parameters (name, volume, LED state) for a specific wireless chime.
26676
+ * cmd_id: 485 (DingDongOpt, option setParam)
26677
+ *
26678
+ * @param chimeId - The chime device ID
26679
+ * @param params - Parameters to set (volLevel, ledState, name)
26680
+ * @param channel - Channel number (0-based, default 0)
26681
+ */
26682
+ async setDingDongParams(chimeId, params, channel) {
26683
+ const ch = this.normalizeChannel(channel);
26684
+ const payloadXml = buildDingDongSetParamsXml(chimeId, params);
26685
+ await this.sendXml({
26686
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
26687
+ channel: ch,
26688
+ payloadXml
26689
+ });
26690
+ }
26691
+ /**
26692
+ * Trigger a wireless chime to ring with a specific ringtone.
26693
+ * cmd_id: 485 (DingDongOpt, option ringWithMusic)
26694
+ *
26695
+ * @param chimeId - The chime device ID
26696
+ * @param musicId - The ringtone/music ID to play
26697
+ * @param channel - Channel number (0-based, default 0)
26698
+ */
26699
+ async ringDingDong(chimeId, musicId, channel) {
26700
+ const ch = this.normalizeChannel(channel);
26701
+ const payloadXml = buildDingDongRingXml(chimeId, musicId);
26702
+ await this.sendXml({
26703
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
26704
+ channel: ch,
26705
+ payloadXml
26706
+ });
26707
+ }
26708
+ /**
26709
+ * Get the per-event alarm configuration for paired wireless chimes.
26710
+ * cmd_id: 486 (GetDingDongCfg)
26711
+ *
26712
+ * @param channel - Channel number (0-based, default 0)
26713
+ * @returns Array of chime configurations (one per paired chime)
26714
+ */
26715
+ async getDingDongCfg(channel) {
26716
+ const ch = this.normalizeChannel(channel);
26717
+ const xml = await this.sendXml({
26718
+ cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
26719
+ channel: ch
26720
+ });
26721
+ return parseDingDongCfgFromXml(xml);
26722
+ }
26723
+ /**
26724
+ * Set the per-event alarm configuration for a specific wireless chime.
26725
+ * cmd_id: 487 (SetDingDongCfg)
26726
+ *
26727
+ * @param chimeId - The chime ring/device ID
26728
+ * @param eventType - Event type string (e.g. "doorbell", "package", "people")
26729
+ * @param state - 0 = disabled, 1 = enabled
26730
+ * @param musicId - Ringtone ID to use for this event type
26731
+ * @param channel - Channel number (0-based, default 0)
26732
+ */
26733
+ async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
26734
+ const ch = this.normalizeChannel(channel);
26735
+ const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
26736
+ await this.sendXml({
26737
+ cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
26738
+ channel: ch,
26739
+ payloadXml
26740
+ });
26741
+ }
26742
+ /** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
26743
+ _hardwiredChimeCache = /* @__PURE__ */ new Map();
26744
+ /**
26745
+ * Get the hardwired (wired-in) chime state.
26746
+ * cmd_id: 483 (GetDingDongCtrl)
26747
+ *
26748
+ * Note: calling this may briefly trigger the physical chime to rattle.
26749
+ *
26750
+ * @param channel - Channel number (0-based, default 0)
26751
+ * @returns Hardwired chime state (type, enabled, time)
26752
+ */
26753
+ async getHardwiredChime(channel) {
26754
+ const ch = this.normalizeChannel(channel);
26755
+ const payloadXml = buildGetDingDongCtrlXml();
26756
+ const xml = await this.sendXml({
26757
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
26758
+ channel: ch,
26759
+ payloadXml
26760
+ });
26761
+ const state = parseHardwiredChimeFromXml(xml);
26762
+ this._hardwiredChimeCache.set(ch, state);
26763
+ return state;
26764
+ }
26765
+ /**
26766
+ * Set the hardwired (wired-in) chime state.
26767
+ * cmd_id: 483 (SetDingDongCtrl)
26768
+ *
26769
+ * Uses the cached state from a previous getHardwiredChime call to fill in
26770
+ * missing type/time fields, avoiding a double round-trip on every set.
26771
+ * Falls back to fetching if no cache is available.
26772
+ *
26773
+ * @param params - Chime configuration (type, enabled, time)
26774
+ * @param channel - Channel number (0-based, default 0)
26775
+ */
26776
+ async setHardwiredChime(params, channel) {
26777
+ const ch = this.normalizeChannel(channel);
26778
+ let current = this._hardwiredChimeCache.get(ch);
26779
+ if (!current) {
26780
+ current = await this.getHardwiredChime(ch);
26781
+ }
26782
+ const chimeType = params.type ?? current.type;
26783
+ const enabled = params.enabled ? 1 : 0;
26784
+ const time = params.time ?? current.time;
26785
+ const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
26786
+ const xml = await this.sendXml({
26787
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
26788
+ channel: ch,
26789
+ payloadXml
26790
+ });
26791
+ const newState = parseHardwiredChimeFromXml(xml);
26792
+ this._hardwiredChimeCache.set(ch, newState);
26793
+ return newState;
26794
+ }
26795
+ /**
26796
+ * Play an audio file on the doorbell / chime device.
26797
+ * cmd_id: 349 (QuickReplyPlay)
26798
+ *
26799
+ * @param fileId - The audio file ID to play
26800
+ * @param channel - Channel number (0-based, default 0)
26801
+ */
26802
+ async quickReplyPlay(fileId, channel) {
26803
+ const ch = this.normalizeChannel(channel);
26804
+ const payloadXml = buildQuickReplyPlayXml(ch, fileId);
26805
+ await this.sendXml({
26806
+ cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
26807
+ channel: ch,
26808
+ payloadXml
26809
+ });
26810
+ }
26811
+ /**
26812
+ * Get the silent mode state of a paired wireless chime.
26813
+ * cmd_id: 609 (GetDingDongSilent)
26814
+ *
26815
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
26816
+ * @param channel - Channel number (0-based, default 0)
26817
+ * @returns Wireless chime silent state (time=0 means active/not silenced)
26818
+ */
26819
+ async getDingDongSilent(chimeId, channel) {
26820
+ const ch = this.normalizeChannel(channel);
26821
+ const payloadXml = buildGetDingDongSilentXml(chimeId);
26822
+ const xml = await this.sendXml({
26823
+ cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
26824
+ channel: ch,
26825
+ payloadXml
26826
+ });
26827
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
26828
+ }
26829
+ /**
26830
+ * Set the silent mode of a paired wireless chime.
26831
+ * cmd_id: 610 (SetDingDongSilent)
26832
+ *
26833
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
26834
+ * @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
26835
+ * @param channel - Channel number (0-based, default 0)
26836
+ * @returns Updated wireless chime silent state
26837
+ */
26838
+ async setDingDongSilent(chimeId, time, channel) {
26839
+ const ch = this.normalizeChannel(channel);
26840
+ const payloadXml = buildSetDingDongSilentXml(chimeId, time);
26841
+ const xml = await this.sendXml({
26842
+ cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
26843
+ channel: ch,
26844
+ payloadXml
26845
+ });
26846
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
26847
+ }
26311
26848
  };
26312
26849
 
26313
26850
  // src/reolink/autodetect.ts