@apocaliss92/nodelink-js 0.4.10 → 0.4.12

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_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, 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;
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_SET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_VERSION_INFO, 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_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, 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";
@@ -126,9 +126,11 @@ var init_constants = __esm({
126
126
  BC_CMD_ID_PING = 93;
127
127
  BC_CMD_ID_CHANNEL_INFO_ALL = 145;
128
128
  BC_CMD_ID_GET_OSD_DATETIME = 44;
129
+ BC_CMD_ID_SET_OSD_DATETIME = 45;
129
130
  BC_CMD_ID_GET_RECORD_CFG = 54;
130
131
  BC_CMD_ID_GET_ABILITY_SUPPORT = 58;
131
132
  BC_CMD_ID_GET_FTP_TASK = 70;
133
+ BC_CMD_ID_GET_VERSION_INFO = 80;
132
134
  BC_CMD_ID_GET_RECORD = 81;
133
135
  BC_CMD_ID_GET_HDD_INFO_LIST = 102;
134
136
  BC_CMD_ID_GET_WIFI_SIGNAL = 115;
@@ -488,10 +490,15 @@ var init_BcMediaCodec = __esm({
488
490
  strict;
489
491
  amountSkipped = 0;
490
492
  logger;
493
+ onUnknownChunk;
491
494
  constructor(strict = false, logger) {
492
495
  this.strict = strict;
493
496
  this.logger = logger;
494
497
  }
498
+ /** Register a listener that fires for every unknown chunk before recovery. */
499
+ setUnknownChunkListener(listener) {
500
+ this.onUnknownChunk = listener;
501
+ }
495
502
  /**
496
503
  * Push data into the codec buffer and try to parse complete BcMedia packets.
497
504
  * Returns an array of complete BcMedia packets found.
@@ -552,6 +559,13 @@ var init_BcMediaCodec = __esm({
552
559
  }
553
560
  }
554
561
  if (next > 0) {
562
+ if (this.onUnknownChunk) {
563
+ this.onUnknownChunk({
564
+ magic,
565
+ preview: Buffer.from(this.buffer.subarray(0, Math.min(256, next))),
566
+ skipped: next
567
+ });
568
+ }
555
569
  this.amountSkipped += next;
556
570
  this.buffer = this.buffer.subarray(next);
557
571
  continue;
@@ -1414,6 +1428,10 @@ var init_BcMediaAnnexBDecoder = __esm({
1414
1428
  });
1415
1429
 
1416
1430
  // src/baichuan/stream/BaichuanVideoStream.ts
1431
+ var BaichuanVideoStream_exports = {};
1432
+ __export(BaichuanVideoStream_exports, {
1433
+ BaichuanVideoStream: () => BaichuanVideoStream
1434
+ });
1417
1435
  function removeEmulationPreventionBytes(rbsp) {
1418
1436
  const out = [];
1419
1437
  for (let i = 0; i < rbsp.length; i++) {
@@ -1547,6 +1565,15 @@ var init_BaichuanVideoStream = __esm({
1547
1565
  acceptAnyStreamType;
1548
1566
  lockedChannelId;
1549
1567
  bcMediaCodec;
1568
+ /**
1569
+ * Diagnostic-only accessor for the BcMedia codec. Used by tools that need to
1570
+ * inspect unknown chunks (for example to discover undocumented audio
1571
+ * sub-packets the parser currently skips). Not part of the supported public
1572
+ * surface — do not rely on it in application code.
1573
+ */
1574
+ get _bcMediaCodec() {
1575
+ return this.bcMediaCodec;
1576
+ }
1550
1577
  debugH264LogsLeft;
1551
1578
  debugSavedSamples;
1552
1579
  warnedNonAnnexBOnce = false;
@@ -1578,6 +1605,14 @@ var init_BaichuanVideoStream = __esm({
1578
1605
  // Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
1579
1606
  // In CFB mode, continuation frames must use the cipher state from previous frames.
1580
1607
  aesStreamDecryptor = null;
1608
+ // Latest frame dimensions reported by BcMedia InfoV1/V2 packets.
1609
+ // Used to attach width/height context to the `additionalHeader` event so
1610
+ // consumers can normalize box coordinates to a fraction of the stream size.
1611
+ latestFrameWidth;
1612
+ latestFrameHeight;
1613
+ // Teardown returned by ReolinkBaichuanApi._registerVideoStreamForDetection.
1614
+ // Called from stop() to detach the detection bridge.
1615
+ detectionTeardown;
1581
1616
  /**
1582
1617
  * Pending startup error stashed when emitSafeError is called before any
1583
1618
  * "error" listener is registered (e.g. camera returns 400 during start()).
@@ -2240,6 +2275,16 @@ var init_BaichuanVideoStream = __esm({
2240
2275
  }
2241
2276
  videoType = detectedCodec;
2242
2277
  }
2278
+ if (media.additionalHeader && media.additionalHeader.length > 0) {
2279
+ this.emit("additionalHeader", {
2280
+ raw: media.additionalHeader,
2281
+ frameType: "Iframe",
2282
+ videoType,
2283
+ microseconds: media.microseconds,
2284
+ ...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
2285
+ ...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
2286
+ });
2287
+ }
2243
2288
  const annexBData = videoType === "H265" ? convertToAnnexB2(media.data) : convertToAnnexB(media.data);
2244
2289
  const isKeyframe = true;
2245
2290
  maybeCacheParamSets(annexBData, "Iframe", videoType);
@@ -2321,6 +2366,18 @@ var init_BaichuanVideoStream = __esm({
2321
2366
  }
2322
2367
  } else if (media.type === "Pframe") {
2323
2368
  const chunk = media.data;
2369
+ if (media.additionalHeader && media.additionalHeader.length > 0) {
2370
+ const detected = detectVideoCodecFromNal(chunk);
2371
+ const videoTypeForHeader = detected ?? media.videoType;
2372
+ this.emit("additionalHeader", {
2373
+ raw: media.additionalHeader,
2374
+ frameType: "Pframe",
2375
+ videoType: videoTypeForHeader,
2376
+ microseconds: media.microseconds,
2377
+ ...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
2378
+ ...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
2379
+ });
2380
+ }
2324
2381
  let videoType = media.videoType;
2325
2382
  const detectedCodec = detectVideoCodecFromNal(chunk);
2326
2383
  if (detectedCodec && detectedCodec !== videoType) {
@@ -2363,6 +2420,8 @@ var init_BaichuanVideoStream = __esm({
2363
2420
  this.emit("audioFrame", media.data);
2364
2421
  }
2365
2422
  if (media.type === "InfoV1" || media.type === "InfoV2") {
2423
+ if (media.videoWidth > 0) this.latestFrameWidth = media.videoWidth;
2424
+ if (media.videoHeight > 0) this.latestFrameHeight = media.videoHeight;
2366
2425
  }
2367
2426
  }
2368
2427
  if (totalFramesReceived <= 10 || totalFramesReceived % 20 === 0 && (videoFramesEmitted > 0 || audioFramesEmitted > 0)) {
@@ -2382,6 +2441,12 @@ var init_BaichuanVideoStream = __esm({
2382
2441
  this.client.on("push", this.videoFrameHandler);
2383
2442
  this.active = true;
2384
2443
  this.startWatchdog();
2444
+ if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
2445
+ this.detectionTeardown = this.api._registerVideoStreamForDetection(this, {
2446
+ channel: this.channel,
2447
+ profile: this.profile
2448
+ });
2449
+ }
2385
2450
  this.lastMediaAtMs = Date.now();
2386
2451
  if (this.api) {
2387
2452
  try {
@@ -2479,6 +2544,13 @@ var init_BaichuanVideoStream = __esm({
2479
2544
  }
2480
2545
  }
2481
2546
  this.active = false;
2547
+ if (this.detectionTeardown) {
2548
+ try {
2549
+ this.detectionTeardown();
2550
+ } catch {
2551
+ }
2552
+ this.detectionTeardown = void 0;
2553
+ }
2482
2554
  this.emit("close");
2483
2555
  }
2484
2556
  isActive() {
@@ -2714,6 +2786,15 @@ function applyXmlTagPatch(xml, tag, value) {
2714
2786
  const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2715
2787
  return xml.replace(re, `<${tag}>${v}</${tag}>`);
2716
2788
  }
2789
+ function upsertXmlTag(xml, tag, value) {
2790
+ if (value === void 0) return xml;
2791
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2792
+ const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2793
+ if (re.test(xml)) {
2794
+ return xml.replace(re, `<${tag}>${v}</${tag}>`);
2795
+ }
2796
+ return `${xml}<${tag}>${v}</${tag}>`;
2797
+ }
2717
2798
  function patchNestedTag(xml, parent, child, value) {
2718
2799
  if (value === void 0) return xml;
2719
2800
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
@@ -2729,6 +2810,15 @@ function applyStreamPatch(xml, streamTag, patch) {
2729
2810
  );
2730
2811
  return xml.replace(re, (_match, open, body, close) => {
2731
2812
  let next = body;
2813
+ if (patch.audio !== void 0) {
2814
+ next = applyXmlTagPatch(next, "audio", patch.audio);
2815
+ }
2816
+ if (patch.width !== void 0) {
2817
+ next = applyXmlTagPatch(next, "width", patch.width);
2818
+ }
2819
+ if (patch.height !== void 0) {
2820
+ next = applyXmlTagPatch(next, "height", patch.height);
2821
+ }
2732
2822
  if (patch.bitRate !== void 0) {
2733
2823
  next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
2734
2824
  }
@@ -2740,6 +2830,23 @@ function applyStreamPatch(xml, streamTag, patch) {
2740
2830
  const intVal = patch.videoEncType === "h265" ? 1 : 0;
2741
2831
  next = applyXmlTagPatch(next, "videoEncType", intVal);
2742
2832
  }
2833
+ if (patch.encoderType !== void 0) {
2834
+ next = upsertXmlTag(next, "encoderType", patch.encoderType);
2835
+ }
2836
+ if (patch.encoderProfile !== void 0) {
2837
+ next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
2838
+ }
2839
+ if (patch.gop !== void 0) {
2840
+ const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
2841
+ if (gopBlockRe.test(next)) {
2842
+ next = next.replace(
2843
+ gopBlockRe,
2844
+ (_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
2845
+ );
2846
+ } else {
2847
+ next = `${next}<gop><cur>${patch.gop}</cur></gop>`;
2848
+ }
2849
+ }
2743
2850
  return `${open}${next}${close}`;
2744
2851
  });
2745
2852
  }
@@ -12313,6 +12420,23 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12313
12420
  * even if the current client instance is idle/disconnected.
12314
12421
  */
12315
12422
  static streamingRegistry = /* @__PURE__ */ new Map();
12423
+ /**
12424
+ * Per-device set of live BaichuanClient instances.
12425
+ *
12426
+ * Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
12427
+ * and SocketPool tears the streaming socket down), the global streaming
12428
+ * registry decrements but the GENERAL client of the same device has no
12429
+ * way of knowing — its idle-disconnect timer was last evaluated while
12430
+ * `isDeviceStreamingActive()` was still true (because the streaming socket
12431
+ * was still alive) and wasn't rescheduled. Without this registry the
12432
+ * general socket stays connected, the 60-second session-guard timer keeps
12433
+ * sending getOnlineUserList() to the camera, and a battery camera ends up
12434
+ * waking up every minute (issue #18).
12435
+ *
12436
+ * On streamingRegistry decrement-to-zero we walk this set and kick every
12437
+ * sibling's idle-disconnect timer so it can re-evaluate eligibility.
12438
+ */
12439
+ static deviceClients = /* @__PURE__ */ new Map();
12316
12440
  /**
12317
12441
  * Per-host D2C_DISC backoff state that persists across client instance recreation.
12318
12442
  *
@@ -12427,6 +12551,29 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12427
12551
  // AlarmEventList (cmdId=33) can be very chatty (often sent every second).
12428
12552
  // Track last per-channel alarm state so we only emit on transitions.
12429
12553
  alarmEventState = /* @__PURE__ */ new Map();
12554
+ /** Whether this instance is currently in BaichuanClient.deviceClients. */
12555
+ registeredInDeviceClients = false;
12556
+ registerInDeviceClients() {
12557
+ if (this.registeredInDeviceClients) return;
12558
+ const key = this.getDeviceRegistryKey();
12559
+ let set = _BaichuanClient.deviceClients.get(key);
12560
+ if (!set) {
12561
+ set = /* @__PURE__ */ new Set();
12562
+ _BaichuanClient.deviceClients.set(key, set);
12563
+ }
12564
+ set.add(this);
12565
+ this.registeredInDeviceClients = true;
12566
+ }
12567
+ unregisterFromDeviceClients() {
12568
+ if (!this.registeredInDeviceClients) return;
12569
+ const key = this.getDeviceRegistryKey();
12570
+ const set = _BaichuanClient.deviceClients.get(key);
12571
+ if (set) {
12572
+ set.delete(this);
12573
+ if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
12574
+ }
12575
+ this.registeredInDeviceClients = false;
12576
+ }
12430
12577
  constructor(options) {
12431
12578
  super();
12432
12579
  this.opts = options;
@@ -12441,6 +12588,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12441
12588
  code: err?.code
12442
12589
  });
12443
12590
  });
12591
+ this.registerInDeviceClients();
12444
12592
  }
12445
12593
  newSocketSessionId(transport) {
12446
12594
  const short = (0, import_node_crypto2.randomUUID)().split("-")[0] ?? (0, import_node_crypto2.randomUUID)().slice(0, 8);
@@ -12697,6 +12845,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12697
12845
  activeStreamClients: nextCount
12698
12846
  });
12699
12847
  this.contributesToGlobalStreamingRegistry = shouldContribute;
12848
+ if (!shouldContribute && nextCount === 0) {
12849
+ const siblings = _BaichuanClient.deviceClients.get(key);
12850
+ if (siblings) {
12851
+ for (const sib of siblings) {
12852
+ if (sib === this) continue;
12853
+ try {
12854
+ sib.kickIdleDisconnectTimer();
12855
+ } catch {
12856
+ }
12857
+ }
12858
+ }
12859
+ }
12700
12860
  }
12701
12861
  /**
12702
12862
  * True if the device should be considered "awake" due to active streaming.
@@ -13161,6 +13321,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
13161
13321
  `transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
13162
13322
  );
13163
13323
  this.logSocketState("tcp_connected");
13324
+ this.registerInDeviceClients();
13164
13325
  this.startKeepAlive();
13165
13326
  this.kickIdleDisconnectTimer();
13166
13327
  }
@@ -13477,6 +13638,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
13477
13638
  this.logDebug("udp_close_error", e);
13478
13639
  }
13479
13640
  }
13641
+ this.unregisterFromDeviceClients();
13480
13642
  }
13481
13643
  handleFrame(frame) {
13482
13644
  const now = Date.now();
@@ -15804,6 +15966,32 @@ var parseAbilityInfoXml = (xml) => {
15804
15966
  return abilities;
15805
15967
  };
15806
15968
 
15969
+ // src/reolink/baichuan/utils/versionInfo.ts
15970
+ init_xml();
15971
+ function parseVersionInfo(xml) {
15972
+ const out = {};
15973
+ const set = (key) => {
15974
+ const v = getXmlText(xml, key);
15975
+ if (v !== void 0) out[key] = v;
15976
+ };
15977
+ set("name");
15978
+ set("type");
15979
+ set("serialNumber");
15980
+ set("buildDay");
15981
+ set("hardwareVersion");
15982
+ set("cfgVersion");
15983
+ set("firmwareVersion");
15984
+ set("detail");
15985
+ set("IEClient");
15986
+ set("cc3200Version");
15987
+ set("spVersion");
15988
+ set("pakSuffix");
15989
+ set("itemNo");
15990
+ set("aiVersion");
15991
+ set("helpVersion");
15992
+ return out;
15993
+ }
15994
+
15807
15995
  // src/reolink/baichuan/utils/aiState.ts
15808
15996
  init_constants();
15809
15997
  init_xml();
@@ -16110,6 +16298,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
16110
16298
  return { result: resultObj, storedChannels: Object.keys(resultObj) };
16111
16299
  };
16112
16300
 
16301
+ // src/reolink/baichuan/utils/detection.ts
16302
+ var lz4 = __toESM(require("lz4js"), 1);
16303
+
16304
+ // src/reolink/baichuan/utils/aiClassMap.ts
16305
+ function type1ToLabel(type1) {
16306
+ switch (type1) {
16307
+ case 1:
16308
+ return "people";
16309
+ case 2:
16310
+ return "vehicle";
16311
+ case 3:
16312
+ return "animal";
16313
+ case 11259375:
16314
+ return "face";
16315
+ default:
16316
+ return "unknown";
16317
+ }
16318
+ }
16319
+
16320
+ // src/reolink/baichuan/utils/detection.ts
16321
+ var MARKER_LENGTH = 8;
16322
+ var IFRAME_PREFIX_LENGTH = 8;
16323
+ var COUNTER_OFFSET = 8;
16324
+ var BASELINE_SIZE = 128;
16325
+ var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
16326
+ var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
16327
+ var CONFIDENCE_DIVISOR = 100;
16328
+ var DEFAULT_AI_FRAME_WIDTH = 896;
16329
+ var DEFAULT_AI_FRAME_HEIGHT = 480;
16330
+ var LZ4_DECOMPRESS_MAX = 256 * 1024;
16331
+ function walkBoxes(buf, start, end, type1, type2, out) {
16332
+ let pos = start;
16333
+ while (pos + 3 <= end) {
16334
+ const t = buf[pos];
16335
+ if (t === 0) return;
16336
+ const length = buf[pos + 1] | buf[pos + 2] << 8;
16337
+ const recordEnd = pos + 3 + length;
16338
+ if (recordEnd > end) return;
16339
+ const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
16340
+ const isBoxType2 = t === 2 && length === 10;
16341
+ if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
16342
+ const x1 = buf.readUInt16LE(pos + 3);
16343
+ const y1 = buf.readUInt16LE(pos + 5);
16344
+ const x2 = buf.readUInt16LE(pos + 7);
16345
+ const y2 = buf.readUInt16LE(pos + 9);
16346
+ const conf = buf.readUInt16LE(pos + 11);
16347
+ if (x2 > x1 && y2 > y1) {
16348
+ out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
16349
+ }
16350
+ pos = recordEnd;
16351
+ continue;
16352
+ }
16353
+ if (type1 === 255 && type2 === 2 && t === 2 && length >= LZ4F_MAGIC.length && buf[pos + 3] === LZ4F_MAGIC[0] && buf[pos + 4] === LZ4F_MAGIC[1] && buf[pos + 5] === LZ4F_MAGIC[2] && buf[pos + 6] === LZ4F_MAGIC[3]) {
16354
+ try {
16355
+ const decompressed = lz4.decompress(
16356
+ buf.subarray(pos + 3, recordEnd),
16357
+ LZ4_DECOMPRESS_MAX
16358
+ );
16359
+ const decBuf = Buffer.from(decompressed);
16360
+ walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
16361
+ } catch {
16362
+ }
16363
+ pos = recordEnd;
16364
+ continue;
16365
+ }
16366
+ if (length > 0) {
16367
+ let nextT1 = type1;
16368
+ let nextT2 = type2;
16369
+ if (type1 === 0) nextT1 = t;
16370
+ else if (type2 === 0) nextT2 = t;
16371
+ walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
16372
+ }
16373
+ pos = recordEnd;
16374
+ }
16375
+ }
16376
+ function decodeDetectionHeader(raw, frameType) {
16377
+ const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
16378
+ const blockLength = raw.length - markerOffset;
16379
+ const empty = {
16380
+ state: "invalid-marker",
16381
+ markerOffset,
16382
+ blockLength,
16383
+ boxes: []
16384
+ };
16385
+ if (blockLength < MARKER_LENGTH) return empty;
16386
+ if (!hasStandardMarker(raw, markerOffset)) return empty;
16387
+ if (blockLength < COUNTER_OFFSET + 4) return empty;
16388
+ const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
16389
+ const rawBoxes = [];
16390
+ walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
16391
+ let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
16392
+ let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
16393
+ let frameSizeFound = false;
16394
+ const searchStart = markerOffset + MARKER_LENGTH;
16395
+ for (let i = searchStart; i + 7 <= raw.length; i++) {
16396
+ if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
16397
+ const w = raw.readUInt16LE(i + 3);
16398
+ const h = raw.readUInt16LE(i + 5);
16399
+ if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
16400
+ aiFrameWidth = w;
16401
+ aiFrameHeight = h;
16402
+ frameSizeFound = true;
16403
+ break;
16404
+ }
16405
+ }
16406
+ }
16407
+ const specificity = {
16408
+ face: 4,
16409
+ animal: 3,
16410
+ people: 2,
16411
+ vehicle: 1,
16412
+ unknown: 0
16413
+ };
16414
+ const dedup = /* @__PURE__ */ new Map();
16415
+ for (const rb of rawBoxes) {
16416
+ if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
16417
+ const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
16418
+ const prev = dedup.get(key);
16419
+ if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
16420
+ dedup.set(key, rb);
16421
+ }
16422
+ }
16423
+ const boxes = [];
16424
+ for (const rb of dedup.values()) {
16425
+ boxes.push({
16426
+ x: rb.x1 / aiFrameWidth,
16427
+ y: rb.y1 / aiFrameHeight,
16428
+ width: (rb.x2 - rb.x1) / aiFrameWidth,
16429
+ height: (rb.y2 - rb.y1) / aiFrameHeight,
16430
+ ...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
16431
+ ...rb.label !== "unknown" ? { label: rb.label } : {}
16432
+ });
16433
+ }
16434
+ let state;
16435
+ if (boxes.length > 0) state = "overlay-decoded";
16436
+ else if (blockLength === BASELINE_SIZE) state = "no-overlay";
16437
+ else state = "overlay-undecoded";
16438
+ return {
16439
+ state,
16440
+ markerOffset,
16441
+ blockLength,
16442
+ counter,
16443
+ ...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
16444
+ boxes
16445
+ };
16446
+ }
16447
+ function hasStandardMarker(raw, offset) {
16448
+ if (raw.length < offset + MARKER_LENGTH) return false;
16449
+ return raw[offset] === 255 && raw[offset + 2] === 0 && raw[offset + 3] === 1 && raw[offset + 4] === 11 && raw[offset + 5] === 0 && raw[offset + 6] === 1 && raw[offset + 7] === 8;
16450
+ }
16451
+
16452
+ // src/reolink/baichuan/utils/encOptions.ts
16453
+ function buildEncOptions(list, channel) {
16454
+ const result = { channel };
16455
+ const main2 = aggregateByType(list, "mainStream");
16456
+ const sub = aggregateByType(list, "subStream");
16457
+ const third = aggregateByType(list, "thirdStream");
16458
+ if (main2) result.mainStream = main2;
16459
+ if (sub) result.subStream = sub;
16460
+ if (third) result.thirdStream = third;
16461
+ return result;
16462
+ }
16463
+ function aggregateByType(list, type) {
16464
+ const seen = /* @__PURE__ */ new Map();
16465
+ for (const stream of list.streams) {
16466
+ for (const eb of stream.encodeTables) {
16467
+ if (eb.type !== type) continue;
16468
+ const mapped = mapEncodeTable(eb);
16469
+ if (!mapped) continue;
16470
+ const key = `${mapped.width}x${mapped.height}`;
16471
+ if (!seen.has(key)) seen.set(key, mapped);
16472
+ }
16473
+ }
16474
+ if (seen.size === 0) return void 0;
16475
+ return {
16476
+ type,
16477
+ resolutions: [...seen.values()],
16478
+ encoderTypes: ["vbr", "cbr"],
16479
+ encoderProfiles: ["high", "main", "baseline"]
16480
+ };
16481
+ }
16482
+ function mapEncodeTable(eb) {
16483
+ if (eb.width == null || eb.height == null) return void 0;
16484
+ const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
16485
+ (t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
16486
+ ).filter((t) => t !== void 0);
16487
+ return {
16488
+ width: eb.width,
16489
+ height: eb.height,
16490
+ videoEncTypes,
16491
+ ...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
16492
+ ...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
16493
+ ...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
16494
+ framerateOptions: eb.framerateTable ?? [],
16495
+ bitrateOptions: eb.bitrateTable ?? []
16496
+ };
16497
+ }
16498
+
16113
16499
  // src/reolink/baichuan/utils/events.ts
16114
16500
  var mapToSimpleEvent = (event) => {
16115
16501
  const timestamp = event.timestamp ?? Date.now();
@@ -18233,6 +18619,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18233
18619
  sessionGuardIntervalTimer;
18234
18620
  simpleEventListeners = /* @__PURE__ */ new Set();
18235
18621
  simpleEventSubscribed = false;
18622
+ // Detection events are sourced from BcMedia additionalHeader on active video
18623
+ // streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
18624
+ // data flows whenever a stream is open. Active streams register themselves via
18625
+ // _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
18626
+ detectionEventListeners = /* @__PURE__ */ new Set();
18627
+ detectionEventStreamHooks = /* @__PURE__ */ new Map();
18628
+ // Auto-managed substream for `onObjectDetections` listeners. Reference-counted
18629
+ // by the listener set: the substream is opened on the first listener and torn
18630
+ // down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
18631
+ // lifecycle so a caller never has to manage a video stream just to read AI
18632
+ // detections.
18633
+ objectDetectionListeners = /* @__PURE__ */ new Set();
18634
+ objectDetectionStream;
18635
+ objectDetectionStreamStartInFlight;
18636
+ objectDetectionInternalListener;
18236
18637
  simpleEventSubscribeInFlight;
18237
18638
  simpleEventUnsubscribeInFlight;
18238
18639
  simpleEventResubscribeTimer;
@@ -19668,6 +20069,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19668
20069
  }
19669
20070
  }
19670
20071
  }
20072
+ /**
20073
+ * Subscribe to per-frame detection events sourced from the BcMedia
20074
+ * `additionalHeader` block on active video streams.
20075
+ *
20076
+ * Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
20077
+ * one event fires for every I-frame / P-frame that carries an overlay block.
20078
+ * Coordinates are reported in normalized [0, 1] fractions of the source
20079
+ * frame, so the same box renders correctly on mainStream, subStream, and
20080
+ * externStream.
20081
+ *
20082
+ * Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
20083
+ * only flow while a video stream is open. The library hooks every
20084
+ * `BaichuanVideoStream` created via this API for the listener's lifetime.
20085
+ */
20086
+ onDetection(callback) {
20087
+ this.detectionEventListeners.add(callback);
20088
+ }
20089
+ /**
20090
+ * Remove a single detection callback, or all of them if `callback` is omitted.
20091
+ */
20092
+ offDetection(callback) {
20093
+ if (callback) {
20094
+ this.detectionEventListeners.delete(callback);
20095
+ } else {
20096
+ this.detectionEventListeners.clear();
20097
+ }
20098
+ }
20099
+ /**
20100
+ * Subscribe to AI object detections (people / vehicle / animal / face boxes
20101
+ * with class label and confidence) without managing a video stream yourself.
20102
+ *
20103
+ * Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
20104
+ * substream behind the scenes on the first listener, forwards every box-bearing
20105
+ * `additionalHeader` to your callback, and tears the stream down when the last
20106
+ * listener unsubscribes. The substream is the lightest profile (typically
20107
+ * 640×360) so the additional bandwidth/CPU overhead is minimal.
20108
+ *
20109
+ * Each event carries normalized `[0, 1]` box coordinates, a class label, and
20110
+ * a confidence score — render-ready without further conversion.
20111
+ */
20112
+ async onObjectDetections(callback) {
20113
+ this.objectDetectionListeners.add(callback);
20114
+ this.logger.debug?.(
20115
+ `[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
20116
+ );
20117
+ await this.ensureObjectDetectionStream();
20118
+ }
20119
+ /**
20120
+ * Remove one detection callback, or all of them if `callback` is omitted.
20121
+ * When the last listener is removed the auto-managed substream is closed.
20122
+ */
20123
+ async offObjectDetections(callback) {
20124
+ if (callback) {
20125
+ this.objectDetectionListeners.delete(callback);
20126
+ } else {
20127
+ this.objectDetectionListeners.clear();
20128
+ }
20129
+ if (this.objectDetectionListeners.size === 0) {
20130
+ await this.tearDownObjectDetectionStream();
20131
+ }
20132
+ }
20133
+ async ensureObjectDetectionStream() {
20134
+ if (this.objectDetectionStream) return;
20135
+ if (this.objectDetectionStreamStartInFlight) {
20136
+ await this.objectDetectionStreamStartInFlight;
20137
+ return;
20138
+ }
20139
+ this.objectDetectionStreamStartInFlight = (async () => {
20140
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => (init_BaichuanVideoStream(), BaichuanVideoStream_exports));
20141
+ const sessionKey = `live:object-detections:ch0:sub`;
20142
+ const dedicated = await this.createDedicatedSession(sessionKey);
20143
+ const stream = new BaichuanVideoStream2({
20144
+ client: dedicated.client,
20145
+ api: this,
20146
+ channel: 0,
20147
+ profile: "sub",
20148
+ logger: this.logger
20149
+ });
20150
+ this.objectDetectionInternalListener = (event) => {
20151
+ for (const cb of this.objectDetectionListeners) {
20152
+ try {
20153
+ void Promise.resolve(cb(event)).catch((e) => {
20154
+ (this.logger.warn ?? this.logger.error).call(
20155
+ this.logger,
20156
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
20157
+ formatErrorForLog(e)
20158
+ );
20159
+ });
20160
+ } catch (e) {
20161
+ (this.logger.warn ?? this.logger.error).call(
20162
+ this.logger,
20163
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
20164
+ formatErrorForLog(e)
20165
+ );
20166
+ }
20167
+ }
20168
+ };
20169
+ this.detectionEventListeners.add(this.objectDetectionInternalListener);
20170
+ try {
20171
+ await stream.start();
20172
+ } catch (e) {
20173
+ if (this.objectDetectionInternalListener) {
20174
+ this.detectionEventListeners.delete(
20175
+ this.objectDetectionInternalListener
20176
+ );
20177
+ this.objectDetectionInternalListener = void 0;
20178
+ }
20179
+ await dedicated.release().catch(() => {
20180
+ });
20181
+ throw e;
20182
+ }
20183
+ this.objectDetectionStream = {
20184
+ stop: () => stream.stop(),
20185
+ release: () => dedicated.release()
20186
+ };
20187
+ this.logger.debug?.(
20188
+ `[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
20189
+ );
20190
+ })();
20191
+ try {
20192
+ await this.objectDetectionStreamStartInFlight;
20193
+ } finally {
20194
+ this.objectDetectionStreamStartInFlight = void 0;
20195
+ }
20196
+ }
20197
+ async tearDownObjectDetectionStream() {
20198
+ const handle = this.objectDetectionStream;
20199
+ this.objectDetectionStream = void 0;
20200
+ if (this.objectDetectionInternalListener) {
20201
+ this.detectionEventListeners.delete(this.objectDetectionInternalListener);
20202
+ this.objectDetectionInternalListener = void 0;
20203
+ }
20204
+ if (!handle) return;
20205
+ try {
20206
+ await handle.stop();
20207
+ } catch (e) {
20208
+ this.logger.debug?.(
20209
+ `[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
20210
+ );
20211
+ }
20212
+ try {
20213
+ await handle.release();
20214
+ } catch (e) {
20215
+ this.logger.debug?.(
20216
+ `[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
20217
+ );
20218
+ }
20219
+ this.logger.debug?.(
20220
+ `[ReolinkBaichuanApi] onObjectDetections: substream torn down`
20221
+ );
20222
+ }
20223
+ /**
20224
+ * Internal: invoked by BaichuanVideoStream when it starts so the API can hook
20225
+ * its `additionalHeader` event. Returns a teardown function the stream calls
20226
+ * on stop. Not intended for direct use by consumers.
20227
+ */
20228
+ _registerVideoStreamForDetection(stream, context) {
20229
+ const listener = (info) => {
20230
+ if (this.detectionEventListeners.size === 0) return;
20231
+ const decoded = decodeDetectionHeader(info.raw, info.frameType);
20232
+ const event = {
20233
+ channel: context.channel,
20234
+ microseconds: info.microseconds,
20235
+ profile: context.profile,
20236
+ boxes: decoded.boxes,
20237
+ ...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
20238
+ ...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
20239
+ decodeState: decoded.state,
20240
+ rawHeader: info.raw
20241
+ };
20242
+ this.dispatchDetectionEvent(event);
20243
+ };
20244
+ stream.on("additionalHeader", listener);
20245
+ const teardown = () => {
20246
+ stream.off("additionalHeader", listener);
20247
+ this.detectionEventStreamHooks.delete(stream);
20248
+ };
20249
+ this.detectionEventStreamHooks.set(stream, teardown);
20250
+ return teardown;
20251
+ }
20252
+ dispatchDetectionEvent(evt) {
20253
+ for (const cb of this.detectionEventListeners) {
20254
+ try {
20255
+ void Promise.resolve(cb(evt)).catch((e) => {
20256
+ (this.logger.warn ?? this.logger.error).call(
20257
+ this.logger,
20258
+ "[ReolinkBaichuanApi] onDetection handler error",
20259
+ formatErrorForLog(e)
20260
+ );
20261
+ });
20262
+ } catch (e) {
20263
+ (this.logger.warn ?? this.logger.error).call(
20264
+ this.logger,
20265
+ "[ReolinkBaichuanApi] onDetection handler error",
20266
+ formatErrorForLog(e)
20267
+ );
20268
+ }
20269
+ }
20270
+ }
19671
20271
  startSimpleEventResubscribeTimer() {
19672
20272
  if (this.simpleEventResubscribeTimer) return;
19673
20273
  if (this.simpleEventListeners.size === 0) return;
@@ -20050,6 +20650,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
20050
20650
  this.stopUdpSleepInference();
20051
20651
  this.stopSimpleEventWatchdog();
20052
20652
  this.stopSimpleEventResubscribeTimer();
20653
+ this.objectDetectionListeners.clear();
20654
+ await this.tearDownObjectDetectionStream().catch(() => {
20655
+ });
20053
20656
  await this.cleanup();
20054
20657
  await this.stopAllActiveStreams();
20055
20658
  await this.cleanupSocketPool();
@@ -20397,6 +21000,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
20397
21000
  const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
20398
21001
  await this.sendXml({ cmdId: 36, payloadXml: xml });
20399
21002
  }
21003
+ /**
21004
+ * Full port-config setter (cmd_id 36). Patches one or more of the six
21005
+ * service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
21006
+ * RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
21007
+ * `enable` (boolean); fields the caller doesn't pass are left alone.
21008
+ *
21009
+ * Sends one block per port that has any field set, then issues a
21010
+ * single cmd_36 with the merged body. The camera accepts multiple
21011
+ * `<XxxPort>` siblings in the same payload.
21012
+ *
21013
+ * Wire format observed on E1 Zoom:
21014
+ *
21015
+ * <body>
21016
+ * <RtspPort version="1.1">
21017
+ * <rtspPort>554</rtspPort>
21018
+ * <enable>1</enable>
21019
+ * </RtspPort>
21020
+ * <HttpsPort version="1.1">
21021
+ * <enable>0</enable>
21022
+ * </HttpsPort>
21023
+ * ...
21024
+ * </body>
21025
+ */
21026
+ async setPortConfig(patch) {
21027
+ const blocks = [];
21028
+ const append = (tag, portField, cfg) => {
21029
+ if (!cfg) return;
21030
+ if (cfg.port === void 0 && cfg.enable === void 0) return;
21031
+ const inner = [];
21032
+ if (cfg.port !== void 0) {
21033
+ inner.push(`<${portField}>${cfg.port}</${portField}>`);
21034
+ }
21035
+ if (cfg.enable !== void 0) {
21036
+ inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
21037
+ }
21038
+ blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
21039
+ };
21040
+ append("ServerPort", "serverPort", patch.server);
21041
+ append("HttpPort", "httpPort", patch.http);
21042
+ append("HttpsPort", "httpsPort", patch.https);
21043
+ append("RtspPort", "rtspPort", patch.rtsp);
21044
+ append("RtmpPort", "rtmpPort", patch.rtmp);
21045
+ append("OnvifPort", "onvifPort", patch.onvif);
21046
+ if (blocks.length === 0) return;
21047
+ const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
21048
+ await this.sendXml({ cmdId: 36, payloadXml: xml });
21049
+ }
20400
21050
  /** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
20401
21051
  async getInfo(channel, options) {
20402
21052
  const req = { cmdId: channel == null ? 80 : 318 };
@@ -24081,6 +24731,27 @@ ${stderr}`)
24081
24731
  );
24082
24732
  }
24083
24733
  }
24734
+ async gotoPtzPreset(arg1, arg2) {
24735
+ const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
24736
+ const presetId = arg2 === void 0 ? arg1 : arg2;
24737
+ const channelId = ch;
24738
+ const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
24739
+ const extensionXml = buildChannelExtensionXml(channelId);
24740
+ const frame = await this.client.sendFrame({
24741
+ cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
24742
+ channel: ch,
24743
+ channelIdOverride: channelId,
24744
+ extensionXml,
24745
+ payloadXml,
24746
+ messageClass: BC_CLASS_MODERN_24,
24747
+ streamType: 0
24748
+ });
24749
+ if (frame.header.responseCode !== 200) {
24750
+ throw new Error(
24751
+ `PTZ goto preset rejected (response_code ${frame.header.responseCode})`
24752
+ );
24753
+ }
24754
+ }
24084
24755
  async deletePtzPreset(arg1, arg2) {
24085
24756
  const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
24086
24757
  const presetId = arg2 === void 0 ? arg1 : arg2;
@@ -24732,22 +25403,62 @@ ${stderr}`)
24732
25403
  const channel = typeof arg1 === "number" ? arg1 : arg3;
24733
25404
  const enabled = typeof arg1 === "number" ? arg2 : arg1;
24734
25405
  const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
24735
- const ch = this.normalizeChannel(channel);
25406
+ return await this.setMotionAlarmFull({
25407
+ ...channel !== void 0 ? { channel } : {},
25408
+ enabled,
25409
+ ...sensitivity !== void 0 ? { sensitivity } : {}
25410
+ });
25411
+ }
25412
+ /**
25413
+ * Set motion alarm with full control, including the detection-zone grid.
25414
+ *
25415
+ * Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
25416
+ *
25417
+ * <MD version="1.1">
25418
+ * <channelId>0</channelId>
25419
+ * <enable>1</enable>
25420
+ * <usepir>0</usepir>
25421
+ * <width>60</width> <height>33</height>
25422
+ * <scope>
25423
+ * <columns>96</columns> <rows>64</rows>
25424
+ * <valueTable>{base64 6144-bit bitmap}</valueTable>
25425
+ * </scope>
25426
+ * ... other camera-specific fields ...
25427
+ * </MD>
25428
+ *
25429
+ * We do a read-modify-write of the GET response so any camera-specific
25430
+ * extension fields are preserved untouched. Pass `valueTable` to update
25431
+ * the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
25432
+ *
25433
+ * @param channel - 0-based channel
25434
+ * @param enabled - toggle motion detection on/off (optional)
25435
+ * @param sensitivity - 0-50, higher = more sensitive (optional)
25436
+ * @param valueTable - base64-encoded grid bitmap; size must match
25437
+ * `<scope><columns>×<rows></scope>` from the GET (optional)
25438
+ */
25439
+ async setMotionAlarmFull(opts) {
25440
+ const ch = this.normalizeChannel(opts.channel);
24736
25441
  const currentXml = await this.sendXml({
24737
25442
  cmdId: BC_CMD_ID_GET_MOTION_ALARM,
24738
25443
  channel: ch
24739
25444
  });
24740
25445
  let modifiedXml = currentXml;
24741
- if (enabled !== void 0) {
25446
+ if (opts.enabled !== void 0) {
24742
25447
  modifiedXml = modifiedXml.replace(
24743
25448
  /<enable>[^<]*<\/enable>/,
24744
- `<enable>${enabled ? "1" : "0"}</enable>`
25449
+ `<enable>${opts.enabled ? "1" : "0"}</enable>`
24745
25450
  );
24746
25451
  }
24747
- if (sensitivity !== void 0) {
25452
+ if (opts.sensitivity !== void 0) {
24748
25453
  modifiedXml = modifiedXml.replace(
24749
25454
  /<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
24750
- `<sensitivityDefault>${sensitivity}</sensitivityDefault>`
25455
+ `<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
25456
+ );
25457
+ }
25458
+ if (opts.valueTable !== void 0) {
25459
+ modifiedXml = modifiedXml.replace(
25460
+ /<valueTable>[^<]*<\/valueTable>/,
25461
+ `<valueTable>${opts.valueTable}</valueTable>`
24751
25462
  );
24752
25463
  }
24753
25464
  await this.sendXml({
@@ -26091,12 +26802,24 @@ ${xml}`
26091
26802
  }
26092
26803
  /**
26093
26804
  * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
26094
- * unspecified fields. Mirrors reolink_aio's `SetEnc`.
26805
+ * unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
26806
+ * `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
26807
+ * fields observed in the official mobile app (see `pcap/resolution.pcapng`).
26808
+ *
26809
+ * Field meaning per stream:
26810
+ * - `audio` — 0/1 toggle
26811
+ * - `width`/`height` — resolution in pixels. Must be one of the
26812
+ * resolutions returned by {@link getStreamInfoList}.
26813
+ * - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
26814
+ * - `frameRate` — fps. Must match the table from `getStreamInfoList`.
26815
+ * - `videoEncType` — `"h264"` or `"h265"`
26816
+ * - `encoderType` — `"vbr"` or `"cbr"`
26817
+ * - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
26818
+ * - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
26095
26819
  *
26096
26820
  * @param channel - Channel number (0-based)
26097
- * @param patch - Fields to update on `mainStream` and/or `subStream`,
26098
- * plus a top-level `audio` toggle (0/1). Pass only what you want
26099
- * to change.
26821
+ * @param patch - Fields to update. Pass only the fields you want to change;
26822
+ * everything else is preserved from the device's current configuration.
26100
26823
  */
26101
26824
  async setEnc(channel, patch, options) {
26102
26825
  const ch = this.normalizeChannel(channel);
@@ -26113,6 +26836,7 @@ ${xml}`
26113
26836
  }
26114
26837
  xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
26115
26838
  xml = applyStreamPatch(xml, "subStream", patch.subStream);
26839
+ xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
26116
26840
  await this.sendXml({
26117
26841
  cmdId: BC_CMD_ID_SET_ENC,
26118
26842
  channel: ch,
@@ -26720,6 +27444,71 @@ ${xml}`
26720
27444
  `PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
26721
27445
  );
26722
27446
  }
27447
+ /**
27448
+ * Update the OSD timestamp + channel-name overlay via cmd_id=45
27449
+ * (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
27450
+ * `<OsdChannelName>` block returned by `getOsdDatetime` — we
27451
+ * read-modify-write so any extension fields the camera sent are
27452
+ * preserved.
27453
+ *
27454
+ * Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
27455
+ * not preset strings). Set `enable=0` to hide the overlay; the camera
27456
+ * keeps the stored position so re-enabling later restores it.
27457
+ */
27458
+ async setOsdDatetime(channel, patch, options) {
27459
+ const ch = this.normalizeChannel(channel);
27460
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
27461
+ let xml = await this.sendPcapDerivedSettingsGetXml({
27462
+ cmdId: BC_CMD_ID_GET_OSD_DATETIME,
27463
+ channel: ch,
27464
+ ...timeoutOpts
27465
+ });
27466
+ const patchBlock = (block, fields) => {
27467
+ const start = xml.indexOf(`<${block}`);
27468
+ if (start < 0) return;
27469
+ const end = xml.indexOf(`</${block}>`, start);
27470
+ if (end < 0) return;
27471
+ let body = xml.slice(start, end);
27472
+ for (const [tag, value] of Object.entries(fields)) {
27473
+ if (value === void 0) continue;
27474
+ const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
27475
+ const escaped = raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
27476
+ if (body.includes(`<${tag}>`)) {
27477
+ body = body.replace(
27478
+ new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
27479
+ `<${tag}>${escaped}</${tag}>`
27480
+ );
27481
+ } else {
27482
+ body += `<${tag}>${escaped}</${tag}>`;
27483
+ }
27484
+ }
27485
+ xml = xml.slice(0, start) + body + xml.slice(end);
27486
+ };
27487
+ if (patch.datetime) {
27488
+ patchBlock("OsdDatetime", {
27489
+ enable: patch.datetime.enable,
27490
+ topLeftX: patch.datetime.topLeftX,
27491
+ topLeftY: patch.datetime.topLeftY,
27492
+ language: patch.datetime.language
27493
+ });
27494
+ }
27495
+ if (patch.channelName) {
27496
+ patchBlock("OsdChannelName", {
27497
+ name: patch.channelName.name,
27498
+ enable: patch.channelName.enable,
27499
+ topLeftX: patch.channelName.topLeftX,
27500
+ topLeftY: patch.channelName.topLeftY,
27501
+ enWatermark: patch.channelName.enWatermark,
27502
+ enBgcolor: patch.channelName.enBgcolor
27503
+ });
27504
+ }
27505
+ await this.sendXml({
27506
+ cmdId: BC_CMD_ID_SET_OSD_DATETIME,
27507
+ channel: ch,
27508
+ payloadXml: ensureXmlHeader(xml),
27509
+ ...timeoutOpts
27510
+ });
27511
+ }
26723
27512
  async getOsdDatetime(channel, options) {
26724
27513
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
26725
27514
  cmdId: BC_CMD_ID_GET_OSD_DATETIME,
@@ -26912,6 +27701,41 @@ ${xml}`
26912
27701
  });
26913
27702
  return { streams };
26914
27703
  }
27704
+ /**
27705
+ * Return the set of values `setEnc` will accept on each stream of `channel`.
27706
+ * Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
27707
+ * per-stream resolutions with their allowed codecs/framerates/bitrates plus
27708
+ * the enumerated encoder modes/profiles Reolink exposes.
27709
+ *
27710
+ * Useful for populating selectors and validating user input before calling
27711
+ * `setEnc` — picking an unsupported combination causes the camera to reject
27712
+ * the SET_ENC command (responseCode != 200).
27713
+ */
27714
+ async getEncOptions(channel, options) {
27715
+ const list = await this.getStreamInfoList(channel, options);
27716
+ return buildEncOptions(list, channel);
27717
+ }
27718
+ /**
27719
+ * Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
27720
+ * friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
27721
+ * version, hardware revision, build day, AI model bundle version, etc.
27722
+ *
27723
+ * This is the same info the Reolink mobile app shows in "About this
27724
+ * device" — distinct from `getSystemGeneral` (cmd_104) which carries
27725
+ * time/locale.
27726
+ *
27727
+ * No channel parameter: this command is device-global on NVRs/Hubs and
27728
+ * camera-global on standalone cameras. Pass an explicit channel via the
27729
+ * underlying `sendXml` only if a specific firmware demands it (none we've
27730
+ * tested do).
27731
+ */
27732
+ async getVersionInfo(options) {
27733
+ const xml = await this.sendXml({
27734
+ cmdId: BC_CMD_ID_GET_VERSION_INFO,
27735
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
27736
+ });
27737
+ return parseVersionInfo(xml);
27738
+ }
26915
27739
  async getLedState(channel, options) {
26916
27740
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
26917
27741
  cmdId: BC_CMD_ID_GET_LED_STATE,