@apocaliss92/nodelink-js 0.4.11 → 0.4.13

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.
package/dist/index.cjs CHANGED
@@ -34,7 +34,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
34
34
  function bcHeaderHasPayloadOffset(messageClass) {
35
35
  return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
36
36
  }
37
- 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_LOGIN, 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_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, 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_ALARM_EVENT_LIST, 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_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, 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;
37
+ 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_LOGIN, 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_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, 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_ALARM_EVENT_LIST, 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_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_GET_EMAIL, BC_CMD_ID_SET_EMAIL, BC_CMD_ID_TEST_EMAIL, BC_CMD_ID_GET_NTP, BC_CMD_ID_SET_NTP, BC_CMD_ID_SET_SYSTEM_GENERAL, BC_CMD_ID_GET_DST, BC_CMD_ID_SET_DST, BC_CMD_ID_GET_AUTO_REBOOT, BC_CMD_ID_SET_AUTO_REBOOT, 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;
38
38
  var init_constants = __esm({
39
39
  "src/protocol/constants.ts"() {
40
40
  "use strict";
@@ -109,9 +109,11 @@ var init_constants = __esm({
109
109
  BC_CMD_ID_PING = 93;
110
110
  BC_CMD_ID_CHANNEL_INFO_ALL = 145;
111
111
  BC_CMD_ID_GET_OSD_DATETIME = 44;
112
+ BC_CMD_ID_SET_OSD_DATETIME = 45;
112
113
  BC_CMD_ID_GET_RECORD_CFG = 54;
113
114
  BC_CMD_ID_GET_ABILITY_SUPPORT = 58;
114
115
  BC_CMD_ID_GET_FTP_TASK = 70;
116
+ BC_CMD_ID_GET_VERSION_INFO = 80;
115
117
  BC_CMD_ID_GET_RECORD = 81;
116
118
  BC_CMD_ID_GET_HDD_INFO_LIST = 102;
117
119
  BC_CMD_ID_GET_WIFI_SIGNAL = 115;
@@ -153,6 +155,16 @@ var init_constants = __esm({
153
155
  BC_CMD_ID_SET_PUSH_TASK = 218;
154
156
  BC_CMD_ID_GET_AUTO_FOCUS = 224;
155
157
  BC_CMD_ID_SET_AUTO_FOCUS = 225;
158
+ BC_CMD_ID_GET_EMAIL = 42;
159
+ BC_CMD_ID_SET_EMAIL = 43;
160
+ BC_CMD_ID_TEST_EMAIL = 141;
161
+ BC_CMD_ID_GET_NTP = 38;
162
+ BC_CMD_ID_SET_NTP = 39;
163
+ BC_CMD_ID_SET_SYSTEM_GENERAL = 105;
164
+ BC_CMD_ID_GET_DST = 106;
165
+ BC_CMD_ID_SET_DST = 107;
166
+ BC_CMD_ID_GET_AUTO_REBOOT = 101;
167
+ BC_CMD_ID_SET_AUTO_REBOOT = 100;
156
168
  BC_CMD_ID_CMD_123 = 123;
157
169
  BC_CMD_ID_CMD_209 = 209;
158
170
  BC_CMD_ID_CMD_265 = 265;
@@ -480,6 +492,15 @@ function applyXmlTagPatch(xml, tag, value) {
480
492
  const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
481
493
  return xml.replace(re, `<${tag}>${v}</${tag}>`);
482
494
  }
495
+ function upsertXmlTag(xml, tag, value) {
496
+ if (value === void 0) return xml;
497
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
498
+ const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
499
+ if (re.test(xml)) {
500
+ return xml.replace(re, `<${tag}>${v}</${tag}>`);
501
+ }
502
+ return `${xml}<${tag}>${v}</${tag}>`;
503
+ }
483
504
  function patchNestedTag(xml, parent, child, value) {
484
505
  if (value === void 0) return xml;
485
506
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
@@ -495,6 +516,15 @@ function applyStreamPatch(xml, streamTag, patch) {
495
516
  );
496
517
  return xml.replace(re, (_match, open, body, close) => {
497
518
  let next = body;
519
+ if (patch.audio !== void 0) {
520
+ next = applyXmlTagPatch(next, "audio", patch.audio);
521
+ }
522
+ if (patch.width !== void 0) {
523
+ next = applyXmlTagPatch(next, "width", patch.width);
524
+ }
525
+ if (patch.height !== void 0) {
526
+ next = applyXmlTagPatch(next, "height", patch.height);
527
+ }
498
528
  if (patch.bitRate !== void 0) {
499
529
  next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
500
530
  }
@@ -506,6 +536,23 @@ function applyStreamPatch(xml, streamTag, patch) {
506
536
  const intVal = patch.videoEncType === "h265" ? 1 : 0;
507
537
  next = applyXmlTagPatch(next, "videoEncType", intVal);
508
538
  }
539
+ if (patch.encoderType !== void 0) {
540
+ next = upsertXmlTag(next, "encoderType", patch.encoderType);
541
+ }
542
+ if (patch.encoderProfile !== void 0) {
543
+ next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
544
+ }
545
+ if (patch.gop !== void 0) {
546
+ const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
547
+ if (gopBlockRe.test(next)) {
548
+ next = next.replace(
549
+ gopBlockRe,
550
+ (_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
551
+ );
552
+ } else {
553
+ next = `${next}<gop><cur>${patch.gop}</cur></gop>`;
554
+ }
555
+ }
509
556
  return `${open}${next}${close}`;
510
557
  });
511
558
  }
@@ -1117,10 +1164,15 @@ var init_BcMediaCodec = __esm({
1117
1164
  strict;
1118
1165
  amountSkipped = 0;
1119
1166
  logger;
1167
+ onUnknownChunk;
1120
1168
  constructor(strict = false, logger) {
1121
1169
  this.strict = strict;
1122
1170
  this.logger = logger;
1123
1171
  }
1172
+ /** Register a listener that fires for every unknown chunk before recovery. */
1173
+ setUnknownChunkListener(listener) {
1174
+ this.onUnknownChunk = listener;
1175
+ }
1124
1176
  /**
1125
1177
  * Push data into the codec buffer and try to parse complete BcMedia packets.
1126
1178
  * Returns an array of complete BcMedia packets found.
@@ -1181,6 +1233,13 @@ var init_BcMediaCodec = __esm({
1181
1233
  }
1182
1234
  }
1183
1235
  if (next > 0) {
1236
+ if (this.onUnknownChunk) {
1237
+ this.onUnknownChunk({
1238
+ magic,
1239
+ preview: Buffer.from(this.buffer.subarray(0, Math.min(256, next))),
1240
+ skipped: next
1241
+ });
1242
+ }
1184
1243
  this.amountSkipped += next;
1185
1244
  this.buffer = this.buffer.subarray(next);
1186
1245
  continue;
@@ -2085,6 +2144,10 @@ var init_BcMediaAnnexBDecoder = __esm({
2085
2144
  });
2086
2145
 
2087
2146
  // src/baichuan/stream/BaichuanVideoStream.ts
2147
+ var BaichuanVideoStream_exports = {};
2148
+ __export(BaichuanVideoStream_exports, {
2149
+ BaichuanVideoStream: () => BaichuanVideoStream
2150
+ });
2088
2151
  function removeEmulationPreventionBytes(rbsp) {
2089
2152
  const out = [];
2090
2153
  for (let i = 0; i < rbsp.length; i++) {
@@ -2218,6 +2281,15 @@ var init_BaichuanVideoStream = __esm({
2218
2281
  acceptAnyStreamType;
2219
2282
  lockedChannelId;
2220
2283
  bcMediaCodec;
2284
+ /**
2285
+ * Diagnostic-only accessor for the BcMedia codec. Used by tools that need to
2286
+ * inspect unknown chunks (for example to discover undocumented audio
2287
+ * sub-packets the parser currently skips). Not part of the supported public
2288
+ * surface — do not rely on it in application code.
2289
+ */
2290
+ get _bcMediaCodec() {
2291
+ return this.bcMediaCodec;
2292
+ }
2221
2293
  debugH264LogsLeft;
2222
2294
  debugSavedSamples;
2223
2295
  warnedNonAnnexBOnce = false;
@@ -2249,6 +2321,14 @@ var init_BaichuanVideoStream = __esm({
2249
2321
  // Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
2250
2322
  // In CFB mode, continuation frames must use the cipher state from previous frames.
2251
2323
  aesStreamDecryptor = null;
2324
+ // Latest frame dimensions reported by BcMedia InfoV1/V2 packets.
2325
+ // Used to attach width/height context to the `additionalHeader` event so
2326
+ // consumers can normalize box coordinates to a fraction of the stream size.
2327
+ latestFrameWidth;
2328
+ latestFrameHeight;
2329
+ // Teardown returned by ReolinkBaichuanApi._registerVideoStreamForDetection.
2330
+ // Called from stop() to detach the detection bridge.
2331
+ detectionTeardown;
2252
2332
  /**
2253
2333
  * Pending startup error stashed when emitSafeError is called before any
2254
2334
  * "error" listener is registered (e.g. camera returns 400 during start()).
@@ -2911,6 +2991,16 @@ var init_BaichuanVideoStream = __esm({
2911
2991
  }
2912
2992
  videoType = detectedCodec;
2913
2993
  }
2994
+ if (media.additionalHeader && media.additionalHeader.length > 0) {
2995
+ this.emit("additionalHeader", {
2996
+ raw: media.additionalHeader,
2997
+ frameType: "Iframe",
2998
+ videoType,
2999
+ microseconds: media.microseconds,
3000
+ ...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
3001
+ ...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
3002
+ });
3003
+ }
2914
3004
  const annexBData = videoType === "H265" ? convertToAnnexB2(media.data) : convertToAnnexB(media.data);
2915
3005
  const isKeyframe = true;
2916
3006
  maybeCacheParamSets(annexBData, "Iframe", videoType);
@@ -2992,6 +3082,18 @@ var init_BaichuanVideoStream = __esm({
2992
3082
  }
2993
3083
  } else if (media.type === "Pframe") {
2994
3084
  const chunk = media.data;
3085
+ if (media.additionalHeader && media.additionalHeader.length > 0) {
3086
+ const detected = detectVideoCodecFromNal(chunk);
3087
+ const videoTypeForHeader = detected ?? media.videoType;
3088
+ this.emit("additionalHeader", {
3089
+ raw: media.additionalHeader,
3090
+ frameType: "Pframe",
3091
+ videoType: videoTypeForHeader,
3092
+ microseconds: media.microseconds,
3093
+ ...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
3094
+ ...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
3095
+ });
3096
+ }
2995
3097
  let videoType = media.videoType;
2996
3098
  const detectedCodec = detectVideoCodecFromNal(chunk);
2997
3099
  if (detectedCodec && detectedCodec !== videoType) {
@@ -3034,6 +3136,8 @@ var init_BaichuanVideoStream = __esm({
3034
3136
  this.emit("audioFrame", media.data);
3035
3137
  }
3036
3138
  if (media.type === "InfoV1" || media.type === "InfoV2") {
3139
+ if (media.videoWidth > 0) this.latestFrameWidth = media.videoWidth;
3140
+ if (media.videoHeight > 0) this.latestFrameHeight = media.videoHeight;
3037
3141
  }
3038
3142
  }
3039
3143
  if (totalFramesReceived <= 10 || totalFramesReceived % 20 === 0 && (videoFramesEmitted > 0 || audioFramesEmitted > 0)) {
@@ -3053,6 +3157,12 @@ var init_BaichuanVideoStream = __esm({
3053
3157
  this.client.on("push", this.videoFrameHandler);
3054
3158
  this.active = true;
3055
3159
  this.startWatchdog();
3160
+ if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
3161
+ this.detectionTeardown = this.api._registerVideoStreamForDetection(this, {
3162
+ channel: this.channel,
3163
+ profile: this.profile
3164
+ });
3165
+ }
3056
3166
  this.lastMediaAtMs = Date.now();
3057
3167
  if (this.api) {
3058
3168
  try {
@@ -3150,6 +3260,13 @@ var init_BaichuanVideoStream = __esm({
3150
3260
  }
3151
3261
  }
3152
3262
  this.active = false;
3263
+ if (this.detectionTeardown) {
3264
+ try {
3265
+ this.detectionTeardown();
3266
+ } catch {
3267
+ }
3268
+ this.detectionTeardown = void 0;
3269
+ }
3153
3270
  this.emit("close");
3154
3271
  }
3155
3272
  isActive() {
@@ -7377,12 +7494,12 @@ var init_ReolinkCgiApi = __esm({
7377
7494
  "getVideoclipThumbnailJpeg",
7378
7495
  `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`
7379
7496
  );
7380
- const { spawn: spawn12 } = await import("child_process");
7497
+ const { spawn: spawn13 } = await import("child_process");
7381
7498
  return new Promise((resolve, reject) => {
7382
7499
  const chunks = [];
7383
7500
  let stderr = "";
7384
7501
  let timedOut = false;
7385
- const ffmpeg = spawn12(ffmpegPath, [
7502
+ const ffmpeg = spawn13(ffmpegPath, [
7386
7503
  "-y",
7387
7504
  "-analyzeduration",
7388
7505
  "10000000",
@@ -8010,6 +8127,7 @@ __export(index_exports, {
8010
8127
  BC_CMD_ID_GET_AUDIO_CFG: () => BC_CMD_ID_GET_AUDIO_CFG,
8011
8128
  BC_CMD_ID_GET_AUDIO_TASK: () => BC_CMD_ID_GET_AUDIO_TASK,
8012
8129
  BC_CMD_ID_GET_AUTO_FOCUS: () => BC_CMD_ID_GET_AUTO_FOCUS,
8130
+ BC_CMD_ID_GET_AUTO_REBOOT: () => BC_CMD_ID_GET_AUTO_REBOOT,
8013
8131
  BC_CMD_ID_GET_BATTERY_INFO: () => BC_CMD_ID_GET_BATTERY_INFO,
8014
8132
  BC_CMD_ID_GET_BATTERY_INFO_LIST: () => BC_CMD_ID_GET_BATTERY_INFO_LIST,
8015
8133
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
@@ -8017,6 +8135,8 @@ __export(index_exports, {
8017
8135
  BC_CMD_ID_GET_DING_DONG_CFG: () => BC_CMD_ID_GET_DING_DONG_CFG,
8018
8136
  BC_CMD_ID_GET_DING_DONG_LIST: () => BC_CMD_ID_GET_DING_DONG_LIST,
8019
8137
  BC_CMD_ID_GET_DING_DONG_SILENT: () => BC_CMD_ID_GET_DING_DONG_SILENT,
8138
+ BC_CMD_ID_GET_DST: () => BC_CMD_ID_GET_DST,
8139
+ BC_CMD_ID_GET_EMAIL: () => BC_CMD_ID_GET_EMAIL,
8020
8140
  BC_CMD_ID_GET_EMAIL_TASK: () => BC_CMD_ID_GET_EMAIL_TASK,
8021
8141
  BC_CMD_ID_GET_ENC: () => BC_CMD_ID_GET_ENC,
8022
8142
  BC_CMD_ID_GET_FTP_TASK: () => BC_CMD_ID_GET_FTP_TASK,
@@ -8024,6 +8144,7 @@ __export(index_exports, {
8024
8144
  BC_CMD_ID_GET_KIT_AP_CFG: () => BC_CMD_ID_GET_KIT_AP_CFG,
8025
8145
  BC_CMD_ID_GET_LED_STATE: () => BC_CMD_ID_GET_LED_STATE,
8026
8146
  BC_CMD_ID_GET_MOTION_ALARM: () => BC_CMD_ID_GET_MOTION_ALARM,
8147
+ BC_CMD_ID_GET_NTP: () => BC_CMD_ID_GET_NTP,
8027
8148
  BC_CMD_ID_GET_ONLINE_USER_LIST: () => BC_CMD_ID_GET_ONLINE_USER_LIST,
8028
8149
  BC_CMD_ID_GET_OSD_DATETIME: () => BC_CMD_ID_GET_OSD_DATETIME,
8029
8150
  BC_CMD_ID_GET_PIR_INFO: () => BC_CMD_ID_GET_PIR_INFO,
@@ -8040,6 +8161,7 @@ __export(index_exports, {
8040
8161
  BC_CMD_ID_GET_SUPPORT: () => BC_CMD_ID_GET_SUPPORT,
8041
8162
  BC_CMD_ID_GET_SYSTEM_GENERAL: () => BC_CMD_ID_GET_SYSTEM_GENERAL,
8042
8163
  BC_CMD_ID_GET_TIMELAPSE_CFG: () => BC_CMD_ID_GET_TIMELAPSE_CFG,
8164
+ BC_CMD_ID_GET_VERSION_INFO: () => BC_CMD_ID_GET_VERSION_INFO,
8043
8165
  BC_CMD_ID_GET_VIDEO_INPUT: () => BC_CMD_ID_GET_VIDEO_INPUT,
8044
8166
  BC_CMD_ID_GET_WHITE_LED: () => BC_CMD_ID_GET_WHITE_LED,
8045
8167
  BC_CMD_ID_GET_WIFI: () => BC_CMD_ID_GET_WIFI,
@@ -8063,18 +8185,24 @@ __export(index_exports, {
8063
8185
  BC_CMD_ID_SET_AUDIO_CFG: () => BC_CMD_ID_SET_AUDIO_CFG,
8064
8186
  BC_CMD_ID_SET_AUDIO_TASK: () => BC_CMD_ID_SET_AUDIO_TASK,
8065
8187
  BC_CMD_ID_SET_AUTO_FOCUS: () => BC_CMD_ID_SET_AUTO_FOCUS,
8188
+ BC_CMD_ID_SET_AUTO_REBOOT: () => BC_CMD_ID_SET_AUTO_REBOOT,
8066
8189
  BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
8067
8190
  BC_CMD_ID_SET_DING_DONG_CFG: () => BC_CMD_ID_SET_DING_DONG_CFG,
8068
8191
  BC_CMD_ID_SET_DING_DONG_SILENT: () => BC_CMD_ID_SET_DING_DONG_SILENT,
8192
+ BC_CMD_ID_SET_DST: () => BC_CMD_ID_SET_DST,
8193
+ BC_CMD_ID_SET_EMAIL: () => BC_CMD_ID_SET_EMAIL,
8069
8194
  BC_CMD_ID_SET_EMAIL_TASK: () => BC_CMD_ID_SET_EMAIL_TASK,
8070
8195
  BC_CMD_ID_SET_ENC: () => BC_CMD_ID_SET_ENC,
8071
8196
  BC_CMD_ID_SET_LED_STATE: () => BC_CMD_ID_SET_LED_STATE,
8072
8197
  BC_CMD_ID_SET_MOTION_ALARM: () => BC_CMD_ID_SET_MOTION_ALARM,
8198
+ BC_CMD_ID_SET_NTP: () => BC_CMD_ID_SET_NTP,
8199
+ BC_CMD_ID_SET_OSD_DATETIME: () => BC_CMD_ID_SET_OSD_DATETIME,
8073
8200
  BC_CMD_ID_SET_PIR_INFO: () => BC_CMD_ID_SET_PIR_INFO,
8074
8201
  BC_CMD_ID_SET_PRIVACY_MASK: () => BC_CMD_ID_SET_PRIVACY_MASK,
8075
8202
  BC_CMD_ID_SET_PUSH_TASK: () => BC_CMD_ID_SET_PUSH_TASK,
8076
8203
  BC_CMD_ID_SET_RECORD: () => BC_CMD_ID_SET_RECORD,
8077
8204
  BC_CMD_ID_SET_RECORD_CFG: () => BC_CMD_ID_SET_RECORD_CFG,
8205
+ BC_CMD_ID_SET_SYSTEM_GENERAL: () => BC_CMD_ID_SET_SYSTEM_GENERAL,
8078
8206
  BC_CMD_ID_SET_VIDEO_INPUT: () => BC_CMD_ID_SET_VIDEO_INPUT,
8079
8207
  BC_CMD_ID_SET_WHITE_LED_STATE: () => BC_CMD_ID_SET_WHITE_LED_STATE,
8080
8208
  BC_CMD_ID_SET_WHITE_LED_TASK: () => BC_CMD_ID_SET_WHITE_LED_TASK,
@@ -8084,6 +8212,7 @@ __export(index_exports, {
8084
8212
  BC_CMD_ID_TALK_ABILITY: () => BC_CMD_ID_TALK_ABILITY,
8085
8213
  BC_CMD_ID_TALK_CONFIG: () => BC_CMD_ID_TALK_CONFIG,
8086
8214
  BC_CMD_ID_TALK_RESET: () => BC_CMD_ID_TALK_RESET,
8215
+ BC_CMD_ID_TEST_EMAIL: () => BC_CMD_ID_TEST_EMAIL,
8087
8216
  BC_CMD_ID_UDP_KEEP_ALIVE: () => BC_CMD_ID_UDP_KEEP_ALIVE,
8088
8217
  BC_CMD_ID_VIDEO: () => BC_CMD_ID_VIDEO,
8089
8218
  BC_CMD_ID_VIDEO_STOP: () => BC_CMD_ID_VIDEO_STOP,
@@ -8178,6 +8307,7 @@ __export(index_exports, {
8178
8307
  decideSleepInferenceTransition: () => decideSleepInferenceTransition,
8179
8308
  decideVideoclipTranscodeMode: () => decideVideoclipTranscodeMode,
8180
8309
  decodeHeader: () => decodeHeader,
8310
+ decodeMotionScopeBitmap: () => decodeMotionScopeBitmap,
8181
8311
  deriveAesKey: () => deriveAesKey,
8182
8312
  detectIosClient: () => detectIosClient,
8183
8313
  detectVideoCodecFromNal: () => detectVideoCodecFromNal,
@@ -8190,6 +8320,7 @@ __export(index_exports, {
8190
8320
  discoverViaUdpBroadcast: () => discoverViaUdpBroadcast,
8191
8321
  discoverViaUdpDirect: () => discoverViaUdpDirect,
8192
8322
  encodeHeader: () => encodeHeader,
8323
+ encodeMotionScopeBitmap: () => encodeMotionScopeBitmap,
8193
8324
  ensureXmlHeader: () => ensureXmlHeader,
8194
8325
  extractH264ParamSetsFromAccessUnit: () => extractH264ParamSetsFromAccessUnit,
8195
8326
  extractH265ParamSetsFromAccessUnit: () => extractH265ParamSetsFromAccessUnit,
@@ -8198,6 +8329,7 @@ __export(index_exports, {
8198
8329
  extractVpsFromAnnexB: () => extractVpsFromAnnexB,
8199
8330
  flattenAbilitiesForChannel: () => flattenAbilitiesForChannel,
8200
8331
  formatMjpegFrame: () => formatMjpegFrame,
8332
+ fullCoverageScope: () => fullCoverageScope,
8201
8333
  getConstructedVideoStreamOptions: () => getConstructedVideoStreamOptions,
8202
8334
  getGlobalLogger: () => getGlobalLogger,
8203
8335
  getH265NalType: () => getH265NalType,
@@ -8241,6 +8373,7 @@ __export(index_exports, {
8241
8373
  splitAnnexBToNals: () => splitAnnexBToNals,
8242
8374
  splitH265AnnexBToNalPayloads: () => splitAnnexBToNalPayloads2,
8243
8375
  testChannelStreams: () => testChannelStreams,
8376
+ upsertXmlTag: () => upsertXmlTag,
8244
8377
  xmlEscape: () => xmlEscape,
8245
8378
  xmlIndicatesFloodlight: () => xmlIndicatesFloodlight,
8246
8379
  zipDirectory: () => zipDirectory
@@ -9969,6 +10102,23 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
9969
10102
  * even if the current client instance is idle/disconnected.
9970
10103
  */
9971
10104
  static streamingRegistry = /* @__PURE__ */ new Map();
10105
+ /**
10106
+ * Per-device set of live BaichuanClient instances.
10107
+ *
10108
+ * Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
10109
+ * and SocketPool tears the streaming socket down), the global streaming
10110
+ * registry decrements but the GENERAL client of the same device has no
10111
+ * way of knowing — its idle-disconnect timer was last evaluated while
10112
+ * `isDeviceStreamingActive()` was still true (because the streaming socket
10113
+ * was still alive) and wasn't rescheduled. Without this registry the
10114
+ * general socket stays connected, the 60-second session-guard timer keeps
10115
+ * sending getOnlineUserList() to the camera, and a battery camera ends up
10116
+ * waking up every minute (issue #18).
10117
+ *
10118
+ * On streamingRegistry decrement-to-zero we walk this set and kick every
10119
+ * sibling's idle-disconnect timer so it can re-evaluate eligibility.
10120
+ */
10121
+ static deviceClients = /* @__PURE__ */ new Map();
9972
10122
  /**
9973
10123
  * Per-host D2C_DISC backoff state that persists across client instance recreation.
9974
10124
  *
@@ -10083,6 +10233,29 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
10083
10233
  // AlarmEventList (cmdId=33) can be very chatty (often sent every second).
10084
10234
  // Track last per-channel alarm state so we only emit on transitions.
10085
10235
  alarmEventState = /* @__PURE__ */ new Map();
10236
+ /** Whether this instance is currently in BaichuanClient.deviceClients. */
10237
+ registeredInDeviceClients = false;
10238
+ registerInDeviceClients() {
10239
+ if (this.registeredInDeviceClients) return;
10240
+ const key = this.getDeviceRegistryKey();
10241
+ let set = _BaichuanClient.deviceClients.get(key);
10242
+ if (!set) {
10243
+ set = /* @__PURE__ */ new Set();
10244
+ _BaichuanClient.deviceClients.set(key, set);
10245
+ }
10246
+ set.add(this);
10247
+ this.registeredInDeviceClients = true;
10248
+ }
10249
+ unregisterFromDeviceClients() {
10250
+ if (!this.registeredInDeviceClients) return;
10251
+ const key = this.getDeviceRegistryKey();
10252
+ const set = _BaichuanClient.deviceClients.get(key);
10253
+ if (set) {
10254
+ set.delete(this);
10255
+ if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
10256
+ }
10257
+ this.registeredInDeviceClients = false;
10258
+ }
10086
10259
  constructor(options) {
10087
10260
  super();
10088
10261
  this.opts = options;
@@ -10097,6 +10270,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
10097
10270
  code: err?.code
10098
10271
  });
10099
10272
  });
10273
+ this.registerInDeviceClients();
10100
10274
  }
10101
10275
  newSocketSessionId(transport) {
10102
10276
  const short = (0, import_node_crypto2.randomUUID)().split("-")[0] ?? (0, import_node_crypto2.randomUUID)().slice(0, 8);
@@ -10353,6 +10527,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
10353
10527
  activeStreamClients: nextCount
10354
10528
  });
10355
10529
  this.contributesToGlobalStreamingRegistry = shouldContribute;
10530
+ if (!shouldContribute && nextCount === 0) {
10531
+ const siblings = _BaichuanClient.deviceClients.get(key);
10532
+ if (siblings) {
10533
+ for (const sib of siblings) {
10534
+ if (sib === this) continue;
10535
+ try {
10536
+ sib.kickIdleDisconnectTimer();
10537
+ } catch {
10538
+ }
10539
+ }
10540
+ }
10541
+ }
10356
10542
  }
10357
10543
  /**
10358
10544
  * True if the device should be considered "awake" due to active streaming.
@@ -10817,6 +11003,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
10817
11003
  `transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
10818
11004
  );
10819
11005
  this.logSocketState("tcp_connected");
11006
+ this.registerInDeviceClients();
10820
11007
  this.startKeepAlive();
10821
11008
  this.kickIdleDisconnectTimer();
10822
11009
  }
@@ -11133,6 +11320,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
11133
11320
  this.logDebug("udp_close_error", e);
11134
11321
  }
11135
11322
  }
11323
+ this.unregisterFromDeviceClients();
11136
11324
  }
11137
11325
  handleFrame(frame) {
11138
11326
  const now = Date.now();
@@ -16134,6 +16322,309 @@ function parseXmlFragmentToJson(xml) {
16134
16322
  return parsed.root;
16135
16323
  }
16136
16324
 
16325
+ // src/reolink/baichuan/utils/email.ts
16326
+ init_xml();
16327
+ var parseInt01 = (text) => {
16328
+ if (text === void 0) return void 0;
16329
+ return text.trim() === "1" ? 1 : 0;
16330
+ };
16331
+ var parseNumberSafe = (text) => {
16332
+ if (text === void 0) return void 0;
16333
+ const n = Number(text);
16334
+ return Number.isFinite(n) ? n : void 0;
16335
+ };
16336
+ var VALID_ATTACHMENT_TYPES = [
16337
+ "picture",
16338
+ "video",
16339
+ "none"
16340
+ ];
16341
+ var VALID_TEXT_TYPES = ["withText", "noText"];
16342
+ var normalizeAttachmentType = (text) => {
16343
+ const t = (text ?? "").trim();
16344
+ return VALID_ATTACHMENT_TYPES.includes(t) ? t : "picture";
16345
+ };
16346
+ var normalizeTextType = (text) => {
16347
+ const t = (text ?? "").trim();
16348
+ return VALID_TEXT_TYPES.includes(t) ? t : "withText";
16349
+ };
16350
+ function parseEmailConfigFromXml(xml) {
16351
+ const cfg = {
16352
+ smtpServer: getXmlText(xml, "smtpServer") ?? "",
16353
+ userName: getXmlText(xml, "userName") ?? "",
16354
+ password: getXmlText(xml, "password") ?? "",
16355
+ address1: getXmlText(xml, "address1") ?? "",
16356
+ address2: getXmlText(xml, "address2") ?? "",
16357
+ address3: getXmlText(xml, "address3") ?? "",
16358
+ smtpPort: parseNumberSafe(getXmlText(xml, "smtpPort")) ?? 0,
16359
+ sendNickname: getXmlText(xml, "sendNickname") ?? "",
16360
+ attachment: parseInt01(getXmlText(xml, "attachment")) ?? 0,
16361
+ attachmentType: normalizeAttachmentType(getXmlText(xml, "attachmentType")),
16362
+ textType: normalizeTextType(getXmlText(xml, "textType")),
16363
+ ssl: parseInt01(getXmlText(xml, "ssl")) ?? 0,
16364
+ interval: parseNumberSafe(getXmlText(xml, "interval")) ?? 30
16365
+ };
16366
+ const senderMaxLen = parseNumberSafe(getXmlText(xml, "senderMaxLen"));
16367
+ if (senderMaxLen !== void 0) cfg.senderMaxLen = senderMaxLen;
16368
+ const pwdMaxLen = parseNumberSafe(getXmlText(xml, "pwdMaxLen"));
16369
+ if (pwdMaxLen !== void 0) cfg.pwdMaxLen = pwdMaxLen;
16370
+ const ability = parseNumberSafe(getXmlText(xml, "emailAttachAbility"));
16371
+ if (ability !== void 0) cfg.emailAttachAbility = ability;
16372
+ return cfg;
16373
+ }
16374
+ function buildSetEmailXml(current, patch) {
16375
+ const merged = { ...current, ...patch };
16376
+ return ensureXmlHeader(
16377
+ `<body><Email version="1.1"><smtpServer>${xmlEscape(merged.smtpServer)}</smtpServer><userName>${xmlEscape(merged.userName)}</userName><password>${xmlEscape(merged.password)}</password><address1>${xmlEscape(merged.address1)}</address1><address2>${xmlEscape(merged.address2)}</address2><address3>${xmlEscape(merged.address3)}</address3><smtpPort>${merged.smtpPort}</smtpPort><sendNickname>${xmlEscape(merged.sendNickname)}</sendNickname><attachment>${merged.attachment}</attachment><attachmentType>${merged.attachmentType}</attachmentType><textType>${merged.textType}</textType><ssl>${merged.ssl}</ssl><interval>${merged.interval}</interval></Email></body>`
16378
+ );
16379
+ }
16380
+ function parseEmailTaskFromXml(xml) {
16381
+ const channelId = parseNumberSafe(getXmlText(xml, "channelId")) ?? 0;
16382
+ const enable = parseInt01(getXmlText(xml, "enable")) ?? 0;
16383
+ const items = [];
16384
+ const itemRe = /<item>([\s\S]*?)<\/item>/g;
16385
+ let match;
16386
+ while ((match = itemRe.exec(xml)) !== null) {
16387
+ const block = match[1] ?? "";
16388
+ items.push({
16389
+ type: getXmlText(block, "type") ?? "none",
16390
+ valueTable: getXmlText(block, "valueTable") ?? ""
16391
+ });
16392
+ }
16393
+ return { channelId, enable, typeScheduleList: items };
16394
+ }
16395
+ function buildEmailScheduleValueTable(spec) {
16396
+ if (spec.kind === "always") return "1".repeat(168);
16397
+ const grid = new Array(168).fill("0");
16398
+ if (spec.kind === "never") return grid.join("");
16399
+ for (const w of spec.windows) {
16400
+ const startH = Math.max(0, Math.min(24, w.startHour));
16401
+ const endH = Math.max(startH, Math.min(24, w.endHour));
16402
+ for (const d of w.days) {
16403
+ if (d < 0 || d > 6) continue;
16404
+ for (let h = startH; h < endH; h++) {
16405
+ grid[d * 24 + h] = "1";
16406
+ }
16407
+ }
16408
+ }
16409
+ return grid.join("");
16410
+ }
16411
+ function buildSetEmailTaskXml(task) {
16412
+ const items = task.typeScheduleList.map(
16413
+ (item) => `<item><type>${xmlEscape(item.type)}</type><valueTable>${xmlEscape(item.valueTable)}</valueTable></item>`
16414
+ ).join("");
16415
+ return ensureXmlHeader(
16416
+ `<body><EmailTask version="1.1"><channelId>${task.channelId}</channelId><enable>${task.enable}</enable><typeScheduleList>${items}</typeScheduleList></EmailTask></body>`
16417
+ );
16418
+ }
16419
+
16420
+ // src/reolink/baichuan/utils/ntp.ts
16421
+ init_xml();
16422
+ var parseNumberSafe2 = (text) => {
16423
+ if (text === void 0) return void 0;
16424
+ const n = Number(text);
16425
+ return Number.isFinite(n) ? n : void 0;
16426
+ };
16427
+ var parseInt012 = (text) => {
16428
+ if (text === void 0) return void 0;
16429
+ return text.trim() === "1" ? 1 : 0;
16430
+ };
16431
+ function parseNtpConfigFromXml(xml) {
16432
+ return {
16433
+ enable: parseInt012(getXmlText(xml, "enable")) ?? 0,
16434
+ server: getXmlText(xml, "server") ?? "",
16435
+ synchronizeInterval: parseNumberSafe2(getXmlText(xml, "synchronizeInterval")) ?? 1440,
16436
+ port: parseNumberSafe2(getXmlText(xml, "port")) ?? 123
16437
+ };
16438
+ }
16439
+ function buildSetNtpXml(current, patch) {
16440
+ const merged = { ...current, ...patch };
16441
+ return ensureXmlHeader(
16442
+ `<body><Ntp version="1.1"><enable>${merged.enable}</enable><server>${xmlEscape(merged.server)}</server><synchronizeInterval>${merged.synchronizeInterval}</synchronizeInterval><port>${merged.port}</port></Ntp></body>`
16443
+ );
16444
+ }
16445
+
16446
+ // src/reolink/baichuan/utils/dst.ts
16447
+ init_xml();
16448
+ var parseNumberSafe3 = (text) => {
16449
+ if (text === void 0) return void 0;
16450
+ const n = Number(text);
16451
+ return Number.isFinite(n) ? n : void 0;
16452
+ };
16453
+ var parseInt013 = (text) => {
16454
+ if (text === void 0) return void 0;
16455
+ return text.trim() === "1" ? 1 : 0;
16456
+ };
16457
+ var VALID_WEEKDAYS = [
16458
+ "Sunday",
16459
+ "Monday",
16460
+ "Tuesday",
16461
+ "Wednesday",
16462
+ "Thursday",
16463
+ "Friday",
16464
+ "Saturday"
16465
+ ];
16466
+ var normalizeWeekday = (text) => {
16467
+ const t = (text ?? "").trim();
16468
+ return VALID_WEEKDAYS.includes(t) ? t : "Sunday";
16469
+ };
16470
+ function parseDstConfigFromXml(xml) {
16471
+ const cfg = {
16472
+ enable: parseInt013(getXmlText(xml, "enable")) ?? 0,
16473
+ offset: parseNumberSafe3(getXmlText(xml, "offset")) ?? 1,
16474
+ startMonth: parseNumberSafe3(getXmlText(xml, "startMonth")) ?? 3,
16475
+ startWeekIndex: parseNumberSafe3(getXmlText(xml, "startWeekIndex")) ?? 5,
16476
+ startWeekday: normalizeWeekday(getXmlText(xml, "startWeekday")),
16477
+ startHour: parseNumberSafe3(getXmlText(xml, "startHour")) ?? 2,
16478
+ startMinute: parseNumberSafe3(getXmlText(xml, "startMinute")) ?? 0,
16479
+ startSecond: parseNumberSafe3(getXmlText(xml, "startSecond")) ?? 0,
16480
+ endMonth: parseNumberSafe3(getXmlText(xml, "endMonth")) ?? 10,
16481
+ endWeekIndex: parseNumberSafe3(getXmlText(xml, "endWeekIndex")) ?? 4,
16482
+ endWeekday: normalizeWeekday(getXmlText(xml, "endWeekday")),
16483
+ endHour: parseNumberSafe3(getXmlText(xml, "endHour")) ?? 3,
16484
+ endMinute: parseNumberSafe3(getXmlText(xml, "endMinute")) ?? 0,
16485
+ endSecond: parseNumberSafe3(getXmlText(xml, "endSecond")) ?? 0
16486
+ };
16487
+ const version = parseNumberSafe3(getXmlText(xml, "version"));
16488
+ if (version !== void 0) cfg.version = version;
16489
+ return cfg;
16490
+ }
16491
+ function buildSetDstXml(current, patch) {
16492
+ const merged = { ...current, ...patch };
16493
+ return ensureXmlHeader(
16494
+ `<body><Dst version="1.1"><enable>${merged.enable}</enable><offset>${merged.offset}</offset><startMonth>${merged.startMonth}</startMonth><startWeekIndex>${merged.startWeekIndex}</startWeekIndex><startWeekday>${xmlEscape(merged.startWeekday)}</startWeekday><startHour>${merged.startHour}</startHour><startMinute>${merged.startMinute}</startMinute><startSecond>${merged.startSecond}</startSecond><endMonth>${merged.endMonth}</endMonth><endWeekIndex>${merged.endWeekIndex}</endWeekIndex><endWeekday>${xmlEscape(merged.endWeekday)}</endWeekday><endHour>${merged.endHour}</endHour><endMinute>${merged.endMinute}</endMinute><endSecond>${merged.endSecond}</endSecond></Dst></body>`
16495
+ );
16496
+ }
16497
+
16498
+ // src/reolink/baichuan/utils/autoReboot.ts
16499
+ init_xml();
16500
+ var parseNumberSafe4 = (text) => {
16501
+ if (text === void 0) return void 0;
16502
+ const n = Number(text);
16503
+ return Number.isFinite(n) ? n : void 0;
16504
+ };
16505
+ var parseInt014 = (text) => {
16506
+ if (text === void 0) return void 0;
16507
+ return text.trim() === "1" ? 1 : 0;
16508
+ };
16509
+ var VALID_WEEKDAYS2 = [
16510
+ "Sunday",
16511
+ "Monday",
16512
+ "Tuesday",
16513
+ "Wednesday",
16514
+ "Thursday",
16515
+ "Friday",
16516
+ "Saturday",
16517
+ "everyday"
16518
+ ];
16519
+ var normalizeWeekday2 = (text) => {
16520
+ const t = (text ?? "").trim();
16521
+ return VALID_WEEKDAYS2.includes(t) ? t : "Sunday";
16522
+ };
16523
+ function parseAutoRebootFromXml(xml) {
16524
+ return {
16525
+ enable: parseInt014(getXmlText(xml, "enable")) ?? 0,
16526
+ weekDay: normalizeWeekday2(getXmlText(xml, "weekDay")),
16527
+ hour: parseNumberSafe4(getXmlText(xml, "hour")) ?? 0,
16528
+ minute: parseNumberSafe4(getXmlText(xml, "minute")) ?? 0,
16529
+ second: parseNumberSafe4(getXmlText(xml, "second")) ?? 0
16530
+ };
16531
+ }
16532
+ function buildSetAutoRebootXml(current, patch) {
16533
+ const merged = { ...current, ...patch };
16534
+ return ensureXmlHeader(
16535
+ `<body><AutoReboot version="1.1"><enable>${merged.enable}</enable><weekDay>${xmlEscape(merged.weekDay)}</weekDay><hour>${merged.hour}</hour><minute>${merged.minute}</minute><second>${merged.second}</second></AutoReboot></body>`
16536
+ );
16537
+ }
16538
+
16539
+ // src/reolink/baichuan/utils/systemGeneral.ts
16540
+ init_xml();
16541
+ var parseNumberSafe5 = (text) => {
16542
+ if (text === void 0) return void 0;
16543
+ const n = Number(text);
16544
+ return Number.isFinite(n) ? n : void 0;
16545
+ };
16546
+ var parseInt015 = (text) => {
16547
+ if (text === void 0) return void 0;
16548
+ return text.trim() === "1" ? 1 : 0;
16549
+ };
16550
+ var VALID_OSD_FORMATS = ["DMY", "MDY", "YMD"];
16551
+ var normalizeOsdFormat = (text) => {
16552
+ const t = (text ?? "").trim();
16553
+ return VALID_OSD_FORMATS.includes(t) ? t : "YMD";
16554
+ };
16555
+ function parseSystemGeneralFromXml(xml) {
16556
+ return {
16557
+ timeZone: parseNumberSafe5(getXmlText(xml, "timeZone")) ?? 0,
16558
+ osdFormat: normalizeOsdFormat(getXmlText(xml, "osdFormat")),
16559
+ year: parseNumberSafe5(getXmlText(xml, "year")) ?? 0,
16560
+ month: parseNumberSafe5(getXmlText(xml, "month")) ?? 0,
16561
+ day: parseNumberSafe5(getXmlText(xml, "day")) ?? 0,
16562
+ hour: parseNumberSafe5(getXmlText(xml, "hour")) ?? 0,
16563
+ minute: parseNumberSafe5(getXmlText(xml, "minute")) ?? 0,
16564
+ second: parseNumberSafe5(getXmlText(xml, "second")) ?? 0,
16565
+ deviceId: parseNumberSafe5(getXmlText(xml, "deviceId")) ?? 0,
16566
+ timeFormat: parseInt015(getXmlText(xml, "timeFormat")) ?? 0,
16567
+ language: getXmlText(xml, "language") ?? "English",
16568
+ deviceName: getXmlText(xml, "deviceName") ?? "",
16569
+ loginLock: parseInt015(getXmlText(xml, "loginLock")) ?? 0,
16570
+ lockTime: parseNumberSafe5(getXmlText(xml, "lockTime")) ?? 0,
16571
+ allowedTimes: parseNumberSafe5(getXmlText(xml, "allowedTimes")) ?? 0,
16572
+ isDst: parseInt015(getXmlText(xml, "isDst")) ?? 0
16573
+ };
16574
+ }
16575
+ function buildSetSystemGeneralXml(patch) {
16576
+ const parts = [];
16577
+ const isDeviceNameOnly = patch.deviceName !== void 0 && patch.timeZone === void 0 && patch.osdFormat === void 0 && patch.timeFormat === void 0 && patch.language === void 0 && patch.loginLock === void 0 && patch.lockTime === void 0 && patch.allowedTimes === void 0 && patch.manualTime === void 0;
16578
+ if (isDeviceNameOnly) {
16579
+ parts.push("<year>0</year>");
16580
+ parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
16581
+ parts.push("<deviceNameOnly>1</deviceNameOnly>");
16582
+ } else if (patch.manualTime !== void 0) {
16583
+ const mt = patch.manualTime;
16584
+ if (patch.timeZone !== void 0)
16585
+ parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
16586
+ if (patch.osdFormat !== void 0)
16587
+ parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
16588
+ parts.push(`<year>${mt.year}</year>`);
16589
+ parts.push(`<month>${mt.month}</month>`);
16590
+ parts.push(`<day>${mt.day}</day>`);
16591
+ parts.push(`<hour>${mt.hour}</hour>`);
16592
+ parts.push(`<minute>${mt.minute}</minute>`);
16593
+ parts.push(`<second>${mt.second}</second>`);
16594
+ if (patch.timeFormat !== void 0)
16595
+ parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
16596
+ if (patch.language !== void 0)
16597
+ parts.push(`<language>${xmlEscape(patch.language)}</language>`);
16598
+ if (patch.deviceName !== void 0)
16599
+ parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
16600
+ if (patch.loginLock !== void 0)
16601
+ parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
16602
+ if (patch.lockTime !== void 0)
16603
+ parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
16604
+ if (patch.allowedTimes !== void 0)
16605
+ parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
16606
+ } else {
16607
+ if (patch.timeZone !== void 0)
16608
+ parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
16609
+ if (patch.osdFormat !== void 0)
16610
+ parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
16611
+ if (patch.timeFormat !== void 0)
16612
+ parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
16613
+ if (patch.language !== void 0)
16614
+ parts.push(`<language>${xmlEscape(patch.language)}</language>`);
16615
+ if (patch.loginLock !== void 0)
16616
+ parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
16617
+ if (patch.lockTime !== void 0)
16618
+ parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
16619
+ if (patch.allowedTimes !== void 0)
16620
+ parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
16621
+ parts.push("<year>0</year>");
16622
+ }
16623
+ return ensureXmlHeader(
16624
+ `<body><SystemGeneral version="1.1">${parts.join("")}</SystemGeneral></body>`
16625
+ );
16626
+ }
16627
+
16137
16628
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
16138
16629
  var import_jimp = require("jimp");
16139
16630
  init_ReolinkCgiApi();
@@ -16410,6 +16901,32 @@ var parseAbilityInfoXml = (xml) => {
16410
16901
  return abilities;
16411
16902
  };
16412
16903
 
16904
+ // src/reolink/baichuan/utils/versionInfo.ts
16905
+ init_xml();
16906
+ function parseVersionInfo(xml) {
16907
+ const out = {};
16908
+ const set = (key) => {
16909
+ const v = getXmlText(xml, key);
16910
+ if (v !== void 0) out[key] = v;
16911
+ };
16912
+ set("name");
16913
+ set("type");
16914
+ set("serialNumber");
16915
+ set("buildDay");
16916
+ set("hardwareVersion");
16917
+ set("cfgVersion");
16918
+ set("firmwareVersion");
16919
+ set("detail");
16920
+ set("IEClient");
16921
+ set("cc3200Version");
16922
+ set("spVersion");
16923
+ set("pakSuffix");
16924
+ set("itemNo");
16925
+ set("aiVersion");
16926
+ set("helpVersion");
16927
+ return out;
16928
+ }
16929
+
16413
16930
  // src/reolink/baichuan/utils/aiState.ts
16414
16931
  init_constants();
16415
16932
  init_xml();
@@ -16716,6 +17233,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
16716
17233
  return { result: resultObj, storedChannels: Object.keys(resultObj) };
16717
17234
  };
16718
17235
 
17236
+ // src/reolink/baichuan/utils/detection.ts
17237
+ var lz4 = __toESM(require("lz4js"), 1);
17238
+
17239
+ // src/reolink/baichuan/utils/aiClassMap.ts
17240
+ function type1ToLabel(type1) {
17241
+ switch (type1) {
17242
+ case 1:
17243
+ return "people";
17244
+ case 2:
17245
+ return "vehicle";
17246
+ case 3:
17247
+ return "animal";
17248
+ case 11259375:
17249
+ return "face";
17250
+ default:
17251
+ return "unknown";
17252
+ }
17253
+ }
17254
+
17255
+ // src/reolink/baichuan/utils/detection.ts
17256
+ var MARKER_LENGTH = 8;
17257
+ var IFRAME_PREFIX_LENGTH = 8;
17258
+ var COUNTER_OFFSET = 8;
17259
+ var BASELINE_SIZE = 128;
17260
+ var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
17261
+ var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
17262
+ var CONFIDENCE_DIVISOR = 100;
17263
+ var DEFAULT_AI_FRAME_WIDTH = 896;
17264
+ var DEFAULT_AI_FRAME_HEIGHT = 480;
17265
+ var LZ4_DECOMPRESS_MAX = 256 * 1024;
17266
+ function walkBoxes(buf, start, end, type1, type2, out) {
17267
+ let pos = start;
17268
+ while (pos + 3 <= end) {
17269
+ const t = buf[pos];
17270
+ if (t === 0) return;
17271
+ const length = buf[pos + 1] | buf[pos + 2] << 8;
17272
+ const recordEnd = pos + 3 + length;
17273
+ if (recordEnd > end) return;
17274
+ const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
17275
+ const isBoxType2 = t === 2 && length === 10;
17276
+ if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
17277
+ const x1 = buf.readUInt16LE(pos + 3);
17278
+ const y1 = buf.readUInt16LE(pos + 5);
17279
+ const x2 = buf.readUInt16LE(pos + 7);
17280
+ const y2 = buf.readUInt16LE(pos + 9);
17281
+ const conf = buf.readUInt16LE(pos + 11);
17282
+ if (x2 > x1 && y2 > y1) {
17283
+ out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
17284
+ }
17285
+ pos = recordEnd;
17286
+ continue;
17287
+ }
17288
+ 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]) {
17289
+ try {
17290
+ const decompressed = lz4.decompress(
17291
+ buf.subarray(pos + 3, recordEnd),
17292
+ LZ4_DECOMPRESS_MAX
17293
+ );
17294
+ const decBuf = Buffer.from(decompressed);
17295
+ walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
17296
+ } catch {
17297
+ }
17298
+ pos = recordEnd;
17299
+ continue;
17300
+ }
17301
+ if (length > 0) {
17302
+ let nextT1 = type1;
17303
+ let nextT2 = type2;
17304
+ if (type1 === 0) nextT1 = t;
17305
+ else if (type2 === 0) nextT2 = t;
17306
+ walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
17307
+ }
17308
+ pos = recordEnd;
17309
+ }
17310
+ }
17311
+ function decodeDetectionHeader(raw, frameType) {
17312
+ const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
17313
+ const blockLength = raw.length - markerOffset;
17314
+ const empty = {
17315
+ state: "invalid-marker",
17316
+ markerOffset,
17317
+ blockLength,
17318
+ boxes: []
17319
+ };
17320
+ if (blockLength < MARKER_LENGTH) return empty;
17321
+ if (!hasStandardMarker(raw, markerOffset)) return empty;
17322
+ if (blockLength < COUNTER_OFFSET + 4) return empty;
17323
+ const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
17324
+ const rawBoxes = [];
17325
+ walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
17326
+ let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
17327
+ let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
17328
+ let frameSizeFound = false;
17329
+ const searchStart = markerOffset + MARKER_LENGTH;
17330
+ for (let i = searchStart; i + 7 <= raw.length; i++) {
17331
+ if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
17332
+ const w = raw.readUInt16LE(i + 3);
17333
+ const h = raw.readUInt16LE(i + 5);
17334
+ if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
17335
+ aiFrameWidth = w;
17336
+ aiFrameHeight = h;
17337
+ frameSizeFound = true;
17338
+ break;
17339
+ }
17340
+ }
17341
+ }
17342
+ const specificity = {
17343
+ face: 4,
17344
+ animal: 3,
17345
+ people: 2,
17346
+ vehicle: 1,
17347
+ unknown: 0
17348
+ };
17349
+ const dedup = /* @__PURE__ */ new Map();
17350
+ for (const rb of rawBoxes) {
17351
+ if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
17352
+ const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
17353
+ const prev = dedup.get(key);
17354
+ if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
17355
+ dedup.set(key, rb);
17356
+ }
17357
+ }
17358
+ const boxes = [];
17359
+ for (const rb of dedup.values()) {
17360
+ boxes.push({
17361
+ x: rb.x1 / aiFrameWidth,
17362
+ y: rb.y1 / aiFrameHeight,
17363
+ width: (rb.x2 - rb.x1) / aiFrameWidth,
17364
+ height: (rb.y2 - rb.y1) / aiFrameHeight,
17365
+ ...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
17366
+ ...rb.label !== "unknown" ? { label: rb.label } : {}
17367
+ });
17368
+ }
17369
+ let state;
17370
+ if (boxes.length > 0) state = "overlay-decoded";
17371
+ else if (blockLength === BASELINE_SIZE) state = "no-overlay";
17372
+ else state = "overlay-undecoded";
17373
+ return {
17374
+ state,
17375
+ markerOffset,
17376
+ blockLength,
17377
+ counter,
17378
+ ...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
17379
+ boxes
17380
+ };
17381
+ }
17382
+ function hasStandardMarker(raw, offset) {
17383
+ if (raw.length < offset + MARKER_LENGTH) return false;
17384
+ 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;
17385
+ }
17386
+
17387
+ // src/reolink/baichuan/utils/encOptions.ts
17388
+ function buildEncOptions(list, channel) {
17389
+ const result = { channel };
17390
+ const main = aggregateByType(list, "mainStream");
17391
+ const sub = aggregateByType(list, "subStream");
17392
+ const third = aggregateByType(list, "thirdStream");
17393
+ if (main) result.mainStream = main;
17394
+ if (sub) result.subStream = sub;
17395
+ if (third) result.thirdStream = third;
17396
+ return result;
17397
+ }
17398
+ function aggregateByType(list, type) {
17399
+ const seen = /* @__PURE__ */ new Map();
17400
+ for (const stream of list.streams) {
17401
+ for (const eb of stream.encodeTables) {
17402
+ if (eb.type !== type) continue;
17403
+ const mapped = mapEncodeTable(eb);
17404
+ if (!mapped) continue;
17405
+ const key = `${mapped.width}x${mapped.height}`;
17406
+ if (!seen.has(key)) seen.set(key, mapped);
17407
+ }
17408
+ }
17409
+ if (seen.size === 0) return void 0;
17410
+ return {
17411
+ type,
17412
+ resolutions: [...seen.values()],
17413
+ encoderTypes: ["vbr", "cbr"],
17414
+ encoderProfiles: ["high", "main", "baseline"]
17415
+ };
17416
+ }
17417
+ function mapEncodeTable(eb) {
17418
+ if (eb.width == null || eb.height == null) return void 0;
17419
+ const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
17420
+ (t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
17421
+ ).filter((t) => t !== void 0);
17422
+ return {
17423
+ width: eb.width,
17424
+ height: eb.height,
17425
+ videoEncTypes,
17426
+ ...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
17427
+ ...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
17428
+ ...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
17429
+ framerateOptions: eb.framerateTable ?? [],
17430
+ bitrateOptions: eb.bitrateTable ?? []
17431
+ };
17432
+ }
17433
+
16719
17434
  // src/reolink/baichuan/utils/events.ts
16720
17435
  var mapToSimpleEvent = (event) => {
16721
17436
  const timestamp = event.timestamp ?? Date.now();
@@ -18839,6 +19554,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18839
19554
  sessionGuardIntervalTimer;
18840
19555
  simpleEventListeners = /* @__PURE__ */ new Set();
18841
19556
  simpleEventSubscribed = false;
19557
+ // Detection events are sourced from BcMedia additionalHeader on active video
19558
+ // streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
19559
+ // data flows whenever a stream is open. Active streams register themselves via
19560
+ // _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
19561
+ detectionEventListeners = /* @__PURE__ */ new Set();
19562
+ detectionEventStreamHooks = /* @__PURE__ */ new Map();
19563
+ // Auto-managed substream for `onObjectDetections` listeners. Reference-counted
19564
+ // by the listener set: the substream is opened on the first listener and torn
19565
+ // down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
19566
+ // lifecycle so a caller never has to manage a video stream just to read AI
19567
+ // detections.
19568
+ objectDetectionListeners = /* @__PURE__ */ new Set();
19569
+ objectDetectionStream;
19570
+ objectDetectionStreamStartInFlight;
19571
+ objectDetectionInternalListener;
18842
19572
  simpleEventSubscribeInFlight;
18843
19573
  simpleEventUnsubscribeInFlight;
18844
19574
  simpleEventResubscribeTimer;
@@ -20274,6 +21004,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
20274
21004
  }
20275
21005
  }
20276
21006
  }
21007
+ /**
21008
+ * Subscribe to per-frame detection events sourced from the BcMedia
21009
+ * `additionalHeader` block on active video streams.
21010
+ *
21011
+ * Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
21012
+ * one event fires for every I-frame / P-frame that carries an overlay block.
21013
+ * Coordinates are reported in normalized [0, 1] fractions of the source
21014
+ * frame, so the same box renders correctly on mainStream, subStream, and
21015
+ * externStream.
21016
+ *
21017
+ * Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
21018
+ * only flow while a video stream is open. The library hooks every
21019
+ * `BaichuanVideoStream` created via this API for the listener's lifetime.
21020
+ */
21021
+ onDetection(callback) {
21022
+ this.detectionEventListeners.add(callback);
21023
+ }
21024
+ /**
21025
+ * Remove a single detection callback, or all of them if `callback` is omitted.
21026
+ */
21027
+ offDetection(callback) {
21028
+ if (callback) {
21029
+ this.detectionEventListeners.delete(callback);
21030
+ } else {
21031
+ this.detectionEventListeners.clear();
21032
+ }
21033
+ }
21034
+ /**
21035
+ * Subscribe to AI object detections (people / vehicle / animal / face boxes
21036
+ * with class label and confidence) without managing a video stream yourself.
21037
+ *
21038
+ * Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
21039
+ * substream behind the scenes on the first listener, forwards every box-bearing
21040
+ * `additionalHeader` to your callback, and tears the stream down when the last
21041
+ * listener unsubscribes. The substream is the lightest profile (typically
21042
+ * 640×360) so the additional bandwidth/CPU overhead is minimal.
21043
+ *
21044
+ * Each event carries normalized `[0, 1]` box coordinates, a class label, and
21045
+ * a confidence score — render-ready without further conversion.
21046
+ */
21047
+ async onObjectDetections(callback) {
21048
+ this.objectDetectionListeners.add(callback);
21049
+ this.logger.debug?.(
21050
+ `[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
21051
+ );
21052
+ await this.ensureObjectDetectionStream();
21053
+ }
21054
+ /**
21055
+ * Remove one detection callback, or all of them if `callback` is omitted.
21056
+ * When the last listener is removed the auto-managed substream is closed.
21057
+ */
21058
+ async offObjectDetections(callback) {
21059
+ if (callback) {
21060
+ this.objectDetectionListeners.delete(callback);
21061
+ } else {
21062
+ this.objectDetectionListeners.clear();
21063
+ }
21064
+ if (this.objectDetectionListeners.size === 0) {
21065
+ await this.tearDownObjectDetectionStream();
21066
+ }
21067
+ }
21068
+ async ensureObjectDetectionStream() {
21069
+ if (this.objectDetectionStream) return;
21070
+ if (this.objectDetectionStreamStartInFlight) {
21071
+ await this.objectDetectionStreamStartInFlight;
21072
+ return;
21073
+ }
21074
+ this.objectDetectionStreamStartInFlight = (async () => {
21075
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => (init_BaichuanVideoStream(), BaichuanVideoStream_exports));
21076
+ const sessionKey = `live:object-detections:ch0:sub`;
21077
+ const dedicated = await this.createDedicatedSession(sessionKey);
21078
+ const stream = new BaichuanVideoStream2({
21079
+ client: dedicated.client,
21080
+ api: this,
21081
+ channel: 0,
21082
+ profile: "sub",
21083
+ logger: this.logger
21084
+ });
21085
+ this.objectDetectionInternalListener = (event) => {
21086
+ for (const cb of this.objectDetectionListeners) {
21087
+ try {
21088
+ void Promise.resolve(cb(event)).catch((e) => {
21089
+ (this.logger.warn ?? this.logger.error).call(
21090
+ this.logger,
21091
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
21092
+ formatErrorForLog(e)
21093
+ );
21094
+ });
21095
+ } catch (e) {
21096
+ (this.logger.warn ?? this.logger.error).call(
21097
+ this.logger,
21098
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
21099
+ formatErrorForLog(e)
21100
+ );
21101
+ }
21102
+ }
21103
+ };
21104
+ this.detectionEventListeners.add(this.objectDetectionInternalListener);
21105
+ try {
21106
+ await stream.start();
21107
+ } catch (e) {
21108
+ if (this.objectDetectionInternalListener) {
21109
+ this.detectionEventListeners.delete(
21110
+ this.objectDetectionInternalListener
21111
+ );
21112
+ this.objectDetectionInternalListener = void 0;
21113
+ }
21114
+ await dedicated.release().catch(() => {
21115
+ });
21116
+ throw e;
21117
+ }
21118
+ this.objectDetectionStream = {
21119
+ stop: () => stream.stop(),
21120
+ release: () => dedicated.release()
21121
+ };
21122
+ this.logger.debug?.(
21123
+ `[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
21124
+ );
21125
+ })();
21126
+ try {
21127
+ await this.objectDetectionStreamStartInFlight;
21128
+ } finally {
21129
+ this.objectDetectionStreamStartInFlight = void 0;
21130
+ }
21131
+ }
21132
+ async tearDownObjectDetectionStream() {
21133
+ const handle = this.objectDetectionStream;
21134
+ this.objectDetectionStream = void 0;
21135
+ if (this.objectDetectionInternalListener) {
21136
+ this.detectionEventListeners.delete(this.objectDetectionInternalListener);
21137
+ this.objectDetectionInternalListener = void 0;
21138
+ }
21139
+ if (!handle) return;
21140
+ try {
21141
+ await handle.stop();
21142
+ } catch (e) {
21143
+ this.logger.debug?.(
21144
+ `[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
21145
+ );
21146
+ }
21147
+ try {
21148
+ await handle.release();
21149
+ } catch (e) {
21150
+ this.logger.debug?.(
21151
+ `[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
21152
+ );
21153
+ }
21154
+ this.logger.debug?.(
21155
+ `[ReolinkBaichuanApi] onObjectDetections: substream torn down`
21156
+ );
21157
+ }
21158
+ /**
21159
+ * Internal: invoked by BaichuanVideoStream when it starts so the API can hook
21160
+ * its `additionalHeader` event. Returns a teardown function the stream calls
21161
+ * on stop. Not intended for direct use by consumers.
21162
+ */
21163
+ _registerVideoStreamForDetection(stream, context) {
21164
+ const listener = (info) => {
21165
+ if (this.detectionEventListeners.size === 0) return;
21166
+ const decoded = decodeDetectionHeader(info.raw, info.frameType);
21167
+ const event = {
21168
+ channel: context.channel,
21169
+ microseconds: info.microseconds,
21170
+ profile: context.profile,
21171
+ boxes: decoded.boxes,
21172
+ ...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
21173
+ ...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
21174
+ decodeState: decoded.state,
21175
+ rawHeader: info.raw
21176
+ };
21177
+ this.dispatchDetectionEvent(event);
21178
+ };
21179
+ stream.on("additionalHeader", listener);
21180
+ const teardown = () => {
21181
+ stream.off("additionalHeader", listener);
21182
+ this.detectionEventStreamHooks.delete(stream);
21183
+ };
21184
+ this.detectionEventStreamHooks.set(stream, teardown);
21185
+ return teardown;
21186
+ }
21187
+ dispatchDetectionEvent(evt) {
21188
+ for (const cb of this.detectionEventListeners) {
21189
+ try {
21190
+ void Promise.resolve(cb(evt)).catch((e) => {
21191
+ (this.logger.warn ?? this.logger.error).call(
21192
+ this.logger,
21193
+ "[ReolinkBaichuanApi] onDetection handler error",
21194
+ formatErrorForLog(e)
21195
+ );
21196
+ });
21197
+ } catch (e) {
21198
+ (this.logger.warn ?? this.logger.error).call(
21199
+ this.logger,
21200
+ "[ReolinkBaichuanApi] onDetection handler error",
21201
+ formatErrorForLog(e)
21202
+ );
21203
+ }
21204
+ }
21205
+ }
20277
21206
  startSimpleEventResubscribeTimer() {
20278
21207
  if (this.simpleEventResubscribeTimer) return;
20279
21208
  if (this.simpleEventListeners.size === 0) return;
@@ -20656,6 +21585,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
20656
21585
  this.stopUdpSleepInference();
20657
21586
  this.stopSimpleEventWatchdog();
20658
21587
  this.stopSimpleEventResubscribeTimer();
21588
+ this.objectDetectionListeners.clear();
21589
+ await this.tearDownObjectDetectionStream().catch(() => {
21590
+ });
20659
21591
  await this.cleanup();
20660
21592
  await this.stopAllActiveStreams();
20661
21593
  await this.cleanupSocketPool();
@@ -21003,6 +21935,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
21003
21935
  const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
21004
21936
  await this.sendXml({ cmdId: 36, payloadXml: xml });
21005
21937
  }
21938
+ /**
21939
+ * Full port-config setter (cmd_id 36). Patches one or more of the six
21940
+ * service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
21941
+ * RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
21942
+ * `enable` (boolean); fields the caller doesn't pass are left alone.
21943
+ *
21944
+ * Sends one block per port that has any field set, then issues a
21945
+ * single cmd_36 with the merged body. The camera accepts multiple
21946
+ * `<XxxPort>` siblings in the same payload.
21947
+ *
21948
+ * Wire format observed on E1 Zoom:
21949
+ *
21950
+ * <body>
21951
+ * <RtspPort version="1.1">
21952
+ * <rtspPort>554</rtspPort>
21953
+ * <enable>1</enable>
21954
+ * </RtspPort>
21955
+ * <HttpsPort version="1.1">
21956
+ * <enable>0</enable>
21957
+ * </HttpsPort>
21958
+ * ...
21959
+ * </body>
21960
+ */
21961
+ async setPortConfig(patch) {
21962
+ const blocks = [];
21963
+ const append = (tag, portField, cfg) => {
21964
+ if (!cfg) return;
21965
+ if (cfg.port === void 0 && cfg.enable === void 0) return;
21966
+ const inner = [];
21967
+ if (cfg.port !== void 0) {
21968
+ inner.push(`<${portField}>${cfg.port}</${portField}>`);
21969
+ }
21970
+ if (cfg.enable !== void 0) {
21971
+ inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
21972
+ }
21973
+ blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
21974
+ };
21975
+ append("ServerPort", "serverPort", patch.server);
21976
+ append("HttpPort", "httpPort", patch.http);
21977
+ append("HttpsPort", "httpsPort", patch.https);
21978
+ append("RtspPort", "rtspPort", patch.rtsp);
21979
+ append("RtmpPort", "rtmpPort", patch.rtmp);
21980
+ append("OnvifPort", "onvifPort", patch.onvif);
21981
+ if (blocks.length === 0) return;
21982
+ const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
21983
+ await this.sendXml({ cmdId: 36, payloadXml: xml });
21984
+ }
21006
21985
  /** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
21007
21986
  async getInfo(channel, options) {
21008
21987
  const req = { cmdId: channel == null ? 80 : 318 };
@@ -23663,7 +24642,7 @@ ${stderr}`)
23663
24642
  * Convert a raw video keyframe to JPEG using ffmpeg.
23664
24643
  */
23665
24644
  async convertFrameToJpeg(params) {
23666
- const { spawn: spawn12 } = await import("child_process");
24645
+ const { spawn: spawn13 } = await import("child_process");
23667
24646
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
23668
24647
  const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
23669
24648
  return new Promise((resolve, reject) => {
@@ -23685,7 +24664,7 @@ ${stderr}`)
23685
24664
  "2",
23686
24665
  "pipe:1"
23687
24666
  ];
23688
- const proc = spawn12(ffmpeg, args, {
24667
+ const proc = spawn13(ffmpeg, args, {
23689
24668
  stdio: ["pipe", "pipe", "pipe"]
23690
24669
  });
23691
24670
  const chunks = [];
@@ -23828,7 +24807,7 @@ ${stderr}`)
23828
24807
  * Internal helper to mux video+audio into MP4 using ffmpeg.
23829
24808
  */
23830
24809
  async muxToMp4(params) {
23831
- const { spawn: spawn12 } = await import("child_process");
24810
+ const { spawn: spawn13 } = await import("child_process");
23832
24811
  const { randomUUID: randomUUID3 } = await import("crypto");
23833
24812
  const fs6 = await import("fs/promises");
23834
24813
  const os2 = await import("os");
@@ -23880,7 +24859,7 @@ ${stderr}`)
23880
24859
  outputPath
23881
24860
  );
23882
24861
  await new Promise((resolve, reject) => {
23883
- const p = spawn12(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
24862
+ const p = spawn13(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
23884
24863
  let stderr = "";
23885
24864
  p.stderr.on("data", (d) => {
23886
24865
  stderr += d.toString();
@@ -24687,6 +25666,27 @@ ${stderr}`)
24687
25666
  );
24688
25667
  }
24689
25668
  }
25669
+ async gotoPtzPreset(arg1, arg2) {
25670
+ const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
25671
+ const presetId = arg2 === void 0 ? arg1 : arg2;
25672
+ const channelId = ch;
25673
+ const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
25674
+ const extensionXml = buildChannelExtensionXml(channelId);
25675
+ const frame = await this.client.sendFrame({
25676
+ cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
25677
+ channel: ch,
25678
+ channelIdOverride: channelId,
25679
+ extensionXml,
25680
+ payloadXml,
25681
+ messageClass: BC_CLASS_MODERN_24,
25682
+ streamType: 0
25683
+ });
25684
+ if (frame.header.responseCode !== 200) {
25685
+ throw new Error(
25686
+ `PTZ goto preset rejected (response_code ${frame.header.responseCode})`
25687
+ );
25688
+ }
25689
+ }
24690
25690
  async deletePtzPreset(arg1, arg2) {
24691
25691
  const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
24692
25692
  const presetId = arg2 === void 0 ? arg1 : arg2;
@@ -25338,22 +26338,62 @@ ${stderr}`)
25338
26338
  const channel = typeof arg1 === "number" ? arg1 : arg3;
25339
26339
  const enabled = typeof arg1 === "number" ? arg2 : arg1;
25340
26340
  const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
25341
- const ch = this.normalizeChannel(channel);
26341
+ return await this.setMotionAlarmFull({
26342
+ ...channel !== void 0 ? { channel } : {},
26343
+ enabled,
26344
+ ...sensitivity !== void 0 ? { sensitivity } : {}
26345
+ });
26346
+ }
26347
+ /**
26348
+ * Set motion alarm with full control, including the detection-zone grid.
26349
+ *
26350
+ * Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
26351
+ *
26352
+ * <MD version="1.1">
26353
+ * <channelId>0</channelId>
26354
+ * <enable>1</enable>
26355
+ * <usepir>0</usepir>
26356
+ * <width>60</width> <height>33</height>
26357
+ * <scope>
26358
+ * <columns>96</columns> <rows>64</rows>
26359
+ * <valueTable>{base64 6144-bit bitmap}</valueTable>
26360
+ * </scope>
26361
+ * ... other camera-specific fields ...
26362
+ * </MD>
26363
+ *
26364
+ * We do a read-modify-write of the GET response so any camera-specific
26365
+ * extension fields are preserved untouched. Pass `valueTable` to update
26366
+ * the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
26367
+ *
26368
+ * @param channel - 0-based channel
26369
+ * @param enabled - toggle motion detection on/off (optional)
26370
+ * @param sensitivity - 0-50, higher = more sensitive (optional)
26371
+ * @param valueTable - base64-encoded grid bitmap; size must match
26372
+ * `<scope><columns>×<rows></scope>` from the GET (optional)
26373
+ */
26374
+ async setMotionAlarmFull(opts) {
26375
+ const ch = this.normalizeChannel(opts.channel);
25342
26376
  const currentXml = await this.sendXml({
25343
26377
  cmdId: BC_CMD_ID_GET_MOTION_ALARM,
25344
26378
  channel: ch
25345
26379
  });
25346
26380
  let modifiedXml = currentXml;
25347
- if (enabled !== void 0) {
26381
+ if (opts.enabled !== void 0) {
25348
26382
  modifiedXml = modifiedXml.replace(
25349
26383
  /<enable>[^<]*<\/enable>/,
25350
- `<enable>${enabled ? "1" : "0"}</enable>`
26384
+ `<enable>${opts.enabled ? "1" : "0"}</enable>`
25351
26385
  );
25352
26386
  }
25353
- if (sensitivity !== void 0) {
26387
+ if (opts.sensitivity !== void 0) {
25354
26388
  modifiedXml = modifiedXml.replace(
25355
26389
  /<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
25356
- `<sensitivityDefault>${sensitivity}</sensitivityDefault>`
26390
+ `<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
26391
+ );
26392
+ }
26393
+ if (opts.valueTable !== void 0) {
26394
+ modifiedXml = modifiedXml.replace(
26395
+ /<valueTable>[^<]*<\/valueTable>/,
26396
+ `<valueTable>${opts.valueTable}</valueTable>`
25357
26397
  );
25358
26398
  }
25359
26399
  await this.sendXml({
@@ -26697,12 +27737,24 @@ ${xml}`
26697
27737
  }
26698
27738
  /**
26699
27739
  * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
26700
- * unspecified fields. Mirrors reolink_aio's `SetEnc`.
27740
+ * unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
27741
+ * `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
27742
+ * fields observed in the official mobile app (see `pcap/resolution.pcapng`).
27743
+ *
27744
+ * Field meaning per stream:
27745
+ * - `audio` — 0/1 toggle
27746
+ * - `width`/`height` — resolution in pixels. Must be one of the
27747
+ * resolutions returned by {@link getStreamInfoList}.
27748
+ * - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
27749
+ * - `frameRate` — fps. Must match the table from `getStreamInfoList`.
27750
+ * - `videoEncType` — `"h264"` or `"h265"`
27751
+ * - `encoderType` — `"vbr"` or `"cbr"`
27752
+ * - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
27753
+ * - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
26701
27754
  *
26702
27755
  * @param channel - Channel number (0-based)
26703
- * @param patch - Fields to update on `mainStream` and/or `subStream`,
26704
- * plus a top-level `audio` toggle (0/1). Pass only what you want
26705
- * to change.
27756
+ * @param patch - Fields to update. Pass only the fields you want to change;
27757
+ * everything else is preserved from the device's current configuration.
26706
27758
  */
26707
27759
  async setEnc(channel, patch, options) {
26708
27760
  const ch = this.normalizeChannel(channel);
@@ -26719,6 +27771,7 @@ ${xml}`
26719
27771
  }
26720
27772
  xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
26721
27773
  xml = applyStreamPatch(xml, "subStream", patch.subStream);
27774
+ xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
26722
27775
  await this.sendXml({
26723
27776
  cmdId: BC_CMD_ID_SET_ENC,
26724
27777
  channel: ch,
@@ -27326,6 +28379,71 @@ ${xml}`
27326
28379
  `PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
27327
28380
  );
27328
28381
  }
28382
+ /**
28383
+ * Update the OSD timestamp + channel-name overlay via cmd_id=45
28384
+ * (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
28385
+ * `<OsdChannelName>` block returned by `getOsdDatetime` — we
28386
+ * read-modify-write so any extension fields the camera sent are
28387
+ * preserved.
28388
+ *
28389
+ * Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
28390
+ * not preset strings). Set `enable=0` to hide the overlay; the camera
28391
+ * keeps the stored position so re-enabling later restores it.
28392
+ */
28393
+ async setOsdDatetime(channel, patch, options) {
28394
+ const ch = this.normalizeChannel(channel);
28395
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
28396
+ let xml = await this.sendPcapDerivedSettingsGetXml({
28397
+ cmdId: BC_CMD_ID_GET_OSD_DATETIME,
28398
+ channel: ch,
28399
+ ...timeoutOpts
28400
+ });
28401
+ const patchBlock = (block, fields) => {
28402
+ const start = xml.indexOf(`<${block}`);
28403
+ if (start < 0) return;
28404
+ const end = xml.indexOf(`</${block}>`, start);
28405
+ if (end < 0) return;
28406
+ let body = xml.slice(start, end);
28407
+ for (const [tag, value] of Object.entries(fields)) {
28408
+ if (value === void 0) continue;
28409
+ const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
28410
+ const escaped = raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
28411
+ if (body.includes(`<${tag}>`)) {
28412
+ body = body.replace(
28413
+ new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
28414
+ `<${tag}>${escaped}</${tag}>`
28415
+ );
28416
+ } else {
28417
+ body += `<${tag}>${escaped}</${tag}>`;
28418
+ }
28419
+ }
28420
+ xml = xml.slice(0, start) + body + xml.slice(end);
28421
+ };
28422
+ if (patch.datetime) {
28423
+ patchBlock("OsdDatetime", {
28424
+ enable: patch.datetime.enable,
28425
+ topLeftX: patch.datetime.topLeftX,
28426
+ topLeftY: patch.datetime.topLeftY,
28427
+ language: patch.datetime.language
28428
+ });
28429
+ }
28430
+ if (patch.channelName) {
28431
+ patchBlock("OsdChannelName", {
28432
+ name: patch.channelName.name,
28433
+ enable: patch.channelName.enable,
28434
+ topLeftX: patch.channelName.topLeftX,
28435
+ topLeftY: patch.channelName.topLeftY,
28436
+ enWatermark: patch.channelName.enWatermark,
28437
+ enBgcolor: patch.channelName.enBgcolor
28438
+ });
28439
+ }
28440
+ await this.sendXml({
28441
+ cmdId: BC_CMD_ID_SET_OSD_DATETIME,
28442
+ channel: ch,
28443
+ payloadXml: ensureXmlHeader(xml),
28444
+ ...timeoutOpts
28445
+ });
28446
+ }
27329
28447
  async getOsdDatetime(channel, options) {
27330
28448
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
27331
28449
  cmdId: BC_CMD_ID_GET_OSD_DATETIME,
@@ -27518,6 +28636,41 @@ ${xml}`
27518
28636
  });
27519
28637
  return { streams };
27520
28638
  }
28639
+ /**
28640
+ * Return the set of values `setEnc` will accept on each stream of `channel`.
28641
+ * Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
28642
+ * per-stream resolutions with their allowed codecs/framerates/bitrates plus
28643
+ * the enumerated encoder modes/profiles Reolink exposes.
28644
+ *
28645
+ * Useful for populating selectors and validating user input before calling
28646
+ * `setEnc` — picking an unsupported combination causes the camera to reject
28647
+ * the SET_ENC command (responseCode != 200).
28648
+ */
28649
+ async getEncOptions(channel, options) {
28650
+ const list = await this.getStreamInfoList(channel, options);
28651
+ return buildEncOptions(list, channel);
28652
+ }
28653
+ /**
28654
+ * Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
28655
+ * friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
28656
+ * version, hardware revision, build day, AI model bundle version, etc.
28657
+ *
28658
+ * This is the same info the Reolink mobile app shows in "About this
28659
+ * device" — distinct from `getSystemGeneral` (cmd_104) which carries
28660
+ * time/locale.
28661
+ *
28662
+ * No channel parameter: this command is device-global on NVRs/Hubs and
28663
+ * camera-global on standalone cameras. Pass an explicit channel via the
28664
+ * underlying `sendXml` only if a specific firmware demands it (none we've
28665
+ * tested do).
28666
+ */
28667
+ async getVersionInfo(options) {
28668
+ const xml = await this.sendXml({
28669
+ cmdId: BC_CMD_ID_GET_VERSION_INFO,
28670
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28671
+ });
28672
+ return parseVersionInfo(xml);
28673
+ }
27521
28674
  async getLedState(channel, options) {
27522
28675
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
27523
28676
  cmdId: BC_CMD_ID_GET_LED_STATE,
@@ -27600,7 +28753,279 @@ ${xml}`
27600
28753
  ...channel != null ? { channel } : {},
27601
28754
  ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
27602
28755
  });
27603
- return parseXmlFragmentToJson(xml);
28756
+ return parseEmailTaskFromXml(xml);
28757
+ }
28758
+ /**
28759
+ * SetEmailTask via Baichuan (cmdId=216). Updates the email alarm schedule
28760
+ * (per-event-type 7×24 valueTable + master enable).
28761
+ *
28762
+ * Reolink expects the FULL `typeScheduleList` — pass the array from a prior
28763
+ * GET and only flip the entries you care about. Slots you don't track must
28764
+ * be sent back unchanged to avoid the camera dropping them.
28765
+ */
28766
+ async setEmailTask(channel, task, options) {
28767
+ const ch = this.normalizeChannel(channel);
28768
+ const payloadXml = buildSetEmailTaskXml({ ...task, channelId: ch });
28769
+ await this.sendXml({
28770
+ cmdId: BC_CMD_ID_SET_EMAIL_TASK,
28771
+ channel: ch,
28772
+ payloadXml,
28773
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28774
+ });
28775
+ }
28776
+ /**
28777
+ * Convenience wrapper that patches the schedule of one or more trigger
28778
+ * types on the camera's EmailTask without touching the others.
28779
+ *
28780
+ * Pass a high-level schedule spec (`always` / `never` / explicit windows)
28781
+ * and the trigger types it should apply to. The method:
28782
+ *
28783
+ * 1. Reads the current EmailTask via GET (so we keep every existing slot).
28784
+ * 2. Builds the new `valueTable` once from `schedule`.
28785
+ * 3. Replaces the `valueTable` of every matching `type` in the list.
28786
+ * 4. Appends entries for any requested type not already present.
28787
+ * 5. Writes the merged list back via SET.
28788
+ *
28789
+ * Returns the list of types that were actually touched.
28790
+ */
28791
+ async patchEmailSchedule(channel, spec, options) {
28792
+ const current = await this.getEmailTask(channel, options);
28793
+ const newValueTable = buildEmailScheduleValueTable(spec.schedule);
28794
+ const targetSet = new Set(spec.types);
28795
+ const touched = [];
28796
+ const updatedList = current.typeScheduleList.map((item) => {
28797
+ if (targetSet.has(item.type)) {
28798
+ touched.push(item.type);
28799
+ return { ...item, valueTable: newValueTable };
28800
+ }
28801
+ return item;
28802
+ });
28803
+ for (const t of spec.types) {
28804
+ if (!current.typeScheduleList.some((item) => item.type === t)) {
28805
+ updatedList.push({ type: t, valueTable: newValueTable });
28806
+ touched.push(t);
28807
+ }
28808
+ }
28809
+ await this.setEmailTask(
28810
+ channel,
28811
+ {
28812
+ channelId: current.channelId,
28813
+ enable: spec.enable ?? current.enable,
28814
+ typeScheduleList: updatedList
28815
+ },
28816
+ options
28817
+ );
28818
+ return { touchedTypes: touched };
28819
+ }
28820
+ // ====================================================================
28821
+ // Email server (cmdId 42/43/141), NTP (38/39), DST (106/107),
28822
+ // SystemGeneral SET (105), AutoReboot (100/101).
28823
+ // Schemas derived from Reolink Client pcap (2026-05-16).
28824
+ // ====================================================================
28825
+ /**
28826
+ * Read the SMTP email configuration (cmdId=42). Returns the full `<Email>`
28827
+ * block including capability hints (`senderMaxLen`, `pwdMaxLen`,
28828
+ * `emailAttachAbility`).
28829
+ */
28830
+ async getEmail(options) {
28831
+ const xml = await this.sendXml({
28832
+ cmdId: BC_CMD_ID_GET_EMAIL,
28833
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28834
+ });
28835
+ return parseEmailConfigFromXml(xml);
28836
+ }
28837
+ /**
28838
+ * Patch the SMTP email configuration (cmdId=43). Reads the current config
28839
+ * first then merges the patch — Reolink rejects partial `<Email>` blocks.
28840
+ */
28841
+ async setEmail(patch, options) {
28842
+ const current = await this.getEmail(options);
28843
+ const payloadXml = buildSetEmailXml(current, patch);
28844
+ await this.sendXml({
28845
+ cmdId: BC_CMD_ID_SET_EMAIL,
28846
+ payloadXml,
28847
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28848
+ });
28849
+ }
28850
+ /**
28851
+ * Send a test email using either the current config or an override patch
28852
+ * (cmdId=141). Returns true when the camera reports 200 (test succeeded),
28853
+ * false when it reports 482 (test failed — server unreachable / bad creds).
28854
+ * Other non-200 codes propagate as exceptions via `sendXml`.
28855
+ */
28856
+ async testEmail(patch, options) {
28857
+ const current = await this.getEmail(options);
28858
+ const payloadXml = buildSetEmailXml(current, patch ?? {});
28859
+ const timeoutMs = options?.timeoutMs ?? 6e4;
28860
+ try {
28861
+ await this.sendXml({
28862
+ cmdId: BC_CMD_ID_TEST_EMAIL,
28863
+ payloadXml,
28864
+ timeoutMs
28865
+ });
28866
+ return true;
28867
+ } catch (err) {
28868
+ const msg = err instanceof Error ? err.message : String(err);
28869
+ if (msg.includes("response_code 482") || msg.includes("response_code=482")) {
28870
+ return false;
28871
+ }
28872
+ throw err;
28873
+ }
28874
+ }
28875
+ /**
28876
+ * Read the NTP server configuration (cmdId=38).
28877
+ */
28878
+ async getNtp(options) {
28879
+ const xml = await this.sendXml({
28880
+ cmdId: BC_CMD_ID_GET_NTP,
28881
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28882
+ });
28883
+ return parseNtpConfigFromXml(xml);
28884
+ }
28885
+ /**
28886
+ * Patch the NTP server configuration (cmdId=39). Reads the current state
28887
+ * first and merges the patch — Reolink rejects partial `<Ntp>` blocks.
28888
+ */
28889
+ async setNtp(patch, options) {
28890
+ const current = await this.getNtp(options);
28891
+ const payloadXml = buildSetNtpXml(current, patch);
28892
+ await this.sendXml({
28893
+ cmdId: BC_CMD_ID_SET_NTP,
28894
+ payloadXml,
28895
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28896
+ });
28897
+ }
28898
+ /**
28899
+ * Patch SystemGeneral (cmdId=105). Supports partial payloads: include only
28900
+ * the fields you want to change. By default the builder emits `<year>0</year>`
28901
+ * as the "do not set manual clock" marker; pass `manualTime` to actually
28902
+ * set the date/time. Setting only `deviceName` automatically uses the
28903
+ * Reolink Client's `deviceNameOnly=1` shape.
28904
+ */
28905
+ async setSystemGeneral(patch, options) {
28906
+ const payloadXml = buildSetSystemGeneralXml(patch);
28907
+ await this.sendXml({
28908
+ cmdId: BC_CMD_ID_SET_SYSTEM_GENERAL,
28909
+ payloadXml,
28910
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28911
+ });
28912
+ }
28913
+ /**
28914
+ * Read the Daylight Saving Time configuration (cmdId=106).
28915
+ */
28916
+ async getDst(options) {
28917
+ const xml = await this.sendXml({
28918
+ cmdId: BC_CMD_ID_GET_DST,
28919
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28920
+ });
28921
+ return parseDstConfigFromXml(xml);
28922
+ }
28923
+ /**
28924
+ * Patch the DST configuration (cmdId=107). Reads the current state first
28925
+ * and merges the patch.
28926
+ */
28927
+ async setDst(patch, options) {
28928
+ const current = await this.getDst(options);
28929
+ const payloadXml = buildSetDstXml(current, patch);
28930
+ await this.sendXml({
28931
+ cmdId: BC_CMD_ID_SET_DST,
28932
+ payloadXml,
28933
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28934
+ });
28935
+ }
28936
+ /**
28937
+ * Read the auto-reboot schedule (cmdId=101).
28938
+ */
28939
+ async getAutoReboot(options) {
28940
+ const xml = await this.sendXml({
28941
+ cmdId: BC_CMD_ID_GET_AUTO_REBOOT,
28942
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28943
+ });
28944
+ return parseAutoRebootFromXml(xml);
28945
+ }
28946
+ /**
28947
+ * Patch the auto-reboot schedule (cmdId=100).
28948
+ */
28949
+ async setAutoReboot(patch, options) {
28950
+ const current = await this.getAutoReboot(options);
28951
+ const payloadXml = buildSetAutoRebootXml(current, patch);
28952
+ await this.sendXml({
28953
+ cmdId: BC_CMD_ID_SET_AUTO_REBOOT,
28954
+ payloadXml,
28955
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28956
+ });
28957
+ }
28958
+ /**
28959
+ * High-level helper that configures the camera to deliver motion alerts via
28960
+ * SMTP to the local nodelink manager. Orchestrates `setEmail` + `setEmailTask`
28961
+ * in a single call so UI code can offer "auto-configure" without juggling
28962
+ * the underlying commands.
28963
+ *
28964
+ * Pass `runTest: true` to also send a test email (cmdId=141). Returns a
28965
+ * structured result describing each leg of the flow so the caller can show
28966
+ * granular feedback.
28967
+ *
28968
+ * @param params Auto-configuration parameters
28969
+ * @param channel Logical channel (default 0). Used for the EmailTask SET.
28970
+ */
28971
+ async setupEmailPushToManager(params, channel, options) {
28972
+ const port = params.managerPort ?? 2525;
28973
+ const domain = params.domain ?? "nodelink.local";
28974
+ const recipient = `${params.recipientLocalPart}@${domain}`;
28975
+ const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
28976
+ const attachmentType = params.attachmentType ?? "picture";
28977
+ const interval = params.interval ?? 30;
28978
+ const emailPatch = {
28979
+ smtpServer: params.managerHost,
28980
+ smtpPort: port,
28981
+ userName: params.authUsername ?? recipient,
28982
+ password: params.authPassword ?? "",
28983
+ address1: recipient,
28984
+ address2: "",
28985
+ address3: "",
28986
+ sendNickname: params.sendNickname ?? params.recipientLocalPart,
28987
+ attachment: attachmentType === "none" ? 0 : 1,
28988
+ attachmentType,
28989
+ textType: "withText",
28990
+ ssl: 0,
28991
+ interval
28992
+ };
28993
+ await this.setEmail(emailPatch, options);
28994
+ const fullWeekOn = "1".repeat(168);
28995
+ const current = await this.getEmailTask(channel, options);
28996
+ const triggerSet = new Set(triggers);
28997
+ const touched = [];
28998
+ const updatedList = current.typeScheduleList.map((item) => {
28999
+ if (triggerSet.has(item.type)) {
29000
+ touched.push(item.type);
29001
+ return { ...item, valueTable: fullWeekOn };
29002
+ }
29003
+ return item;
29004
+ });
29005
+ for (const t of triggers) {
29006
+ if (!current.typeScheduleList.some((item) => item.type === t)) {
29007
+ updatedList.push({ type: t, valueTable: fullWeekOn });
29008
+ touched.push(t);
29009
+ }
29010
+ }
29011
+ await this.setEmailTask(
29012
+ channel,
29013
+ {
29014
+ channelId: current.channelId,
29015
+ enable: 1,
29016
+ typeScheduleList: updatedList
29017
+ },
29018
+ options
29019
+ );
29020
+ const result = {
29021
+ setEmail: { applied: true },
29022
+ setEmailTask: { applied: true, touchedTypes: touched }
29023
+ };
29024
+ if (params.runTest) {
29025
+ const ok = await this.testEmail(emailPatch, options);
29026
+ result.testEmail = { success: ok };
29027
+ }
29028
+ return result;
27604
29029
  }
27605
29030
  /**
27606
29031
  * Get siren-on-motion state via AudioTask (cmdId=232).
@@ -27869,7 +29294,7 @@ ${xml}`
27869
29294
  cmdId: BC_CMD_ID_GET_SYSTEM_GENERAL,
27870
29295
  ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
27871
29296
  });
27872
- return parseXmlFragmentToJson(xml);
29297
+ return parseSystemGeneralFromXml(xml);
27873
29298
  }
27874
29299
  /**
27875
29300
  * Get device support/capability flags.
@@ -34596,6 +36021,17 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34596
36021
  // Native stream
34597
36022
  nativeFanout = null;
34598
36023
  nativeStreamActive = false;
36024
+ // Set only by stopNativeStream() (explicit teardown) so the fanout's onEnd
36025
+ // callback can short-circuit cleanup/restart logic. NOT set by the inactivity-
36026
+ // timeout force-restart path — that flow wants onEnd to run and decide
36027
+ // whether to restart based on prestartStream / connected clients.
36028
+ nativeStreamStopping = false;
36029
+ // Pending retry timer for the unbounded auto-restart loop. When a stream
36030
+ // start fails transiently (camera in maintenance reboot, idle-disconnect
36031
+ // race, etc.) we keep trying with exponential backoff until either the
36032
+ // server is stopped or a frame finally arrives.
36033
+ nativeStreamRetryTimer;
36034
+ nativeStreamRetryDelayMs = 0;
34599
36035
  dedicatedSessionRelease;
34600
36036
  detectedVideoType;
34601
36037
  // Client tracking
@@ -34664,6 +36100,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34664
36100
  if (!this.active) return;
34665
36101
  this.active = false;
34666
36102
  clearTimeout(this.stopGraceTimer);
36103
+ this.clearNativeStreamRetry();
34667
36104
  this.stopStreamHealthMonitor();
34668
36105
  for (const [id, sock] of this.clientSockets) {
34669
36106
  sock.destroy();
@@ -34992,6 +36429,46 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34992
36429
  // -----------------------------------------------------------------------
34993
36430
  // Native stream management
34994
36431
  // -----------------------------------------------------------------------
36432
+ /**
36433
+ * Schedule another startNativeStream() attempt after the given delay.
36434
+ * Idempotent: a no-op if a retry is already scheduled, the server is no
36435
+ * longer active, or an explicit stop is in progress. Implements unbounded
36436
+ * exponential backoff (5s → 60s) so a camera that stays unreachable for
36437
+ * minutes (e.g. nightly maintenance reboot) eventually recovers without
36438
+ * manual intervention — see issue #16.
36439
+ */
36440
+ scheduleNativeStreamRetry(reason) {
36441
+ if (!this.active) return;
36442
+ if (this.nativeStreamStopping) return;
36443
+ if (this.nativeStreamRetryTimer) return;
36444
+ const delay = this.nativeStreamRetryDelayMs > 0 ? this.nativeStreamRetryDelayMs : 5e3;
36445
+ this.logger.info?.(
36446
+ `[Go2rtcTcpServer] scheduling native stream retry in ${(delay / 1e3).toFixed(0)}s (reason=${reason})`
36447
+ );
36448
+ this.nativeStreamRetryTimer = setTimeout(() => {
36449
+ this.nativeStreamRetryTimer = void 0;
36450
+ if (!this.active) return;
36451
+ if (this.nativeStreamStopping) return;
36452
+ this.startNativeStream().catch((err) => {
36453
+ this.logger.warn?.(
36454
+ `[Go2rtcTcpServer] retry of startNativeStream threw: ${err instanceof Error ? err.message : err}`
36455
+ );
36456
+ });
36457
+ }, delay);
36458
+ this.nativeStreamRetryDelayMs = Math.min(delay * 2, 6e4);
36459
+ }
36460
+ /**
36461
+ * Cancel any pending retry timer and reset the backoff. Called on explicit
36462
+ * stop and on first-frame-received so the next failure starts the backoff
36463
+ * window from scratch.
36464
+ */
36465
+ clearNativeStreamRetry() {
36466
+ if (this.nativeStreamRetryTimer) {
36467
+ clearTimeout(this.nativeStreamRetryTimer);
36468
+ this.nativeStreamRetryTimer = void 0;
36469
+ }
36470
+ this.nativeStreamRetryDelayMs = 0;
36471
+ }
34995
36472
  async startNativeStream() {
34996
36473
  if (this.nativeStreamActive) return;
34997
36474
  if (!this.api.isReady) {
@@ -35008,8 +36485,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
35008
36485
  await this.api.ensureConnected();
35009
36486
  } catch (e) {
35010
36487
  this.logger.warn?.(
35011
- `[Go2rtcTcpServer] ensureConnected failed, aborting stream start: ${e}`
36488
+ `[Go2rtcTcpServer] ensureConnected failed: ${e}`
35012
36489
  );
36490
+ this.scheduleNativeStreamRetry("ensureConnected failed");
35013
36491
  return;
35014
36492
  }
35015
36493
  }
@@ -35039,6 +36517,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
35039
36517
  ...dedicatedClient ? { client: dedicatedClient } : {}
35040
36518
  }),
35041
36519
  onFrame: (frame) => {
36520
+ if (!hadFrames) {
36521
+ this.clearNativeStreamRetry();
36522
+ }
35042
36523
  hadFrames = true;
35043
36524
  this.lastFrameAt = Date.now();
35044
36525
  this.totalFramesReceived++;
@@ -35082,7 +36563,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
35082
36563
  this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
35083
36564
  },
35084
36565
  onEnd: () => {
35085
- if (!this.nativeStreamActive) return;
36566
+ if (this.nativeStreamStopping) return;
35086
36567
  this.nativeStreamActive = false;
35087
36568
  this.nativeFanout = null;
35088
36569
  this.stopStreamHealthMonitor();
@@ -35124,18 +36605,24 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
35124
36605
  this.startStreamHealthMonitor();
35125
36606
  }
35126
36607
  async stopNativeStream() {
36608
+ this.nativeStreamStopping = true;
35127
36609
  this.nativeStreamActive = false;
36610
+ this.clearNativeStreamRetry();
35128
36611
  this.stopStreamHealthMonitor();
35129
36612
  const fanout = this.nativeFanout;
35130
36613
  this.nativeFanout = null;
35131
- if (fanout) {
35132
- await fanout.stop();
35133
- }
35134
- this.prebuffer = [];
35135
- if (this.dedicatedSessionRelease) {
35136
- await this.dedicatedSessionRelease().catch(() => {
35137
- });
35138
- this.dedicatedSessionRelease = void 0;
36614
+ try {
36615
+ if (fanout) {
36616
+ await fanout.stop();
36617
+ }
36618
+ this.prebuffer = [];
36619
+ if (this.dedicatedSessionRelease) {
36620
+ await this.dedicatedSessionRelease().catch(() => {
36621
+ });
36622
+ this.dedicatedSessionRelease = void 0;
36623
+ }
36624
+ } finally {
36625
+ this.nativeStreamStopping = false;
35139
36626
  }
35140
36627
  }
35141
36628
  // -----------------------------------------------------------------------
@@ -35325,10 +36812,15 @@ var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
35325
36812
  // Force a known frame rate on raw H.264 input so the muxer gets valid PTS/DTS.
35326
36813
  "-r",
35327
36814
  String(this.inputFps),
36815
+ // `+genpts` generates uniform PTS from `-r` for raw Annex-B input. We
36816
+ // deliberately do NOT pass `-use_wallclock_as_timestamps 1`: that
36817
+ // overrides the generated PTS with the host wallclock at frame ARRIVAL
36818
+ // time, and the network stream is bursty so the resulting PTS is
36819
+ // uneven. With `-r` forcing a target rate downstream, ffmpeg then
36820
+ // drops/duplicates frames to match — visible as the periodic stutter
36821
+ // reported on local-restreamer recordings (issue #11).
35328
36822
  "-fflags",
35329
36823
  "+genpts",
35330
- "-use_wallclock_as_timestamps",
35331
- "1",
35332
36824
  "-f",
35333
36825
  "h264",
35334
36826
  // Input format (H.264 Annex-B)
@@ -36007,8 +37499,195 @@ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
36007
37499
  };
36008
37500
 
36009
37501
  // src/baichuan/stream/BaichuanWebRTCServer.ts
36010
- var import_node_events10 = require("events");
37502
+ var import_node_events11 = require("events");
36011
37503
  init_BcMediaAnnexBDecoder();
37504
+
37505
+ // src/baichuan/stream/AacToOpusTranscoder.ts
37506
+ var import_node_child_process11 = require("child_process");
37507
+ var import_node_dgram3 = require("dgram");
37508
+ var import_node_events10 = require("events");
37509
+ var AacToOpusTranscoder = class extends import_node_events10.EventEmitter {
37510
+ opts;
37511
+ socket = null;
37512
+ ffmpeg = null;
37513
+ port = 0;
37514
+ starting = null;
37515
+ stopped = false;
37516
+ constructor(options = {}) {
37517
+ super();
37518
+ this.opts = {
37519
+ ffmpegPath: options.ffmpegPath ?? "ffmpeg",
37520
+ opusSampleRate: options.opusSampleRate ?? 48e3,
37521
+ opusChannels: options.opusChannels ?? 2,
37522
+ opusFrameMs: options.opusFrameMs ?? 20,
37523
+ opusBitrate: options.opusBitrate ?? 64e3,
37524
+ ...options.logger !== void 0 ? { logger: options.logger } : {}
37525
+ };
37526
+ }
37527
+ log(level, message) {
37528
+ this.opts.logger?.(level, `[AacToOpusTranscoder] ${message}`);
37529
+ }
37530
+ /**
37531
+ * Allocate the UDP loopback socket and spawn ffmpeg. Must be awaited before
37532
+ * the first call to `feedAac`.
37533
+ */
37534
+ async start() {
37535
+ if (this.starting) return this.starting;
37536
+ this.starting = this._start();
37537
+ return this.starting;
37538
+ }
37539
+ async _start() {
37540
+ if (this.stopped) throw new Error("transcoder stopped");
37541
+ this.socket = (0, import_node_dgram3.createSocket)("udp4");
37542
+ await new Promise((resolve, reject) => {
37543
+ this.socket.once("error", reject);
37544
+ this.socket.bind({ address: "127.0.0.1", port: 0 }, () => {
37545
+ this.socket.removeListener("error", reject);
37546
+ this.port = this.socket.address().port;
37547
+ resolve();
37548
+ });
37549
+ });
37550
+ this.log("info", `UDP loopback bound on 127.0.0.1:${this.port}`);
37551
+ this.socket.on("message", (rtpPacket) => this.handleRtp(rtpPacket));
37552
+ this.socket.on("error", (err) => {
37553
+ this.log("error", `socket error: ${err.message}`);
37554
+ this.emit("error", err);
37555
+ });
37556
+ const args = [
37557
+ "-loglevel",
37558
+ "warning",
37559
+ "-fflags",
37560
+ "nobuffer",
37561
+ "-f",
37562
+ "aac",
37563
+ "-i",
37564
+ "pipe:0",
37565
+ "-c:a",
37566
+ "libopus",
37567
+ "-ar",
37568
+ String(this.opts.opusSampleRate),
37569
+ "-ac",
37570
+ String(this.opts.opusChannels),
37571
+ "-application",
37572
+ "audio",
37573
+ "-frame_duration",
37574
+ String(this.opts.opusFrameMs),
37575
+ "-b:a",
37576
+ String(this.opts.opusBitrate),
37577
+ // Important: disable VBR so the output rate matches the configured
37578
+ // bitrate consistently — easier on the browser jitter buffer.
37579
+ "-vbr",
37580
+ "off",
37581
+ "-f",
37582
+ "rtp",
37583
+ `rtp://127.0.0.1:${this.port}`
37584
+ ];
37585
+ this.log("info", `spawning ffmpeg with: ${this.opts.ffmpegPath} ${args.join(" ")}`);
37586
+ this.ffmpeg = (0, import_node_child_process11.spawn)(this.opts.ffmpegPath, args, {
37587
+ stdio: ["pipe", "ignore", "pipe"]
37588
+ });
37589
+ this.ffmpeg.on("error", (err) => {
37590
+ this.log("error", `ffmpeg spawn error: ${err.message}`);
37591
+ this.emit("error", err);
37592
+ });
37593
+ this.ffmpeg.stderr?.on("data", (chunk) => {
37594
+ this.log("debug", `ffmpeg stderr: ${chunk.toString().trim()}`);
37595
+ });
37596
+ this.ffmpeg.on("exit", (code) => {
37597
+ this.log(code === 0 ? "info" : "warn", `ffmpeg exited code=${code}`);
37598
+ this.emit("exit", code);
37599
+ });
37600
+ }
37601
+ /**
37602
+ * Feed one or more concatenated ADTS AAC frames to ffmpeg. Returns the
37603
+ * number of bytes written. Safe to call before a frame is fully buffered.
37604
+ */
37605
+ feedAac(buf) {
37606
+ if (this.stopped) return false;
37607
+ if (!this.ffmpeg?.stdin || !this.ffmpeg.stdin.writable) {
37608
+ this.log("debug", "drop AAC frame \u2014 ffmpeg stdin not writable");
37609
+ return false;
37610
+ }
37611
+ return this.ffmpeg.stdin.write(buf);
37612
+ }
37613
+ /**
37614
+ * Close stdin (ffmpeg exits cleanly on EOF) and tear down the UDP socket.
37615
+ */
37616
+ async stop() {
37617
+ if (this.stopped) return;
37618
+ this.stopped = true;
37619
+ try {
37620
+ this.ffmpeg?.stdin?.end();
37621
+ } catch {
37622
+ }
37623
+ if (this.ffmpeg && this.ffmpeg.exitCode === null) {
37624
+ await new Promise((resolve) => {
37625
+ const t = setTimeout(() => {
37626
+ this.ffmpeg?.kill("SIGKILL");
37627
+ resolve();
37628
+ }, 500);
37629
+ this.ffmpeg?.once("exit", () => {
37630
+ clearTimeout(t);
37631
+ resolve();
37632
+ });
37633
+ });
37634
+ }
37635
+ this.ffmpeg = null;
37636
+ if (this.socket) {
37637
+ try {
37638
+ this.socket.close();
37639
+ } catch {
37640
+ }
37641
+ this.socket = null;
37642
+ }
37643
+ this.log("info", "stopped");
37644
+ }
37645
+ /**
37646
+ * Parse an RTP packet ffmpeg emitted on the loopback socket and surface
37647
+ * its Opus payload. RTP header layout (RFC 3550):
37648
+ *
37649
+ * 0 1 2 3
37650
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
37651
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37652
+ * |V=2|P|X| CC |M| PT | sequence number |
37653
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37654
+ * | timestamp |
37655
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37656
+ * | synchronization source (SSRC) identifier |
37657
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37658
+ *
37659
+ * We honor the CC, X (extension) and P (padding) bits to compute the
37660
+ * payload offset / length correctly even if ffmpeg ever emits those.
37661
+ */
37662
+ handleRtp(packet) {
37663
+ if (packet.length < 12) return;
37664
+ const b0 = packet[0];
37665
+ const b1 = packet[1];
37666
+ const version = b0 >> 6;
37667
+ if (version !== 2) return;
37668
+ const padding = (b0 & 32) !== 0;
37669
+ const extension = (b0 & 16) !== 0;
37670
+ const csrcCount = b0 & 15;
37671
+ const marker = (b1 & 128) !== 0;
37672
+ let offset = 12 + csrcCount * 4;
37673
+ if (extension) {
37674
+ if (packet.length < offset + 4) return;
37675
+ const extLen = packet.readUInt16BE(offset + 2);
37676
+ offset += 4 + extLen * 4;
37677
+ }
37678
+ let end = packet.length;
37679
+ if (padding) {
37680
+ const pad = packet[packet.length - 1];
37681
+ end -= pad;
37682
+ }
37683
+ if (offset >= end) return;
37684
+ const timestamp = packet.readUInt32BE(4);
37685
+ const payload = Buffer.from(packet.subarray(offset, end));
37686
+ this.emit("packet", { payload, timestamp, marker });
37687
+ }
37688
+ };
37689
+
37690
+ // src/baichuan/stream/BaichuanWebRTCServer.ts
36012
37691
  init_H264Converter();
36013
37692
  function parseAnnexBNalUnits(annexB) {
36014
37693
  const nalUnits = [];
@@ -36044,7 +37723,7 @@ function getH264NalType(nalUnit) {
36044
37723
  function getH265NalType2(nalUnit) {
36045
37724
  return nalUnit[0] >> 1 & 63;
36046
37725
  }
36047
- var BaichuanWebRTCServer = class extends import_node_events10.EventEmitter {
37726
+ var BaichuanWebRTCServer = class extends import_node_events11.EventEmitter {
36048
37727
  options;
36049
37728
  sessions = /* @__PURE__ */ new Map();
36050
37729
  sessionIdCounter = 0;
@@ -36282,6 +37961,14 @@ Error: ${err}`
36282
37961
  }
36283
37962
  session.dataChannel = null;
36284
37963
  }
37964
+ if (session.audioTranscoder) {
37965
+ try {
37966
+ await session.audioTranscoder.stop();
37967
+ } catch (err) {
37968
+ this.log("debug", `Error stopping audio transcoder: ${err}`);
37969
+ }
37970
+ session.audioTranscoder = null;
37971
+ }
36285
37972
  if (session.cleanup) {
36286
37973
  session.cleanup();
36287
37974
  }
@@ -36408,6 +38095,17 @@ Error: ${err}`
36408
38095
  }
36409
38096
  if (frame.audio) {
36410
38097
  session.stats.audioFrames++;
38098
+ if (session.stats.audioFrames === 1) {
38099
+ const head = frame.data && frame.data.length > 0 ? frame.data.subarray(0, Math.min(8, frame.data.length)).toString("hex") : "(empty)";
38100
+ this.log(
38101
+ "info",
38102
+ `First audio frame for ${session.id}: codec=${frame.codec ?? "?"} bytes=${frame.data?.length ?? 0} head=${head}`
38103
+ );
38104
+ }
38105
+ if (this.options.ffmpegPath !== "" && frame.data && frame.data.length > 0) {
38106
+ await this.ensureAudioTranscoder(session, werift);
38107
+ session.audioTranscoder?.feedAac(frame.data);
38108
+ }
36411
38109
  } else {
36412
38110
  if (frame.data) {
36413
38111
  if (!session.videoCodec && frame.videoType) {
@@ -36478,7 +38176,7 @@ Error: ${err}`
36478
38176
  if (now - lastLogTime >= 5e3) {
36479
38177
  this.log(
36480
38178
  "debug",
36481
- `WebRTC session ${session.id} [${session.videoCodec}]: sent ${session.stats.videoFrames} frames, ${packetsSentSinceLastLog} packets, ${Math.round(session.stats.bytesSent / 1024)} KB`
38179
+ `WebRTC session ${session.id} [${session.videoCodec}]: sent ${session.stats.videoFrames} video frames, ${packetsSentSinceLastLog} packets, ${Math.round(session.stats.bytesSent / 1024)} KB | audio frames=${session.stats.audioFrames}`
36482
38180
  );
36483
38181
  lastLogTime = now;
36484
38182
  packetsSentSinceLastLog = 0;
@@ -36494,6 +38192,77 @@ Error: ${err}`
36494
38192
  }
36495
38193
  this.log("info", `Native stream ended for session ${session.id}`);
36496
38194
  }
38195
+ /**
38196
+ * Lazily start the AAC → Opus transcoder for `session` and wire it to the
38197
+ * audio RTP track. ffmpeg writes RTP-formatted Opus packets back to a UDP
38198
+ * loopback the transcoder owns; we strip the RTP header and rewrap the
38199
+ * Opus payload with our audioTrack's SSRC so the browser receives a
38200
+ * coherent stream.
38201
+ */
38202
+ async ensureAudioTranscoder(session, werift) {
38203
+ if (session.audioTranscoder !== void 0) return;
38204
+ const { RtpPacket, RtpHeader } = werift;
38205
+ const ssrc = session.audioTrack?.ssrc ?? Math.floor(Math.random() * 4294967295);
38206
+ const transcoder = new AacToOpusTranscoder({
38207
+ ...this.options.ffmpegPath ? { ffmpegPath: this.options.ffmpegPath } : {},
38208
+ logger: (level, msg) => this.log(level, msg)
38209
+ });
38210
+ session.audioRtpSequence = Math.floor(Math.random() * 65535);
38211
+ session.audioRtpTimestampBase = 0;
38212
+ transcoder.on("packet", ({ payload, timestamp, marker }) => {
38213
+ try {
38214
+ if (session.audioRtpTimestampBase === void 0 || session.audioRtpTimestampBase === 0) {
38215
+ session.audioRtpTimestampBase = timestamp;
38216
+ }
38217
+ const localTs = timestamp - session.audioRtpTimestampBase >>> 0;
38218
+ const seq = session.audioRtpSequence;
38219
+ session.audioRtpSequence = seq + 1 & 65535;
38220
+ const header = new RtpHeader({
38221
+ version: 2,
38222
+ padding: false,
38223
+ extension: false,
38224
+ marker,
38225
+ // Werift assigns 111 to Opus by default in the receiver SDP, but it
38226
+ // also accepts other PTs. Use 111 for compatibility with the offer
38227
+ // we generated earlier.
38228
+ payloadType: 111,
38229
+ sequenceNumber: seq,
38230
+ timestamp: localTs,
38231
+ ssrc
38232
+ });
38233
+ const rtp = new RtpPacket(header, payload);
38234
+ session.audioTrack?.writeRtp(rtp);
38235
+ if (!session.audioStartedLogged) {
38236
+ session.audioStartedLogged = true;
38237
+ this.log(
38238
+ "info",
38239
+ `Audio RTP started for ${session.id} (PT=111, ssrc=${ssrc})`
38240
+ );
38241
+ }
38242
+ } catch (err) {
38243
+ this.log(
38244
+ "warn",
38245
+ `audio writeRtp failed for ${session.id}: ${err.message}`
38246
+ );
38247
+ }
38248
+ });
38249
+ transcoder.on("error", (err) => {
38250
+ this.log("error", `audio transcoder error for ${session.id}: ${err.message}`);
38251
+ });
38252
+ transcoder.on("exit", (code) => {
38253
+ this.log("info", `audio transcoder exited (${code}) for ${session.id}`);
38254
+ });
38255
+ try {
38256
+ await transcoder.start();
38257
+ session.audioTranscoder = transcoder;
38258
+ } catch (err) {
38259
+ this.log(
38260
+ "error",
38261
+ `failed to start audio transcoder for ${session.id}: ${err.message}`
38262
+ );
38263
+ session.audioTranscoder = null;
38264
+ }
38265
+ }
36497
38266
  /**
36498
38267
  * Send H.264 frame via RTP media track
36499
38268
  * Returns the number of RTP packets sent
@@ -36781,21 +38550,21 @@ Error: ${err}`
36781
38550
  `Sending ${codec} frame ${frameNumber}: ${packet.length} bytes, keyframe=${isKeyframe}`
36782
38551
  );
36783
38552
  }
36784
- const MAX_CHUNK_SIZE = 16e3;
38553
+ const CHUNK_HEADER_LEN = 4;
38554
+ const MAX_PAYLOAD_PER_CHUNK = 16e3 - CHUNK_HEADER_LEN;
36785
38555
  try {
36786
- if (packet.length <= MAX_CHUNK_SIZE) {
36787
- session.videoDataChannel.send(packet);
36788
- } else {
36789
- const totalChunks = Math.ceil(packet.length / MAX_CHUNK_SIZE);
36790
- for (let i = 0; i < totalChunks; i++) {
36791
- const start = i * MAX_CHUNK_SIZE;
36792
- const end = Math.min(start + MAX_CHUNK_SIZE, packet.length);
36793
- const chunk = packet.subarray(start, end);
36794
- const chunkHeader = Buffer.alloc(2);
36795
- chunkHeader.writeUInt8(i, 0);
36796
- chunkHeader.writeUInt8(totalChunks, 1);
36797
- session.videoDataChannel.send(Buffer.concat([chunkHeader, chunk]));
36798
- }
38556
+ const totalChunks = Math.max(
38557
+ 1,
38558
+ Math.ceil(packet.length / MAX_PAYLOAD_PER_CHUNK)
38559
+ );
38560
+ for (let i = 0; i < totalChunks; i++) {
38561
+ const start = i * MAX_PAYLOAD_PER_CHUNK;
38562
+ const end = Math.min(start + MAX_PAYLOAD_PER_CHUNK, packet.length);
38563
+ const chunk = packet.subarray(start, end);
38564
+ const chunkHeader = Buffer.alloc(CHUNK_HEADER_LEN);
38565
+ chunkHeader.writeUInt16BE(i, 0);
38566
+ chunkHeader.writeUInt16BE(totalChunks, 2);
38567
+ session.videoDataChannel.send(Buffer.concat([chunkHeader, chunk]));
36799
38568
  }
36800
38569
  return true;
36801
38570
  } catch (err) {
@@ -36946,12 +38715,12 @@ Error: ${err}`
36946
38715
  };
36947
38716
 
36948
38717
  // src/baichuan/stream/BaichuanHlsServer.ts
36949
- var import_node_events11 = require("events");
38718
+ var import_node_events12 = require("events");
36950
38719
  var import_node_fs = __toESM(require("fs"), 1);
36951
38720
  var import_promises3 = __toESM(require("fs/promises"), 1);
36952
38721
  var import_node_os3 = __toESM(require("os"), 1);
36953
38722
  var import_node_path3 = __toESM(require("path"), 1);
36954
- var import_node_child_process11 = require("child_process");
38723
+ var import_node_child_process12 = require("child_process");
36955
38724
  init_BcMediaAnnexBDecoder();
36956
38725
  init_H264Converter();
36957
38726
  init_H265Converter();
@@ -37026,7 +38795,7 @@ function getNalTypes(codec, annexB) {
37026
38795
  }
37027
38796
  });
37028
38797
  }
37029
- var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
38798
+ var BaichuanHlsServer = class extends import_node_events12.EventEmitter {
37030
38799
  api;
37031
38800
  channel;
37032
38801
  profile;
@@ -37383,10 +39152,17 @@ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
37383
39152
  "-hide_banner",
37384
39153
  "-loglevel",
37385
39154
  "warning",
39155
+ // `+genpts` makes ffmpeg generate uniform PTS from the declared `-r`
39156
+ // when the raw H.264/H.265 input has none. We deliberately do NOT use
39157
+ // `-use_wallclock_as_timestamps 1` here: it replaces the generated
39158
+ // PTS with the host wallclock at FRAME ARRIVAL time, and because the
39159
+ // camera ships frames in bursty network reads, the resulting PTS
39160
+ // sequence is uneven. With `-r 25` (or anything else) forcing a
39161
+ // target rate downstream, ffmpeg then drops/duplicates frames to
39162
+ // match — visible as the periodic stutter / pulsing reported on
39163
+ // local-restreamer recordings (issue #11).
37386
39164
  "-fflags",
37387
39165
  "+genpts",
37388
- "-use_wallclock_as_timestamps",
37389
- "1",
37390
39166
  "-r",
37391
39167
  "25",
37392
39168
  "-f",
@@ -37421,7 +39197,7 @@ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
37421
39197
  this.segmentPattern,
37422
39198
  this.playlistPath
37423
39199
  );
37424
- const p = (0, import_node_child_process11.spawn)(this.ffmpegPath, args, {
39200
+ const p = (0, import_node_child_process12.spawn)(this.ffmpegPath, args, {
37425
39201
  stdio: ["pipe", "ignore", "pipe"]
37426
39202
  });
37427
39203
  p.on("error", (err) => {
@@ -38043,10 +39819,10 @@ async function autoDetectDeviceType(inputs) {
38043
39819
  }
38044
39820
 
38045
39821
  // src/multifocal/compositeRtspServer.ts
38046
- var import_node_events12 = require("events");
38047
- var import_node_child_process12 = require("child_process");
39822
+ var import_node_events13 = require("events");
39823
+ var import_node_child_process13 = require("child_process");
38048
39824
  var net5 = __toESM(require("net"), 1);
38049
- var CompositeRtspServer = class extends import_node_events12.EventEmitter {
39825
+ var CompositeRtspServer = class extends import_node_events13.EventEmitter {
38050
39826
  options;
38051
39827
  compositeStream = null;
38052
39828
  rtspServer = null;
@@ -38151,7 +39927,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38151
39927
  this.logger.log?.(
38152
39928
  `[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
38153
39929
  );
38154
- this.ffmpegProcess = (0, import_node_child_process12.spawn)("ffmpeg", ffmpegArgs, {
39930
+ this.ffmpegProcess = (0, import_node_child_process13.spawn)("ffmpeg", ffmpegArgs, {
38155
39931
  stdio: ["pipe", "pipe", "pipe"]
38156
39932
  });
38157
39933
  this.ffmpegProcess.on("error", (error) => {
@@ -38259,6 +40035,86 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38259
40035
  return this.connectedClients.size;
38260
40036
  }
38261
40037
  };
40038
+
40039
+ // src/reolink/baichuan/utils/motionZone.ts
40040
+ function decodeMotionScopeBitmap(valueTable, columns, rows, width = columns, height = rows) {
40041
+ const trimmed = valueTable.trim().replace(/[^A-Za-z0-9+/=]/g, "");
40042
+ const bytes = base64DecodeToBytes(trimmed);
40043
+ const totalBits = columns * rows;
40044
+ if (bytes.length * 8 < totalBits) {
40045
+ throw new Error(
40046
+ `valueTable too short: have ${bytes.length * 8} bits, need ${totalBits}`
40047
+ );
40048
+ }
40049
+ const w = Math.min(width, columns);
40050
+ const h = Math.min(height, rows);
40051
+ const cells = new Array(w * h);
40052
+ for (let r = 0; r < h; r++) {
40053
+ for (let c = 0; c < w; c++) {
40054
+ const bitIndex = r * columns + c;
40055
+ const byteIdx = bitIndex >> 3;
40056
+ const bitIdx = 7 - (bitIndex & 7);
40057
+ cells[r * w + c] = (bytes[byteIdx] ?? 0) >> bitIdx & 1 ? true : false;
40058
+ }
40059
+ }
40060
+ return { width: w, height: h, columns, rows, cells };
40061
+ }
40062
+ function encodeMotionScopeBitmap(scope) {
40063
+ const bytes = new Uint8Array(Math.ceil(scope.columns * scope.rows / 8));
40064
+ for (let r = 0; r < scope.height; r++) {
40065
+ for (let c = 0; c < scope.width; c++) {
40066
+ const on = scope.cells[r * scope.width + c];
40067
+ if (!on) continue;
40068
+ const bitIndex = r * scope.columns + c;
40069
+ const byteIdx = bitIndex >> 3;
40070
+ const bitIdx = 7 - (bitIndex & 7);
40071
+ bytes[byteIdx] = (bytes[byteIdx] ?? 0) | 1 << bitIdx;
40072
+ }
40073
+ }
40074
+ return base64EncodeBytes(bytes);
40075
+ }
40076
+ function fullCoverageScope(columns, rows, width = columns, height = rows) {
40077
+ return {
40078
+ width,
40079
+ height,
40080
+ columns,
40081
+ rows,
40082
+ cells: new Array(width * height).fill(true)
40083
+ };
40084
+ }
40085
+ var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
40086
+ function base64EncodeBytes(bytes) {
40087
+ let out = "";
40088
+ for (let i = 0; i < bytes.length; i += 3) {
40089
+ const b0 = bytes[i] ?? 0;
40090
+ const b1 = bytes[i + 1] ?? 0;
40091
+ const b2 = bytes[i + 2] ?? 0;
40092
+ out += B64_ALPHABET[b0 >> 2];
40093
+ out += B64_ALPHABET[(b0 & 3) << 4 | b1 >> 4];
40094
+ out += i + 1 < bytes.length ? B64_ALPHABET[(b1 & 15) << 2 | b2 >> 6] : "=";
40095
+ out += i + 2 < bytes.length ? B64_ALPHABET[b2 & 63] : "=";
40096
+ }
40097
+ return out;
40098
+ }
40099
+ function base64DecodeToBytes(b64) {
40100
+ const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
40101
+ const stripped = padded.replace(/=+$/, "");
40102
+ const out = new Uint8Array(Math.floor(stripped.length * 6 / 8));
40103
+ let bits = 0;
40104
+ let value = 0;
40105
+ let outIdx = 0;
40106
+ for (const ch of stripped) {
40107
+ const idx = B64_ALPHABET.indexOf(ch);
40108
+ if (idx < 0) continue;
40109
+ value = value << 6 | idx;
40110
+ bits += 6;
40111
+ if (bits >= 8) {
40112
+ bits -= 8;
40113
+ out[outIdx++] = value >> bits & 255;
40114
+ }
40115
+ }
40116
+ return out.subarray(0, outIdx);
40117
+ }
38262
40118
  // Annotate the CommonJS export names for ESM import in node:
38263
40119
  0 && (module.exports = {
38264
40120
  AesStreamDecryptor,
@@ -38306,6 +40162,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38306
40162
  BC_CMD_ID_GET_AUDIO_CFG,
38307
40163
  BC_CMD_ID_GET_AUDIO_TASK,
38308
40164
  BC_CMD_ID_GET_AUTO_FOCUS,
40165
+ BC_CMD_ID_GET_AUTO_REBOOT,
38309
40166
  BC_CMD_ID_GET_BATTERY_INFO,
38310
40167
  BC_CMD_ID_GET_BATTERY_INFO_LIST,
38311
40168
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
@@ -38313,6 +40170,8 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38313
40170
  BC_CMD_ID_GET_DING_DONG_CFG,
38314
40171
  BC_CMD_ID_GET_DING_DONG_LIST,
38315
40172
  BC_CMD_ID_GET_DING_DONG_SILENT,
40173
+ BC_CMD_ID_GET_DST,
40174
+ BC_CMD_ID_GET_EMAIL,
38316
40175
  BC_CMD_ID_GET_EMAIL_TASK,
38317
40176
  BC_CMD_ID_GET_ENC,
38318
40177
  BC_CMD_ID_GET_FTP_TASK,
@@ -38320,6 +40179,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38320
40179
  BC_CMD_ID_GET_KIT_AP_CFG,
38321
40180
  BC_CMD_ID_GET_LED_STATE,
38322
40181
  BC_CMD_ID_GET_MOTION_ALARM,
40182
+ BC_CMD_ID_GET_NTP,
38323
40183
  BC_CMD_ID_GET_ONLINE_USER_LIST,
38324
40184
  BC_CMD_ID_GET_OSD_DATETIME,
38325
40185
  BC_CMD_ID_GET_PIR_INFO,
@@ -38336,6 +40196,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38336
40196
  BC_CMD_ID_GET_SUPPORT,
38337
40197
  BC_CMD_ID_GET_SYSTEM_GENERAL,
38338
40198
  BC_CMD_ID_GET_TIMELAPSE_CFG,
40199
+ BC_CMD_ID_GET_VERSION_INFO,
38339
40200
  BC_CMD_ID_GET_VIDEO_INPUT,
38340
40201
  BC_CMD_ID_GET_WHITE_LED,
38341
40202
  BC_CMD_ID_GET_WIFI,
@@ -38359,18 +40220,24 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38359
40220
  BC_CMD_ID_SET_AUDIO_CFG,
38360
40221
  BC_CMD_ID_SET_AUDIO_TASK,
38361
40222
  BC_CMD_ID_SET_AUTO_FOCUS,
40223
+ BC_CMD_ID_SET_AUTO_REBOOT,
38362
40224
  BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
38363
40225
  BC_CMD_ID_SET_DING_DONG_CFG,
38364
40226
  BC_CMD_ID_SET_DING_DONG_SILENT,
40227
+ BC_CMD_ID_SET_DST,
40228
+ BC_CMD_ID_SET_EMAIL,
38365
40229
  BC_CMD_ID_SET_EMAIL_TASK,
38366
40230
  BC_CMD_ID_SET_ENC,
38367
40231
  BC_CMD_ID_SET_LED_STATE,
38368
40232
  BC_CMD_ID_SET_MOTION_ALARM,
40233
+ BC_CMD_ID_SET_NTP,
40234
+ BC_CMD_ID_SET_OSD_DATETIME,
38369
40235
  BC_CMD_ID_SET_PIR_INFO,
38370
40236
  BC_CMD_ID_SET_PRIVACY_MASK,
38371
40237
  BC_CMD_ID_SET_PUSH_TASK,
38372
40238
  BC_CMD_ID_SET_RECORD,
38373
40239
  BC_CMD_ID_SET_RECORD_CFG,
40240
+ BC_CMD_ID_SET_SYSTEM_GENERAL,
38374
40241
  BC_CMD_ID_SET_VIDEO_INPUT,
38375
40242
  BC_CMD_ID_SET_WHITE_LED_STATE,
38376
40243
  BC_CMD_ID_SET_WHITE_LED_TASK,
@@ -38380,6 +40247,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38380
40247
  BC_CMD_ID_TALK_ABILITY,
38381
40248
  BC_CMD_ID_TALK_CONFIG,
38382
40249
  BC_CMD_ID_TALK_RESET,
40250
+ BC_CMD_ID_TEST_EMAIL,
38383
40251
  BC_CMD_ID_UDP_KEEP_ALIVE,
38384
40252
  BC_CMD_ID_VIDEO,
38385
40253
  BC_CMD_ID_VIDEO_STOP,
@@ -38474,6 +40342,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38474
40342
  decideSleepInferenceTransition,
38475
40343
  decideVideoclipTranscodeMode,
38476
40344
  decodeHeader,
40345
+ decodeMotionScopeBitmap,
38477
40346
  deriveAesKey,
38478
40347
  detectIosClient,
38479
40348
  detectVideoCodecFromNal,
@@ -38486,6 +40355,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38486
40355
  discoverViaUdpBroadcast,
38487
40356
  discoverViaUdpDirect,
38488
40357
  encodeHeader,
40358
+ encodeMotionScopeBitmap,
38489
40359
  ensureXmlHeader,
38490
40360
  extractH264ParamSetsFromAccessUnit,
38491
40361
  extractH265ParamSetsFromAccessUnit,
@@ -38494,6 +40364,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38494
40364
  extractVpsFromAnnexB,
38495
40365
  flattenAbilitiesForChannel,
38496
40366
  formatMjpegFrame,
40367
+ fullCoverageScope,
38497
40368
  getConstructedVideoStreamOptions,
38498
40369
  getGlobalLogger,
38499
40370
  getH265NalType,
@@ -38537,6 +40408,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
38537
40408
  splitAnnexBToNals,
38538
40409
  splitH265AnnexBToNalPayloads,
38539
40410
  testChannelStreams,
40411
+ upsertXmlTag,
38540
40412
  xmlEscape,
38541
40413
  xmlIndicatesFloodlight,
38542
40414
  zipDirectory