@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.
@@ -1,3 +1,36 @@
1
+ import {
2
+ ReolinkCgiApi,
3
+ ReolinkHttpClient,
4
+ applyStreamPatch,
5
+ applyXmlTagPatch,
6
+ buildAbilityInfoExtensionXml,
7
+ buildBinaryExtensionXml,
8
+ buildChannelExtensionXml,
9
+ buildFloodlightManualXml,
10
+ buildLoginXml,
11
+ buildLogoutXml,
12
+ buildPreviewStopXml,
13
+ buildPreviewStopXmlV11,
14
+ buildPreviewXml,
15
+ buildPreviewXmlV11,
16
+ buildPtzControlXml,
17
+ buildPtzPresetXml,
18
+ buildPtzPresetXmlV2,
19
+ buildRtspUrl,
20
+ buildSirenManualXml,
21
+ buildSirenTimesXml,
22
+ buildStartZoomFocusXml,
23
+ collectNvrDiagnostics,
24
+ ensureXmlHeader,
25
+ getXmlText,
26
+ normalizeDayNightMode,
27
+ normalizeOpenClose,
28
+ parseRecordingFileName,
29
+ patchNestedTag,
30
+ runAllDiagnosticsConsecutively,
31
+ runMultifocalDiagnosticsConsecutively,
32
+ xmlEscape
33
+ } from "./chunk-IJG45AOT.js";
1
34
  import {
2
35
  BC_CLASS_FILE_DOWNLOAD,
3
36
  BC_CLASS_LEGACY,
@@ -61,6 +94,7 @@ import {
61
94
  BC_CMD_ID_GET_SUPPORT,
62
95
  BC_CMD_ID_GET_SYSTEM_GENERAL,
63
96
  BC_CMD_ID_GET_TIMELAPSE_CFG,
97
+ BC_CMD_ID_GET_VERSION_INFO,
64
98
  BC_CMD_ID_GET_VIDEO_INPUT,
65
99
  BC_CMD_ID_GET_WHITE_LED,
66
100
  BC_CMD_ID_GET_WIFI,
@@ -89,6 +123,7 @@ import {
89
123
  BC_CMD_ID_SET_ENC,
90
124
  BC_CMD_ID_SET_LED_STATE,
91
125
  BC_CMD_ID_SET_MOTION_ALARM,
126
+ BC_CMD_ID_SET_OSD_DATETIME,
92
127
  BC_CMD_ID_SET_PIR_INFO,
93
128
  BC_CMD_ID_SET_PRIVACY_MASK,
94
129
  BC_CMD_ID_SET_VIDEO_INPUT,
@@ -108,60 +143,29 @@ import {
108
143
  BC_TCP_DEFAULT_PORT,
109
144
  BaichuanVideoStream,
110
145
  BcMediaAnnexBDecoder,
111
- ReolinkCgiApi,
112
- ReolinkHttpClient,
113
146
  __require,
114
147
  aesDecrypt,
115
148
  aesEncrypt,
116
- applyStreamPatch,
117
- applyXmlTagPatch,
118
149
  bcDecrypt,
119
150
  bcEncrypt,
120
151
  bcHeaderHasPayloadOffset,
121
- buildAbilityInfoExtensionXml,
122
- buildBinaryExtensionXml,
123
- buildChannelExtensionXml,
124
- buildFloodlightManualXml,
125
- buildLoginXml,
126
- buildLogoutXml,
127
- buildPreviewStopXml,
128
- buildPreviewStopXmlV11,
129
- buildPreviewXml,
130
- buildPreviewXmlV11,
131
- buildPtzControlXml,
132
- buildPtzPresetXml,
133
- buildPtzPresetXmlV2,
134
- buildRtspUrl,
135
- buildSirenManualXml,
136
- buildSirenTimesXml,
137
- buildStartZoomFocusXml,
138
- collectNvrDiagnostics,
139
152
  convertToAnnexB,
140
153
  convertToAnnexB2,
141
154
  debugLog,
142
155
  deriveAesKey,
143
- ensureXmlHeader,
144
156
  eventTraceLog,
145
157
  extractPpsFromAnnexB,
146
158
  extractSpsFromAnnexB,
147
159
  extractVpsFromAnnexB,
148
- getXmlText,
149
160
  isH265Irap,
150
161
  md5StrModern,
151
- normalizeDayNightMode,
152
162
  normalizeDebugOptions,
153
- normalizeOpenClose,
154
- parseRecordingFileName,
155
- patchNestedTag,
156
163
  recordingsTraceLog,
157
- runAllDiagnosticsConsecutively,
158
- runMultifocalDiagnosticsConsecutively,
159
164
  splitAnnexBToNalPayloads,
160
165
  splitAnnexBToNalPayloads2,
161
166
  talkTraceLog,
162
- traceLog,
163
- xmlEscape
164
- } from "./chunk-EDLMKBG2.js";
167
+ traceLog
168
+ } from "./chunk-W2ANCJVM.js";
165
169
 
166
170
  // src/protocol/framing.ts
167
171
  function encodeHeader(h) {
@@ -1870,6 +1874,23 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1870
1874
  * even if the current client instance is idle/disconnected.
1871
1875
  */
1872
1876
  static streamingRegistry = /* @__PURE__ */ new Map();
1877
+ /**
1878
+ * Per-device set of live BaichuanClient instances.
1879
+ *
1880
+ * Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
1881
+ * and SocketPool tears the streaming socket down), the global streaming
1882
+ * registry decrements but the GENERAL client of the same device has no
1883
+ * way of knowing — its idle-disconnect timer was last evaluated while
1884
+ * `isDeviceStreamingActive()` was still true (because the streaming socket
1885
+ * was still alive) and wasn't rescheduled. Without this registry the
1886
+ * general socket stays connected, the 60-second session-guard timer keeps
1887
+ * sending getOnlineUserList() to the camera, and a battery camera ends up
1888
+ * waking up every minute (issue #18).
1889
+ *
1890
+ * On streamingRegistry decrement-to-zero we walk this set and kick every
1891
+ * sibling's idle-disconnect timer so it can re-evaluate eligibility.
1892
+ */
1893
+ static deviceClients = /* @__PURE__ */ new Map();
1873
1894
  /**
1874
1895
  * Per-host D2C_DISC backoff state that persists across client instance recreation.
1875
1896
  *
@@ -1984,6 +2005,29 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1984
2005
  // AlarmEventList (cmdId=33) can be very chatty (often sent every second).
1985
2006
  // Track last per-channel alarm state so we only emit on transitions.
1986
2007
  alarmEventState = /* @__PURE__ */ new Map();
2008
+ /** Whether this instance is currently in BaichuanClient.deviceClients. */
2009
+ registeredInDeviceClients = false;
2010
+ registerInDeviceClients() {
2011
+ if (this.registeredInDeviceClients) return;
2012
+ const key = this.getDeviceRegistryKey();
2013
+ let set = _BaichuanClient.deviceClients.get(key);
2014
+ if (!set) {
2015
+ set = /* @__PURE__ */ new Set();
2016
+ _BaichuanClient.deviceClients.set(key, set);
2017
+ }
2018
+ set.add(this);
2019
+ this.registeredInDeviceClients = true;
2020
+ }
2021
+ unregisterFromDeviceClients() {
2022
+ if (!this.registeredInDeviceClients) return;
2023
+ const key = this.getDeviceRegistryKey();
2024
+ const set = _BaichuanClient.deviceClients.get(key);
2025
+ if (set) {
2026
+ set.delete(this);
2027
+ if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
2028
+ }
2029
+ this.registeredInDeviceClients = false;
2030
+ }
1987
2031
  constructor(options) {
1988
2032
  super();
1989
2033
  this.opts = options;
@@ -1998,6 +2042,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1998
2042
  code: err?.code
1999
2043
  });
2000
2044
  });
2045
+ this.registerInDeviceClients();
2001
2046
  }
2002
2047
  newSocketSessionId(transport) {
2003
2048
  const short = randomUUID().split("-")[0] ?? randomUUID().slice(0, 8);
@@ -2254,6 +2299,18 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2254
2299
  activeStreamClients: nextCount
2255
2300
  });
2256
2301
  this.contributesToGlobalStreamingRegistry = shouldContribute;
2302
+ if (!shouldContribute && nextCount === 0) {
2303
+ const siblings = _BaichuanClient.deviceClients.get(key);
2304
+ if (siblings) {
2305
+ for (const sib of siblings) {
2306
+ if (sib === this) continue;
2307
+ try {
2308
+ sib.kickIdleDisconnectTimer();
2309
+ } catch {
2310
+ }
2311
+ }
2312
+ }
2313
+ }
2257
2314
  }
2258
2315
  /**
2259
2316
  * True if the device should be considered "awake" due to active streaming.
@@ -2718,6 +2775,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2718
2775
  `transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
2719
2776
  );
2720
2777
  this.logSocketState("tcp_connected");
2778
+ this.registerInDeviceClients();
2721
2779
  this.startKeepAlive();
2722
2780
  this.kickIdleDisconnectTimer();
2723
2781
  }
@@ -3034,6 +3092,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
3034
3092
  this.logDebug("udp_close_error", e);
3035
3093
  }
3036
3094
  }
3095
+ this.unregisterFromDeviceClients();
3037
3096
  }
3038
3097
  handleFrame(frame) {
3039
3098
  const now = Date.now();
@@ -8335,6 +8394,31 @@ var parseAbilityInfoXml = (xml) => {
8335
8394
  return abilities;
8336
8395
  };
8337
8396
 
8397
+ // src/reolink/baichuan/utils/versionInfo.ts
8398
+ function parseVersionInfo(xml) {
8399
+ const out = {};
8400
+ const set = (key) => {
8401
+ const v = getXmlText(xml, key);
8402
+ if (v !== void 0) out[key] = v;
8403
+ };
8404
+ set("name");
8405
+ set("type");
8406
+ set("serialNumber");
8407
+ set("buildDay");
8408
+ set("hardwareVersion");
8409
+ set("cfgVersion");
8410
+ set("firmwareVersion");
8411
+ set("detail");
8412
+ set("IEClient");
8413
+ set("cc3200Version");
8414
+ set("spVersion");
8415
+ set("pakSuffix");
8416
+ set("itemNo");
8417
+ set("aiVersion");
8418
+ set("helpVersion");
8419
+ return out;
8420
+ }
8421
+
8338
8422
  // src/reolink/baichuan/utils/logging.ts
8339
8423
  var formatErrorForLog = (e) => {
8340
8424
  if (e instanceof Error) {
@@ -8589,6 +8673,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
8589
8673
  return { result: resultObj, storedChannels: Object.keys(resultObj) };
8590
8674
  };
8591
8675
 
8676
+ // src/reolink/baichuan/utils/detection.ts
8677
+ import * as lz4 from "lz4js";
8678
+
8679
+ // src/reolink/baichuan/utils/aiClassMap.ts
8680
+ function type1ToLabel(type1) {
8681
+ switch (type1) {
8682
+ case 1:
8683
+ return "people";
8684
+ case 2:
8685
+ return "vehicle";
8686
+ case 3:
8687
+ return "animal";
8688
+ case 11259375:
8689
+ return "face";
8690
+ default:
8691
+ return "unknown";
8692
+ }
8693
+ }
8694
+
8695
+ // src/reolink/baichuan/utils/detection.ts
8696
+ var MARKER_LENGTH = 8;
8697
+ var IFRAME_PREFIX_LENGTH = 8;
8698
+ var COUNTER_OFFSET = 8;
8699
+ var BASELINE_SIZE = 128;
8700
+ var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
8701
+ var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
8702
+ var CONFIDENCE_DIVISOR = 100;
8703
+ var DEFAULT_AI_FRAME_WIDTH = 896;
8704
+ var DEFAULT_AI_FRAME_HEIGHT = 480;
8705
+ var LZ4_DECOMPRESS_MAX = 256 * 1024;
8706
+ function walkBoxes(buf, start, end, type1, type2, out) {
8707
+ let pos = start;
8708
+ while (pos + 3 <= end) {
8709
+ const t = buf[pos];
8710
+ if (t === 0) return;
8711
+ const length = buf[pos + 1] | buf[pos + 2] << 8;
8712
+ const recordEnd = pos + 3 + length;
8713
+ if (recordEnd > end) return;
8714
+ const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
8715
+ const isBoxType2 = t === 2 && length === 10;
8716
+ if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
8717
+ const x1 = buf.readUInt16LE(pos + 3);
8718
+ const y1 = buf.readUInt16LE(pos + 5);
8719
+ const x2 = buf.readUInt16LE(pos + 7);
8720
+ const y2 = buf.readUInt16LE(pos + 9);
8721
+ const conf = buf.readUInt16LE(pos + 11);
8722
+ if (x2 > x1 && y2 > y1) {
8723
+ out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
8724
+ }
8725
+ pos = recordEnd;
8726
+ continue;
8727
+ }
8728
+ 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]) {
8729
+ try {
8730
+ const decompressed = lz4.decompress(
8731
+ buf.subarray(pos + 3, recordEnd),
8732
+ LZ4_DECOMPRESS_MAX
8733
+ );
8734
+ const decBuf = Buffer.from(decompressed);
8735
+ walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
8736
+ } catch {
8737
+ }
8738
+ pos = recordEnd;
8739
+ continue;
8740
+ }
8741
+ if (length > 0) {
8742
+ let nextT1 = type1;
8743
+ let nextT2 = type2;
8744
+ if (type1 === 0) nextT1 = t;
8745
+ else if (type2 === 0) nextT2 = t;
8746
+ walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
8747
+ }
8748
+ pos = recordEnd;
8749
+ }
8750
+ }
8751
+ function decodeDetectionHeader(raw, frameType) {
8752
+ const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
8753
+ const blockLength = raw.length - markerOffset;
8754
+ const empty = {
8755
+ state: "invalid-marker",
8756
+ markerOffset,
8757
+ blockLength,
8758
+ boxes: []
8759
+ };
8760
+ if (blockLength < MARKER_LENGTH) return empty;
8761
+ if (!hasStandardMarker(raw, markerOffset)) return empty;
8762
+ if (blockLength < COUNTER_OFFSET + 4) return empty;
8763
+ const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
8764
+ const rawBoxes = [];
8765
+ walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
8766
+ let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
8767
+ let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
8768
+ let frameSizeFound = false;
8769
+ const searchStart = markerOffset + MARKER_LENGTH;
8770
+ for (let i = searchStart; i + 7 <= raw.length; i++) {
8771
+ if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
8772
+ const w = raw.readUInt16LE(i + 3);
8773
+ const h = raw.readUInt16LE(i + 5);
8774
+ if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
8775
+ aiFrameWidth = w;
8776
+ aiFrameHeight = h;
8777
+ frameSizeFound = true;
8778
+ break;
8779
+ }
8780
+ }
8781
+ }
8782
+ const specificity = {
8783
+ face: 4,
8784
+ animal: 3,
8785
+ people: 2,
8786
+ vehicle: 1,
8787
+ unknown: 0
8788
+ };
8789
+ const dedup = /* @__PURE__ */ new Map();
8790
+ for (const rb of rawBoxes) {
8791
+ if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
8792
+ const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
8793
+ const prev = dedup.get(key);
8794
+ if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
8795
+ dedup.set(key, rb);
8796
+ }
8797
+ }
8798
+ const boxes = [];
8799
+ for (const rb of dedup.values()) {
8800
+ boxes.push({
8801
+ x: rb.x1 / aiFrameWidth,
8802
+ y: rb.y1 / aiFrameHeight,
8803
+ width: (rb.x2 - rb.x1) / aiFrameWidth,
8804
+ height: (rb.y2 - rb.y1) / aiFrameHeight,
8805
+ ...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
8806
+ ...rb.label !== "unknown" ? { label: rb.label } : {}
8807
+ });
8808
+ }
8809
+ let state;
8810
+ if (boxes.length > 0) state = "overlay-decoded";
8811
+ else if (blockLength === BASELINE_SIZE) state = "no-overlay";
8812
+ else state = "overlay-undecoded";
8813
+ return {
8814
+ state,
8815
+ markerOffset,
8816
+ blockLength,
8817
+ counter,
8818
+ ...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
8819
+ boxes
8820
+ };
8821
+ }
8822
+ function hasStandardMarker(raw, offset) {
8823
+ if (raw.length < offset + MARKER_LENGTH) return false;
8824
+ 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;
8825
+ }
8826
+
8827
+ // src/reolink/baichuan/utils/encOptions.ts
8828
+ function buildEncOptions(list, channel) {
8829
+ const result = { channel };
8830
+ const main = aggregateByType(list, "mainStream");
8831
+ const sub = aggregateByType(list, "subStream");
8832
+ const third = aggregateByType(list, "thirdStream");
8833
+ if (main) result.mainStream = main;
8834
+ if (sub) result.subStream = sub;
8835
+ if (third) result.thirdStream = third;
8836
+ return result;
8837
+ }
8838
+ function aggregateByType(list, type) {
8839
+ const seen = /* @__PURE__ */ new Map();
8840
+ for (const stream of list.streams) {
8841
+ for (const eb of stream.encodeTables) {
8842
+ if (eb.type !== type) continue;
8843
+ const mapped = mapEncodeTable(eb);
8844
+ if (!mapped) continue;
8845
+ const key = `${mapped.width}x${mapped.height}`;
8846
+ if (!seen.has(key)) seen.set(key, mapped);
8847
+ }
8848
+ }
8849
+ if (seen.size === 0) return void 0;
8850
+ return {
8851
+ type,
8852
+ resolutions: [...seen.values()],
8853
+ encoderTypes: ["vbr", "cbr"],
8854
+ encoderProfiles: ["high", "main", "baseline"]
8855
+ };
8856
+ }
8857
+ function mapEncodeTable(eb) {
8858
+ if (eb.width == null || eb.height == null) return void 0;
8859
+ const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
8860
+ (t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
8861
+ ).filter((t) => t !== void 0);
8862
+ return {
8863
+ width: eb.width,
8864
+ height: eb.height,
8865
+ videoEncTypes,
8866
+ ...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
8867
+ ...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
8868
+ ...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
8869
+ framerateOptions: eb.framerateTable ?? [],
8870
+ bitrateOptions: eb.bitrateTable ?? []
8871
+ };
8872
+ }
8873
+
8592
8874
  // src/reolink/baichuan/utils/events.ts
8593
8875
  var mapToSimpleEvent = (event) => {
8594
8876
  const timestamp = event.timestamp ?? Date.now();
@@ -10688,6 +10970,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10688
10970
  sessionGuardIntervalTimer;
10689
10971
  simpleEventListeners = /* @__PURE__ */ new Set();
10690
10972
  simpleEventSubscribed = false;
10973
+ // Detection events are sourced from BcMedia additionalHeader on active video
10974
+ // streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
10975
+ // data flows whenever a stream is open. Active streams register themselves via
10976
+ // _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
10977
+ detectionEventListeners = /* @__PURE__ */ new Set();
10978
+ detectionEventStreamHooks = /* @__PURE__ */ new Map();
10979
+ // Auto-managed substream for `onObjectDetections` listeners. Reference-counted
10980
+ // by the listener set: the substream is opened on the first listener and torn
10981
+ // down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
10982
+ // lifecycle so a caller never has to manage a video stream just to read AI
10983
+ // detections.
10984
+ objectDetectionListeners = /* @__PURE__ */ new Set();
10985
+ objectDetectionStream;
10986
+ objectDetectionStreamStartInFlight;
10987
+ objectDetectionInternalListener;
10691
10988
  simpleEventSubscribeInFlight;
10692
10989
  simpleEventUnsubscribeInFlight;
10693
10990
  simpleEventResubscribeTimer;
@@ -12123,6 +12420,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12123
12420
  }
12124
12421
  }
12125
12422
  }
12423
+ /**
12424
+ * Subscribe to per-frame detection events sourced from the BcMedia
12425
+ * `additionalHeader` block on active video streams.
12426
+ *
12427
+ * Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
12428
+ * one event fires for every I-frame / P-frame that carries an overlay block.
12429
+ * Coordinates are reported in normalized [0, 1] fractions of the source
12430
+ * frame, so the same box renders correctly on mainStream, subStream, and
12431
+ * externStream.
12432
+ *
12433
+ * Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
12434
+ * only flow while a video stream is open. The library hooks every
12435
+ * `BaichuanVideoStream` created via this API for the listener's lifetime.
12436
+ */
12437
+ onDetection(callback) {
12438
+ this.detectionEventListeners.add(callback);
12439
+ }
12440
+ /**
12441
+ * Remove a single detection callback, or all of them if `callback` is omitted.
12442
+ */
12443
+ offDetection(callback) {
12444
+ if (callback) {
12445
+ this.detectionEventListeners.delete(callback);
12446
+ } else {
12447
+ this.detectionEventListeners.clear();
12448
+ }
12449
+ }
12450
+ /**
12451
+ * Subscribe to AI object detections (people / vehicle / animal / face boxes
12452
+ * with class label and confidence) without managing a video stream yourself.
12453
+ *
12454
+ * Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
12455
+ * substream behind the scenes on the first listener, forwards every box-bearing
12456
+ * `additionalHeader` to your callback, and tears the stream down when the last
12457
+ * listener unsubscribes. The substream is the lightest profile (typically
12458
+ * 640×360) so the additional bandwidth/CPU overhead is minimal.
12459
+ *
12460
+ * Each event carries normalized `[0, 1]` box coordinates, a class label, and
12461
+ * a confidence score — render-ready without further conversion.
12462
+ */
12463
+ async onObjectDetections(callback) {
12464
+ this.objectDetectionListeners.add(callback);
12465
+ this.logger.debug?.(
12466
+ `[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
12467
+ );
12468
+ await this.ensureObjectDetectionStream();
12469
+ }
12470
+ /**
12471
+ * Remove one detection callback, or all of them if `callback` is omitted.
12472
+ * When the last listener is removed the auto-managed substream is closed.
12473
+ */
12474
+ async offObjectDetections(callback) {
12475
+ if (callback) {
12476
+ this.objectDetectionListeners.delete(callback);
12477
+ } else {
12478
+ this.objectDetectionListeners.clear();
12479
+ }
12480
+ if (this.objectDetectionListeners.size === 0) {
12481
+ await this.tearDownObjectDetectionStream();
12482
+ }
12483
+ }
12484
+ async ensureObjectDetectionStream() {
12485
+ if (this.objectDetectionStream) return;
12486
+ if (this.objectDetectionStreamStartInFlight) {
12487
+ await this.objectDetectionStreamStartInFlight;
12488
+ return;
12489
+ }
12490
+ this.objectDetectionStreamStartInFlight = (async () => {
12491
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-PHQG4A2L.js");
12492
+ const sessionKey = `live:object-detections:ch0:sub`;
12493
+ const dedicated = await this.createDedicatedSession(sessionKey);
12494
+ const stream = new BaichuanVideoStream2({
12495
+ client: dedicated.client,
12496
+ api: this,
12497
+ channel: 0,
12498
+ profile: "sub",
12499
+ logger: this.logger
12500
+ });
12501
+ this.objectDetectionInternalListener = (event) => {
12502
+ for (const cb of this.objectDetectionListeners) {
12503
+ try {
12504
+ void Promise.resolve(cb(event)).catch((e) => {
12505
+ (this.logger.warn ?? this.logger.error).call(
12506
+ this.logger,
12507
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
12508
+ formatErrorForLog(e)
12509
+ );
12510
+ });
12511
+ } catch (e) {
12512
+ (this.logger.warn ?? this.logger.error).call(
12513
+ this.logger,
12514
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
12515
+ formatErrorForLog(e)
12516
+ );
12517
+ }
12518
+ }
12519
+ };
12520
+ this.detectionEventListeners.add(this.objectDetectionInternalListener);
12521
+ try {
12522
+ await stream.start();
12523
+ } catch (e) {
12524
+ if (this.objectDetectionInternalListener) {
12525
+ this.detectionEventListeners.delete(
12526
+ this.objectDetectionInternalListener
12527
+ );
12528
+ this.objectDetectionInternalListener = void 0;
12529
+ }
12530
+ await dedicated.release().catch(() => {
12531
+ });
12532
+ throw e;
12533
+ }
12534
+ this.objectDetectionStream = {
12535
+ stop: () => stream.stop(),
12536
+ release: () => dedicated.release()
12537
+ };
12538
+ this.logger.debug?.(
12539
+ `[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
12540
+ );
12541
+ })();
12542
+ try {
12543
+ await this.objectDetectionStreamStartInFlight;
12544
+ } finally {
12545
+ this.objectDetectionStreamStartInFlight = void 0;
12546
+ }
12547
+ }
12548
+ async tearDownObjectDetectionStream() {
12549
+ const handle = this.objectDetectionStream;
12550
+ this.objectDetectionStream = void 0;
12551
+ if (this.objectDetectionInternalListener) {
12552
+ this.detectionEventListeners.delete(this.objectDetectionInternalListener);
12553
+ this.objectDetectionInternalListener = void 0;
12554
+ }
12555
+ if (!handle) return;
12556
+ try {
12557
+ await handle.stop();
12558
+ } catch (e) {
12559
+ this.logger.debug?.(
12560
+ `[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
12561
+ );
12562
+ }
12563
+ try {
12564
+ await handle.release();
12565
+ } catch (e) {
12566
+ this.logger.debug?.(
12567
+ `[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
12568
+ );
12569
+ }
12570
+ this.logger.debug?.(
12571
+ `[ReolinkBaichuanApi] onObjectDetections: substream torn down`
12572
+ );
12573
+ }
12574
+ /**
12575
+ * Internal: invoked by BaichuanVideoStream when it starts so the API can hook
12576
+ * its `additionalHeader` event. Returns a teardown function the stream calls
12577
+ * on stop. Not intended for direct use by consumers.
12578
+ */
12579
+ _registerVideoStreamForDetection(stream, context) {
12580
+ const listener = (info) => {
12581
+ if (this.detectionEventListeners.size === 0) return;
12582
+ const decoded = decodeDetectionHeader(info.raw, info.frameType);
12583
+ const event = {
12584
+ channel: context.channel,
12585
+ microseconds: info.microseconds,
12586
+ profile: context.profile,
12587
+ boxes: decoded.boxes,
12588
+ ...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
12589
+ ...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
12590
+ decodeState: decoded.state,
12591
+ rawHeader: info.raw
12592
+ };
12593
+ this.dispatchDetectionEvent(event);
12594
+ };
12595
+ stream.on("additionalHeader", listener);
12596
+ const teardown = () => {
12597
+ stream.off("additionalHeader", listener);
12598
+ this.detectionEventStreamHooks.delete(stream);
12599
+ };
12600
+ this.detectionEventStreamHooks.set(stream, teardown);
12601
+ return teardown;
12602
+ }
12603
+ dispatchDetectionEvent(evt) {
12604
+ for (const cb of this.detectionEventListeners) {
12605
+ try {
12606
+ void Promise.resolve(cb(evt)).catch((e) => {
12607
+ (this.logger.warn ?? this.logger.error).call(
12608
+ this.logger,
12609
+ "[ReolinkBaichuanApi] onDetection handler error",
12610
+ formatErrorForLog(e)
12611
+ );
12612
+ });
12613
+ } catch (e) {
12614
+ (this.logger.warn ?? this.logger.error).call(
12615
+ this.logger,
12616
+ "[ReolinkBaichuanApi] onDetection handler error",
12617
+ formatErrorForLog(e)
12618
+ );
12619
+ }
12620
+ }
12621
+ }
12126
12622
  startSimpleEventResubscribeTimer() {
12127
12623
  if (this.simpleEventResubscribeTimer) return;
12128
12624
  if (this.simpleEventListeners.size === 0) return;
@@ -12505,6 +13001,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12505
13001
  this.stopUdpSleepInference();
12506
13002
  this.stopSimpleEventWatchdog();
12507
13003
  this.stopSimpleEventResubscribeTimer();
13004
+ this.objectDetectionListeners.clear();
13005
+ await this.tearDownObjectDetectionStream().catch(() => {
13006
+ });
12508
13007
  await this.cleanup();
12509
13008
  await this.stopAllActiveStreams();
12510
13009
  await this.cleanupSocketPool();
@@ -12852,6 +13351,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12852
13351
  const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
12853
13352
  await this.sendXml({ cmdId: 36, payloadXml: xml });
12854
13353
  }
13354
+ /**
13355
+ * Full port-config setter (cmd_id 36). Patches one or more of the six
13356
+ * service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
13357
+ * RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
13358
+ * `enable` (boolean); fields the caller doesn't pass are left alone.
13359
+ *
13360
+ * Sends one block per port that has any field set, then issues a
13361
+ * single cmd_36 with the merged body. The camera accepts multiple
13362
+ * `<XxxPort>` siblings in the same payload.
13363
+ *
13364
+ * Wire format observed on E1 Zoom:
13365
+ *
13366
+ * <body>
13367
+ * <RtspPort version="1.1">
13368
+ * <rtspPort>554</rtspPort>
13369
+ * <enable>1</enable>
13370
+ * </RtspPort>
13371
+ * <HttpsPort version="1.1">
13372
+ * <enable>0</enable>
13373
+ * </HttpsPort>
13374
+ * ...
13375
+ * </body>
13376
+ */
13377
+ async setPortConfig(patch) {
13378
+ const blocks = [];
13379
+ const append = (tag, portField, cfg) => {
13380
+ if (!cfg) return;
13381
+ if (cfg.port === void 0 && cfg.enable === void 0) return;
13382
+ const inner = [];
13383
+ if (cfg.port !== void 0) {
13384
+ inner.push(`<${portField}>${cfg.port}</${portField}>`);
13385
+ }
13386
+ if (cfg.enable !== void 0) {
13387
+ inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
13388
+ }
13389
+ blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
13390
+ };
13391
+ append("ServerPort", "serverPort", patch.server);
13392
+ append("HttpPort", "httpPort", patch.http);
13393
+ append("HttpsPort", "httpsPort", patch.https);
13394
+ append("RtspPort", "rtspPort", patch.rtsp);
13395
+ append("RtmpPort", "rtmpPort", patch.rtmp);
13396
+ append("OnvifPort", "onvifPort", patch.onvif);
13397
+ if (blocks.length === 0) return;
13398
+ const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
13399
+ await this.sendXml({ cmdId: 36, payloadXml: xml });
13400
+ }
12855
13401
  /** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
12856
13402
  async getInfo(channel, options) {
12857
13403
  const req = { cmdId: channel == null ? 80 : 318 };
@@ -16536,6 +17082,27 @@ ${stderr}`)
16536
17082
  );
16537
17083
  }
16538
17084
  }
17085
+ async gotoPtzPreset(arg1, arg2) {
17086
+ const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
17087
+ const presetId = arg2 === void 0 ? arg1 : arg2;
17088
+ const channelId = ch;
17089
+ const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
17090
+ const extensionXml = buildChannelExtensionXml(channelId);
17091
+ const frame = await this.client.sendFrame({
17092
+ cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
17093
+ channel: ch,
17094
+ channelIdOverride: channelId,
17095
+ extensionXml,
17096
+ payloadXml,
17097
+ messageClass: BC_CLASS_MODERN_24,
17098
+ streamType: 0
17099
+ });
17100
+ if (frame.header.responseCode !== 200) {
17101
+ throw new Error(
17102
+ `PTZ goto preset rejected (response_code ${frame.header.responseCode})`
17103
+ );
17104
+ }
17105
+ }
16539
17106
  async deletePtzPreset(arg1, arg2) {
16540
17107
  const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
16541
17108
  const presetId = arg2 === void 0 ? arg1 : arg2;
@@ -17187,22 +17754,62 @@ ${stderr}`)
17187
17754
  const channel = typeof arg1 === "number" ? arg1 : arg3;
17188
17755
  const enabled = typeof arg1 === "number" ? arg2 : arg1;
17189
17756
  const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
17190
- const ch = this.normalizeChannel(channel);
17757
+ return await this.setMotionAlarmFull({
17758
+ ...channel !== void 0 ? { channel } : {},
17759
+ enabled,
17760
+ ...sensitivity !== void 0 ? { sensitivity } : {}
17761
+ });
17762
+ }
17763
+ /**
17764
+ * Set motion alarm with full control, including the detection-zone grid.
17765
+ *
17766
+ * Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
17767
+ *
17768
+ * <MD version="1.1">
17769
+ * <channelId>0</channelId>
17770
+ * <enable>1</enable>
17771
+ * <usepir>0</usepir>
17772
+ * <width>60</width> <height>33</height>
17773
+ * <scope>
17774
+ * <columns>96</columns> <rows>64</rows>
17775
+ * <valueTable>{base64 6144-bit bitmap}</valueTable>
17776
+ * </scope>
17777
+ * ... other camera-specific fields ...
17778
+ * </MD>
17779
+ *
17780
+ * We do a read-modify-write of the GET response so any camera-specific
17781
+ * extension fields are preserved untouched. Pass `valueTable` to update
17782
+ * the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
17783
+ *
17784
+ * @param channel - 0-based channel
17785
+ * @param enabled - toggle motion detection on/off (optional)
17786
+ * @param sensitivity - 0-50, higher = more sensitive (optional)
17787
+ * @param valueTable - base64-encoded grid bitmap; size must match
17788
+ * `<scope><columns>×<rows></scope>` from the GET (optional)
17789
+ */
17790
+ async setMotionAlarmFull(opts) {
17791
+ const ch = this.normalizeChannel(opts.channel);
17191
17792
  const currentXml = await this.sendXml({
17192
17793
  cmdId: BC_CMD_ID_GET_MOTION_ALARM,
17193
17794
  channel: ch
17194
17795
  });
17195
17796
  let modifiedXml = currentXml;
17196
- if (enabled !== void 0) {
17797
+ if (opts.enabled !== void 0) {
17197
17798
  modifiedXml = modifiedXml.replace(
17198
17799
  /<enable>[^<]*<\/enable>/,
17199
- `<enable>${enabled ? "1" : "0"}</enable>`
17800
+ `<enable>${opts.enabled ? "1" : "0"}</enable>`
17200
17801
  );
17201
17802
  }
17202
- if (sensitivity !== void 0) {
17803
+ if (opts.sensitivity !== void 0) {
17203
17804
  modifiedXml = modifiedXml.replace(
17204
17805
  /<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
17205
- `<sensitivityDefault>${sensitivity}</sensitivityDefault>`
17806
+ `<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
17807
+ );
17808
+ }
17809
+ if (opts.valueTable !== void 0) {
17810
+ modifiedXml = modifiedXml.replace(
17811
+ /<valueTable>[^<]*<\/valueTable>/,
17812
+ `<valueTable>${opts.valueTable}</valueTable>`
17206
17813
  );
17207
17814
  }
17208
17815
  await this.sendXml({
@@ -18477,7 +19084,7 @@ ${xml}`
18477
19084
  * @returns Test results for all stream types and profiles
18478
19085
  */
18479
19086
  async testChannelStreams(channel, logger) {
18480
- const { testChannelStreams } = await import("./DiagnosticsTools-RNIDFEJK.js");
19087
+ const { testChannelStreams } = await import("./DiagnosticsTools-HGJGVQXZ.js");
18481
19088
  return await testChannelStreams({
18482
19089
  api: this,
18483
19090
  channel: this.normalizeChannel(channel),
@@ -18493,7 +19100,7 @@ ${xml}`
18493
19100
  * @returns Complete diagnostics for all channels and streams
18494
19101
  */
18495
19102
  async collectMultifocalDiagnostics(logger) {
18496
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-RNIDFEJK.js");
19103
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-HGJGVQXZ.js");
18497
19104
  return await collectMultifocalDiagnostics({
18498
19105
  api: this,
18499
19106
  logger
@@ -18546,12 +19153,24 @@ ${xml}`
18546
19153
  }
18547
19154
  /**
18548
19155
  * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
18549
- * unspecified fields. Mirrors reolink_aio's `SetEnc`.
19156
+ * unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
19157
+ * `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
19158
+ * fields observed in the official mobile app (see `pcap/resolution.pcapng`).
19159
+ *
19160
+ * Field meaning per stream:
19161
+ * - `audio` — 0/1 toggle
19162
+ * - `width`/`height` — resolution in pixels. Must be one of the
19163
+ * resolutions returned by {@link getStreamInfoList}.
19164
+ * - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
19165
+ * - `frameRate` — fps. Must match the table from `getStreamInfoList`.
19166
+ * - `videoEncType` — `"h264"` or `"h265"`
19167
+ * - `encoderType` — `"vbr"` or `"cbr"`
19168
+ * - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
19169
+ * - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
18550
19170
  *
18551
19171
  * @param channel - Channel number (0-based)
18552
- * @param patch - Fields to update on `mainStream` and/or `subStream`,
18553
- * plus a top-level `audio` toggle (0/1). Pass only what you want
18554
- * to change.
19172
+ * @param patch - Fields to update. Pass only the fields you want to change;
19173
+ * everything else is preserved from the device's current configuration.
18555
19174
  */
18556
19175
  async setEnc(channel, patch, options) {
18557
19176
  const ch = this.normalizeChannel(channel);
@@ -18568,6 +19187,7 @@ ${xml}`
18568
19187
  }
18569
19188
  xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
18570
19189
  xml = applyStreamPatch(xml, "subStream", patch.subStream);
19190
+ xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
18571
19191
  await this.sendXml({
18572
19192
  cmdId: BC_CMD_ID_SET_ENC,
18573
19193
  channel: ch,
@@ -19175,6 +19795,71 @@ ${xml}`
19175
19795
  `PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
19176
19796
  );
19177
19797
  }
19798
+ /**
19799
+ * Update the OSD timestamp + channel-name overlay via cmd_id=45
19800
+ * (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
19801
+ * `<OsdChannelName>` block returned by `getOsdDatetime` — we
19802
+ * read-modify-write so any extension fields the camera sent are
19803
+ * preserved.
19804
+ *
19805
+ * Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
19806
+ * not preset strings). Set `enable=0` to hide the overlay; the camera
19807
+ * keeps the stored position so re-enabling later restores it.
19808
+ */
19809
+ async setOsdDatetime(channel, patch, options) {
19810
+ const ch = this.normalizeChannel(channel);
19811
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
19812
+ let xml = await this.sendPcapDerivedSettingsGetXml({
19813
+ cmdId: BC_CMD_ID_GET_OSD_DATETIME,
19814
+ channel: ch,
19815
+ ...timeoutOpts
19816
+ });
19817
+ const patchBlock = (block, fields) => {
19818
+ const start = xml.indexOf(`<${block}`);
19819
+ if (start < 0) return;
19820
+ const end = xml.indexOf(`</${block}>`, start);
19821
+ if (end < 0) return;
19822
+ let body = xml.slice(start, end);
19823
+ for (const [tag, value] of Object.entries(fields)) {
19824
+ if (value === void 0) continue;
19825
+ const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
19826
+ const escaped = raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
19827
+ if (body.includes(`<${tag}>`)) {
19828
+ body = body.replace(
19829
+ new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
19830
+ `<${tag}>${escaped}</${tag}>`
19831
+ );
19832
+ } else {
19833
+ body += `<${tag}>${escaped}</${tag}>`;
19834
+ }
19835
+ }
19836
+ xml = xml.slice(0, start) + body + xml.slice(end);
19837
+ };
19838
+ if (patch.datetime) {
19839
+ patchBlock("OsdDatetime", {
19840
+ enable: patch.datetime.enable,
19841
+ topLeftX: patch.datetime.topLeftX,
19842
+ topLeftY: patch.datetime.topLeftY,
19843
+ language: patch.datetime.language
19844
+ });
19845
+ }
19846
+ if (patch.channelName) {
19847
+ patchBlock("OsdChannelName", {
19848
+ name: patch.channelName.name,
19849
+ enable: patch.channelName.enable,
19850
+ topLeftX: patch.channelName.topLeftX,
19851
+ topLeftY: patch.channelName.topLeftY,
19852
+ enWatermark: patch.channelName.enWatermark,
19853
+ enBgcolor: patch.channelName.enBgcolor
19854
+ });
19855
+ }
19856
+ await this.sendXml({
19857
+ cmdId: BC_CMD_ID_SET_OSD_DATETIME,
19858
+ channel: ch,
19859
+ payloadXml: ensureXmlHeader(xml),
19860
+ ...timeoutOpts
19861
+ });
19862
+ }
19178
19863
  async getOsdDatetime(channel, options) {
19179
19864
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
19180
19865
  cmdId: BC_CMD_ID_GET_OSD_DATETIME,
@@ -19367,6 +20052,41 @@ ${xml}`
19367
20052
  });
19368
20053
  return { streams };
19369
20054
  }
20055
+ /**
20056
+ * Return the set of values `setEnc` will accept on each stream of `channel`.
20057
+ * Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
20058
+ * per-stream resolutions with their allowed codecs/framerates/bitrates plus
20059
+ * the enumerated encoder modes/profiles Reolink exposes.
20060
+ *
20061
+ * Useful for populating selectors and validating user input before calling
20062
+ * `setEnc` — picking an unsupported combination causes the camera to reject
20063
+ * the SET_ENC command (responseCode != 200).
20064
+ */
20065
+ async getEncOptions(channel, options) {
20066
+ const list = await this.getStreamInfoList(channel, options);
20067
+ return buildEncOptions(list, channel);
20068
+ }
20069
+ /**
20070
+ * Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
20071
+ * friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
20072
+ * version, hardware revision, build day, AI model bundle version, etc.
20073
+ *
20074
+ * This is the same info the Reolink mobile app shows in "About this
20075
+ * device" — distinct from `getSystemGeneral` (cmd_104) which carries
20076
+ * time/locale.
20077
+ *
20078
+ * No channel parameter: this command is device-global on NVRs/Hubs and
20079
+ * camera-global on standalone cameras. Pass an explicit channel via the
20080
+ * underlying `sendXml` only if a specific firmware demands it (none we've
20081
+ * tested do).
20082
+ */
20083
+ async getVersionInfo(options) {
20084
+ const xml = await this.sendXml({
20085
+ cmdId: BC_CMD_ID_GET_VERSION_INFO,
20086
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20087
+ });
20088
+ return parseVersionInfo(xml);
20089
+ }
19370
20090
  async getLedState(channel, options) {
19371
20091
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
19372
20092
  cmdId: BC_CMD_ID_GET_LED_STATE,
@@ -22560,4 +23280,4 @@ export {
22560
23280
  isTcpFailureThatShouldFallbackToUdp,
22561
23281
  autoDetectDeviceType
22562
23282
  };
22563
- //# sourceMappingURL=chunk-HGQ53FB3.js.map
23283
+ //# sourceMappingURL=chunk-ND73IJIB.js.map