@apocaliss92/nodelink-js 0.4.7 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,7 +60,7 @@ var init_urls = __esm({
60
60
  function bcHeaderHasPayloadOffset(messageClass) {
61
61
  return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
62
62
  }
63
- var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
63
+ var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
64
64
  var init_constants = __esm({
65
65
  "src/protocol/constants.ts"() {
66
66
  "use strict";
@@ -154,6 +154,17 @@ var init_constants = __esm({
154
154
  BC_CMD_ID_SET_AI_CFG = 300;
155
155
  BC_CMD_ID_GET_SIREN_STATUS = 547;
156
156
  BC_CMD_ID_SET_AUDIO_TASK = 231;
157
+ BC_CMD_ID_SET_VIDEO_INPUT = 25;
158
+ BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD = 297;
159
+ BC_CMD_ID_GET_ENC = 56;
160
+ BC_CMD_ID_SET_ENC = 57;
161
+ BC_CMD_ID_GET_PRIVACY_MASK = 52;
162
+ BC_CMD_ID_SET_PRIVACY_MASK = 53;
163
+ BC_CMD_ID_SET_AI_DENOISE = 440;
164
+ BC_CMD_ID_SET_LED_STATE = 209;
165
+ BC_CMD_ID_SET_AUDIO_CFG = 265;
166
+ BC_CMD_ID_GET_AUTO_FOCUS = 224;
167
+ BC_CMD_ID_SET_AUTO_FOCUS = 225;
157
168
  BC_CMD_ID_CMD_123 = 123;
158
169
  BC_CMD_ID_CMD_209 = 209;
159
170
  BC_CMD_ID_CMD_265 = 265;
@@ -1851,7 +1862,7 @@ var init_BaichuanVideoStream = __esm({
1851
1862
  const allowMsgNum0Fallback = this.acceptAnyStreamType && frame.header.msgNum === 0;
1852
1863
  if (!allowMsgNum0Fallback) {
1853
1864
  const frameCount = this._msgNumMismatchCount = (this._msgNumMismatchCount || 0) + 1;
1854
- if (frameCount <= 5) {
1865
+ if (frameCount <= 5 && this.client.getDebugConfig().general) {
1855
1866
  this.logger?.log(
1856
1867
  `[BaichuanVideoStream] Frame msgNum mismatch: received=${frame.header.msgNum}, expected=${this.activeMsgNum}, channel=${this.channel}, profile=${this.profile}, variant=${this.variant} (frame discarded)`
1857
1868
  );
@@ -1861,7 +1872,7 @@ var init_BaichuanVideoStream = __esm({
1861
1872
  }
1862
1873
  if (!this.acceptAnyStreamType && !this.expectedStreamTypes.has(frame.header.streamType)) {
1863
1874
  const frameCount = this._streamTypeMismatchCount = (this._streamTypeMismatchCount || 0) + 1;
1864
- if (frameCount <= 5) {
1875
+ if (frameCount <= 5 && this.client.getDebugConfig().general) {
1865
1876
  this.logger?.log(
1866
1877
  `[BaichuanVideoStream] Frame streamType mismatch: received=${frame.header.streamType}, expectedAny=[${[
1867
1878
  ...this.expectedStreamTypes
@@ -2691,6 +2702,59 @@ function buildFloodlightManualXml(channelId, status, durationSeconds = 180) {
2691
2702
  </FloodlightManual>
2692
2703
  </body>`;
2693
2704
  }
2705
+ function ensureXmlHeader(xml) {
2706
+ const trimmed = xml.trimStart();
2707
+ if (trimmed.startsWith("<?xml")) return xml;
2708
+ return `${XML_HEADER}
2709
+ ${xml}`;
2710
+ }
2711
+ function applyXmlTagPatch(xml, tag, value) {
2712
+ if (value === void 0) return xml;
2713
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2714
+ const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2715
+ return xml.replace(re, `<${tag}>${v}</${tag}>`);
2716
+ }
2717
+ function patchNestedTag(xml, parent, child, value) {
2718
+ if (value === void 0) return xml;
2719
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2720
+ const re = new RegExp(
2721
+ `(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
2722
+ );
2723
+ return xml.replace(re, `$1${v}$2`);
2724
+ }
2725
+ function applyStreamPatch(xml, streamTag, patch) {
2726
+ if (!patch) return xml;
2727
+ const re = new RegExp(
2728
+ `(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`
2729
+ );
2730
+ return xml.replace(re, (_match, open, body, close) => {
2731
+ let next = body;
2732
+ if (patch.bitRate !== void 0) {
2733
+ next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
2734
+ }
2735
+ if (patch.frameRate !== void 0) {
2736
+ next = applyXmlTagPatch(next, "frameRate", patch.frameRate);
2737
+ next = applyXmlTagPatch(next, "frame", patch.frameRate);
2738
+ }
2739
+ if (patch.videoEncType !== void 0) {
2740
+ const intVal = patch.videoEncType === "h265" ? 1 : 0;
2741
+ next = applyXmlTagPatch(next, "videoEncType", intVal);
2742
+ }
2743
+ return `${open}${next}${close}`;
2744
+ });
2745
+ }
2746
+ function normalizeDayNightMode(input) {
2747
+ const stripped = String(input).replace(/&/g, "And");
2748
+ if (!stripped) return stripped;
2749
+ const first = stripped[0];
2750
+ if (first === void 0) return stripped;
2751
+ return first.toLowerCase() + stripped.slice(1);
2752
+ }
2753
+ function normalizeOpenClose(input) {
2754
+ const v = String(input).toLowerCase();
2755
+ if (v === "on" || v === "open" || v === "1" || v === "true") return "open";
2756
+ return "close";
2757
+ }
2694
2758
  function buildAbilityInfoExtensionXml(username) {
2695
2759
  return `<?xml version="1.0" encoding="UTF-8" ?>
2696
2760
  <Extension version="1.1">
@@ -2698,9 +2762,11 @@ function buildAbilityInfoExtensionXml(username) {
2698
2762
  <token>system, streaming, PTZ, IO, security, replay, disk, network, alarm, record, video, image</token>
2699
2763
  </Extension>`;
2700
2764
  }
2765
+ var XML_HEADER;
2701
2766
  var init_xml = __esm({
2702
2767
  "src/protocol/xml.ts"() {
2703
2768
  "use strict";
2769
+ XML_HEADER = `<?xml version="1.0" encoding="UTF-8" ?>`;
2704
2770
  }
2705
2771
  });
2706
2772
 
@@ -3520,6 +3586,137 @@ var init_ReolinkCgiApi = __esm({
3520
3586
  const param = channel == null ? {} : { channel };
3521
3587
  return await this.call("GetPtzPreset", param, 1);
3522
3588
  }
3589
+ // ── Isp / Image (colour, flip, day-night, exposure) ──────────────
3590
+ async GetIsp(channel) {
3591
+ const param = channel == null ? {} : { channel };
3592
+ return await this.call("GetIsp", param, 1);
3593
+ }
3594
+ async SetIsp(isp) {
3595
+ return await this.call("SetIsp", isp, 0);
3596
+ }
3597
+ async GetImage(channel) {
3598
+ const param = channel == null ? {} : { channel };
3599
+ return await this.call("GetImage", param, 1);
3600
+ }
3601
+ async SetImage(image) {
3602
+ return await this.call("SetImage", image, 0);
3603
+ }
3604
+ // ── AudioCfg (mute / volume) ─────────────────────────────────────
3605
+ async GetAudioCfg(channel) {
3606
+ const param = channel == null ? {} : { channel };
3607
+ return await this.call("GetAudioCfg", param, 1);
3608
+ }
3609
+ async SetAudioCfg(audio) {
3610
+ return await this.call("SetAudioCfg", audio, 0);
3611
+ }
3612
+ // ── Enc setter (Get already exists above) ────────────────────────
3613
+ async SetEnc(enc) {
3614
+ return await this.call("SetEnc", enc, 0);
3615
+ }
3616
+ // ── MdAlarm (motion detection sensitivity / regions) ─────────────
3617
+ async GetMdAlarm(channel) {
3618
+ const param = channel == null ? {} : { channel };
3619
+ return await this.call("GetMdAlarm", param, 1);
3620
+ }
3621
+ async SetMdAlarm(md) {
3622
+ return await this.call("SetMdAlarm", md, 0);
3623
+ }
3624
+ // ── IrLights ─────────────────────────────────────────────────────
3625
+ async GetIrLights(channel) {
3626
+ const param = channel == null ? {} : { channel };
3627
+ return await this.call("GetIrLights", param, 1);
3628
+ }
3629
+ async SetIrLights(ir) {
3630
+ return await this.call("SetIrLights", ir, 0);
3631
+ }
3632
+ // ── AiCfg (smart-detection enable + class filter) ────────────────
3633
+ async GetAiCfg(channel) {
3634
+ const param = channel == null ? {} : { channel };
3635
+ return await this.call("GetAiCfg", param, 1);
3636
+ }
3637
+ async SetAiCfg(ai) {
3638
+ return await this.call("SetAiCfg", ai, 0);
3639
+ }
3640
+ // ── Mask (privacy-mask zones) ────────────────────────────────────
3641
+ async GetMask(channel) {
3642
+ return await this.call("GetMask", { channel }, 1);
3643
+ }
3644
+ async SetMask(mask) {
3645
+ return await this.call("SetMask", mask, 0);
3646
+ }
3647
+ // ── AudioNoise (input noise reduction) ───────────────────────────
3648
+ async GetAudioNoise(channel) {
3649
+ return await this.call("GetAudioNoise", { channel }, 1);
3650
+ }
3651
+ async SetAudioNoise(noise) {
3652
+ return await this.call("SetAudioNoise", noise, 0);
3653
+ }
3654
+ // ── Rec / RecV20 (recording schedule) ────────────────────────────
3655
+ async GetRec(channel) {
3656
+ return await this.call("GetRec", { channel }, 1);
3657
+ }
3658
+ async SetRec(rec) {
3659
+ return await this.call("SetRec", rec, 0);
3660
+ }
3661
+ /** Newer firmwares advertise `GetRecV20` / `SetRecV20` with the
3662
+ * weekly-schedule `table` field. Same payload shape as `Rec`. */
3663
+ async GetRecV20(channel) {
3664
+ return await this.call("GetRecV20", { channel }, 1);
3665
+ }
3666
+ async SetRecV20(rec) {
3667
+ return await this.call("SetRecV20", rec, 0);
3668
+ }
3669
+ // ── Email (SMTP alert) ───────────────────────────────────────────
3670
+ async GetEmail(channel) {
3671
+ return await this.call("GetEmail", { channel }, 1);
3672
+ }
3673
+ async SetEmail(email) {
3674
+ return await this.call("SetEmail", email, 0);
3675
+ }
3676
+ /** V20 variant on newer firmwares with weekly-schedule `table`. */
3677
+ async GetEmailV20(channel) {
3678
+ return await this.call("GetEmailV20", { channel }, 1);
3679
+ }
3680
+ async SetEmailV20(email) {
3681
+ return await this.call("SetEmailV20", email, 0);
3682
+ }
3683
+ // ── Push (Reolink-cloud push notifications) ──────────────────────
3684
+ async GetPush(channel) {
3685
+ return await this.call("GetPush", { channel }, 1);
3686
+ }
3687
+ async SetPush(push) {
3688
+ return await this.call("SetPush", push, 0);
3689
+ }
3690
+ async GetPushV20(channel) {
3691
+ return await this.call("GetPushV20", { channel }, 1);
3692
+ }
3693
+ async SetPushV20(push) {
3694
+ return await this.call("SetPushV20", push, 0);
3695
+ }
3696
+ // ── AudioAlarm (siren-on-event) ──────────────────────────────────
3697
+ async GetAudioAlarm(channel) {
3698
+ return await this.call("GetAudioAlarm", { channel }, 1);
3699
+ }
3700
+ async SetAudioAlarm(audio) {
3701
+ return await this.call("SetAudioAlarm", audio, 0);
3702
+ }
3703
+ async SetAudioAlarmV20(audio) {
3704
+ return await this.call("SetAudioAlarmV20", audio, 0);
3705
+ }
3706
+ // ── AutoFocus (PTZ AF) ───────────────────────────────────────────
3707
+ async GetAutoFocus(channel) {
3708
+ return await this.call("GetAutoFocus", { channel }, 1);
3709
+ }
3710
+ async SetAutoFocus(af) {
3711
+ return await this.call("SetAutoFocus", af, 0);
3712
+ }
3713
+ // ── AiAlarm (per-class smart-detection thresholds) ───────────────
3714
+ async GetAiAlarm(channel, aiType) {
3715
+ return await this.call("GetAiAlarm", { channel, ai_type: aiType }, 1);
3716
+ }
3717
+ async SetAiAlarm(ai) {
3718
+ return await this.call("SetAiAlarm", ai, 0);
3719
+ }
3523
3720
  async GetAudioAlarmV20(channel) {
3524
3721
  const param = channel == null ? {} : { channel };
3525
3722
  return await this.call("GetAudioAlarmV20", param, 0);
@@ -7815,19 +8012,34 @@ async function* createNativeStream(api, channel, profile, options) {
7815
8012
  }
7816
8013
  });
7817
8014
  streamStarted = true;
7818
- while (!closed) {
8015
+ const signal = options?.signal;
8016
+ while (!closed && !signal?.aborted) {
7819
8017
  if (frameQueue.length > 0) {
7820
8018
  const frame = frameQueue.shift();
7821
8019
  yield frame;
7822
8020
  } else {
7823
8021
  await new Promise((resolve) => {
7824
8022
  frameResolve = resolve;
7825
- setTimeout(() => {
8023
+ const timer = setTimeout(() => {
7826
8024
  if (frameResolve === resolve) {
7827
8025
  frameResolve = null;
7828
8026
  resolve();
7829
8027
  }
7830
8028
  }, 1e3);
8029
+ if (signal) {
8030
+ const onAbort = () => {
8031
+ clearTimeout(timer);
8032
+ if (frameResolve === resolve) frameResolve = null;
8033
+ resolve();
8034
+ };
8035
+ if (signal.aborted) {
8036
+ clearTimeout(timer);
8037
+ frameResolve = null;
8038
+ resolve();
8039
+ } else {
8040
+ signal.addEventListener("abort", onAbort, { once: true });
8041
+ }
8042
+ }
7831
8043
  });
7832
8044
  }
7833
8045
  }
@@ -8000,13 +8212,14 @@ var NativeStreamFanout = class {
8000
8212
  source = null;
8001
8213
  running = false;
8002
8214
  pumpPromise = null;
8215
+ abort = new AbortController();
8003
8216
  constructor(opts) {
8004
8217
  this.opts = opts;
8005
8218
  }
8006
8219
  start() {
8007
8220
  if (this.running) return;
8008
8221
  this.running = true;
8009
- this.source = this.opts.createSource();
8222
+ this.source = this.opts.createSource(this.abort.signal);
8010
8223
  this.pumpPromise = (async () => {
8011
8224
  try {
8012
8225
  for await (const frame of this.source) {
@@ -8052,6 +8265,7 @@ var NativeStreamFanout = class {
8052
8265
  this.source = null;
8053
8266
  for (const q of this.queues.values()) q.close();
8054
8267
  this.queues.clear();
8268
+ this.abort.abort();
8055
8269
  try {
8056
8270
  await src?.return(void 0);
8057
8271
  } catch {
@@ -8090,9 +8304,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8090
8304
  requireAuth;
8091
8305
  authNonces = /* @__PURE__ */ new Map();
8092
8306
  // Track nonces per client
8093
- AUTH_REALM = "BaichuanRtspServer";
8307
+ AUTH_REALM;
8094
8308
  NONCE_TIMEOUT_MS = 3e5;
8095
8309
  // 5 minutes
8310
+ lazyMetadata;
8096
8311
  // Client tracking
8097
8312
  connectedClients = /* @__PURE__ */ new Set();
8098
8313
  // Set of client IDs (IP:port)
@@ -8104,8 +8319,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8104
8319
  // Track all client resources for cleanup
8105
8320
  clientResources = /* @__PURE__ */ new Map();
8106
8321
  isRtspDebugEnabled() {
8107
- const dbg = this.api.client.getDebugConfig();
8108
- return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8322
+ try {
8323
+ if (this.api.isClosed) {
8324
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8325
+ }
8326
+ const dbg = this.api.client.getDebugConfig();
8327
+ return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8328
+ } catch {
8329
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8330
+ }
8109
8331
  }
8110
8332
  rtspDebugLog(message) {
8111
8333
  if (!this.isRtspDebugEnabled()) return;
@@ -8127,10 +8349,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8127
8349
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
8128
8350
  nativeFanout = null;
8129
8351
  noClientAutoStopTimer;
8352
+ /** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
8353
+ noFrameDeadlineTimer;
8130
8354
  /** After last RTSP client; 0 = never auto-stop native stream. */
8131
8355
  nativeStreamIdleStopMs;
8132
8356
  /** Primed-but-no-PLAY timeout; 0 = disabled. */
8133
8357
  nativeStreamPrimeIdleStopMs;
8358
+ /**
8359
+ * Max time to wait for the first camera frame after stream start.
8360
+ * If no frames arrive within this window, the native stream is stopped
8361
+ * (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
8362
+ * firing and waking the camera when no real viewer is watching.
8363
+ * 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
8364
+ */
8365
+ nativeStreamNoFrameDeadlineMs;
8134
8366
  // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
8135
8367
  // When a new client connects while the stream is already running it does not need
8136
8368
  // to wait up to one full GOP interval for the next keyframe — we replay frames
@@ -8256,14 +8488,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8256
8488
  this.logger = options.logger ?? console;
8257
8489
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
8258
8490
  this.deviceId = options.deviceId;
8259
- this.externalListener = options.externalListener ?? false;
8491
+ this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
8260
8492
  this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
8261
8493
  this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
8262
- this.authCredentials = options.credentials ?? [];
8494
+ this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
8495
+ this.authCredentials = (options.credentials ?? []).map((c) => ({
8496
+ username: c.username,
8497
+ ...c.password !== void 0 ? { password: c.password } : {},
8498
+ ...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
8499
+ }));
8263
8500
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
8501
+ this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
8502
+ this.lazyMetadata = options.lazyMetadata ?? false;
8264
8503
  const transport = this.api.client.getTransport();
8265
8504
  this.flow = createRtspFlow(transport, "H264");
8266
8505
  }
8506
+ /** Number of currently connected RTSP clients. */
8507
+ get clientCount() {
8508
+ return this.connectedClients.size;
8509
+ }
8267
8510
  // --- Authentication helpers ---
8268
8511
  /**
8269
8512
  * Generate a new nonce for Digest authentication
@@ -8324,9 +8567,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8324
8567
  this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
8325
8568
  return false;
8326
8569
  }
8570
+ if (realm !== this.AUTH_REALM) {
8571
+ this.rtspDebugLog(
8572
+ `Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
8573
+ );
8574
+ return false;
8575
+ }
8327
8576
  for (const cred of this.authCredentials) {
8328
8577
  if (username !== cred.username) continue;
8329
- const ha1 = this.md5(`${cred.username}:${realm}:${cred.password}`);
8578
+ const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
8579
+ if (!ha1) continue;
8330
8580
  const ha2 = this.md5(`${method}:${authUri || uri}`);
8331
8581
  const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
8332
8582
  if (response === expectedResponse) {
@@ -8355,6 +8605,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8355
8605
  this.noClientAutoStopTimer = void 0;
8356
8606
  }
8357
8607
  }
8608
+ clearNoFrameDeadlineTimer() {
8609
+ if (this.noFrameDeadlineTimer) {
8610
+ clearTimeout(this.noFrameDeadlineTimer);
8611
+ this.noFrameDeadlineTimer = void 0;
8612
+ }
8613
+ }
8358
8614
  setFlowVideoType(videoType, reason) {
8359
8615
  if (this.flow.videoType === videoType) return;
8360
8616
  const transport = this.api.client.getTransport();
@@ -8369,25 +8625,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8369
8625
  if (this.active) {
8370
8626
  throw new Error("RTSP server is already active");
8371
8627
  }
8372
- try {
8373
- const metadata = await this.api.getStreamMetadata(this.channel);
8374
- const stream = metadata.streams.find((s) => s.profile === this.profile);
8375
- if (stream) {
8376
- this.streamMetadata = {
8377
- frameRate: stream.frameRate || 25,
8378
- width: stream.width,
8379
- height: stream.height
8380
- };
8381
- const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
8382
- const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
8383
- this.setFlowVideoType(metaVideoType, "metadata");
8384
- }
8385
- } catch (error) {
8386
- this.logger.warn(
8387
- `[BaichuanRtspServer] Could not get stream metadata: ${error}`
8628
+ if (this.lazyMetadata) {
8629
+ this.logger.info(
8630
+ `[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
8388
8631
  );
8389
- this.streamMetadata = { frameRate: 25 };
8390
- this.setFlowVideoType("H264", "metadata unavailable");
8632
+ } else {
8633
+ try {
8634
+ const metadata = await this.api.getStreamMetadata(this.channel);
8635
+ const stream = metadata.streams.find((s) => s.profile === this.profile);
8636
+ if (stream) {
8637
+ this.streamMetadata = {
8638
+ frameRate: stream.frameRate || 25,
8639
+ width: stream.width,
8640
+ height: stream.height
8641
+ };
8642
+ const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
8643
+ const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
8644
+ this.setFlowVideoType(metaVideoType, "metadata");
8645
+ }
8646
+ } catch (error) {
8647
+ this.logger.warn(
8648
+ `[BaichuanRtspServer] Could not get stream metadata: ${error}`
8649
+ );
8650
+ this.streamMetadata = { frameRate: 25 };
8651
+ this.setFlowVideoType("H264", "metadata unavailable");
8652
+ }
8391
8653
  }
8392
8654
  if (!this.externalListener) {
8393
8655
  this.clientConnectionServer = net.createServer((socket) => {
@@ -8428,6 +8690,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8428
8690
  }
8429
8691
  this.handleRtspConnection(socket, initialBuffer);
8430
8692
  }
8693
+ /**
8694
+ * Inject an already-accepted client socket from a multiplexer
8695
+ * (e.g. `LocalRtspMux`) that owns the listening port.
8696
+ *
8697
+ * The mux reads the first RTSP request line to determine the target path,
8698
+ * then hands the socket over. Any bytes already consumed during routing
8699
+ * are replayed back onto the socket via `unshift()` so the RTSP parser in
8700
+ * `handleRtspConnection` sees the complete original request.
8701
+ *
8702
+ * @param socket - Client TCP socket, already accepted by the mux.
8703
+ * @param preReadData - Bytes the mux has already pulled off the socket
8704
+ * while parsing the request line. Replayed via `socket.unshift()`
8705
+ * before any further reads.
8706
+ */
8707
+ injectSocket(socket, preReadData) {
8708
+ if (!this.active) {
8709
+ socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
8710
+ return;
8711
+ }
8712
+ if (preReadData && preReadData.length > 0) {
8713
+ socket.unshift(preReadData);
8714
+ }
8715
+ this.handleRtspConnection(socket);
8716
+ }
8431
8717
  /**
8432
8718
  * Handle RTSP connection from a client.
8433
8719
  */
@@ -8592,6 +8878,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8592
8878
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
8593
8879
  });
8594
8880
  } else if (method === "DESCRIBE") {
8881
+ if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
8882
+ void this.api.ensureConnected().catch(() => {
8883
+ });
8884
+ }
8595
8885
  if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
8596
8886
  try {
8597
8887
  if (!this.nativeStreamActive) {
@@ -8629,6 +8919,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8629
8919
  }
8630
8920
  }
8631
8921
  }
8922
+ if (!this.hasAudio && this.firstAudioPromise) {
8923
+ const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
8924
+ const audioPrimingStart = Date.now();
8925
+ try {
8926
+ await Promise.race([
8927
+ this.firstAudioPromise,
8928
+ new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
8929
+ ]);
8930
+ } catch {
8931
+ }
8932
+ const audioPrimingElapsed = Date.now() - audioPrimingStart;
8933
+ if (this.hasAudio) {
8934
+ this.logger.info(
8935
+ `[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
8936
+ );
8937
+ } else {
8938
+ this.logger.info(
8939
+ `[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
8940
+ );
8941
+ }
8942
+ }
8632
8943
  {
8633
8944
  const { fmtp, hasParamSets } = this.flow.getFmtp();
8634
8945
  const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
@@ -8637,12 +8948,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8637
8948
  );
8638
8949
  }
8639
8950
  const sdp = this.generateSdp();
8951
+ const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
8640
8952
  sendResponse(
8641
8953
  200,
8642
8954
  "OK",
8643
8955
  {
8644
8956
  "Content-Type": "application/sdp",
8645
- "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
8957
+ "Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
8646
8958
  },
8647
8959
  sdp
8648
8960
  );
@@ -8665,7 +8977,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8665
8977
  this.emit("client", clientId);
8666
8978
  this.clearNoClientAutoStopTimer();
8667
8979
  if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
8668
- await this.startNativeStream();
8980
+ void this.startNativeStream();
8669
8981
  }
8670
8982
  const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
8671
8983
  const transport = (transportMatch?.[1] ?? "").trim();
@@ -8799,12 +9111,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8799
9111
  }
8800
9112
  }
8801
9113
  };
9114
+ const runProcessBuffer = () => {
9115
+ processBuffer().catch((err) => {
9116
+ this.logger.debug(
9117
+ `[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
9118
+ );
9119
+ try {
9120
+ socket.destroy();
9121
+ } catch {
9122
+ }
9123
+ });
9124
+ };
8802
9125
  socket.on("data", (data) => {
8803
9126
  buffer = Buffer.concat([buffer, data]);
8804
- void processBuffer();
9127
+ runProcessBuffer();
8805
9128
  });
8806
9129
  if (buffer.includes("\r\n\r\n")) {
8807
- void processBuffer();
9130
+ runProcessBuffer();
8808
9131
  }
8809
9132
  }
8810
9133
  /**
@@ -9672,6 +9995,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9672
9995
  if (this.nativeStreamActive) {
9673
9996
  return;
9674
9997
  }
9998
+ if (!this.api.isReady) {
9999
+ if (this.api.isClosed) {
10000
+ this.logger.warn?.(
10001
+ `[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
10002
+ );
10003
+ return;
10004
+ }
10005
+ try {
10006
+ this.logger.info?.(
10007
+ `[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
10008
+ );
10009
+ await this.api.ensureConnected();
10010
+ } catch (e) {
10011
+ this.logger.warn?.(
10012
+ `[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
10013
+ );
10014
+ return;
10015
+ }
10016
+ }
9675
10017
  this.nativeStreamActive = true;
9676
10018
  this.firstFrameReceived = false;
9677
10019
  this.firstAudioDetected = false;
@@ -9706,13 +10048,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9706
10048
  await this.flow.startKeepAlive(this.api);
9707
10049
  this.nativeFanout = new NativeStreamFanout({
9708
10050
  maxQueueItems: 200,
9709
- createSource: () => createNativeStream(this.api, this.channel, this.profile, {
10051
+ createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
9710
10052
  variant: this.variant,
9711
- ...dedicatedClient ? { client: dedicatedClient } : {}
10053
+ ...dedicatedClient ? { client: dedicatedClient } : {},
10054
+ signal
9712
10055
  }),
9713
10056
  onFrame: (frame) => {
9714
10057
  if (frame.audio) {
9715
- if (!this.hasAudio && this.api.client.getTransport() === "tcp" && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
10058
+ if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
9716
10059
  const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
9717
10060
  if (info) {
9718
10061
  this.hasAudio = true;
@@ -9761,6 +10104,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9761
10104
  onEnd: () => {
9762
10105
  if (!this.nativeStreamActive) return;
9763
10106
  this.nativeStreamActive = false;
10107
+ this.clearNoFrameDeadlineTimer();
10108
+ const hadFrames = this.firstFrameReceived;
9764
10109
  this.firstFrameReceived = false;
9765
10110
  this.firstFramePromise = null;
9766
10111
  this.firstFrameResolve = null;
@@ -9785,7 +10130,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9785
10130
  } catch {
9786
10131
  }
9787
10132
  }
9788
- if (this.connectedClients.size > 0) {
10133
+ if (this.connectedClients.size > 0 && hadFrames) {
9789
10134
  this.logger.info(
9790
10135
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
9791
10136
  );
@@ -9797,6 +10142,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9797
10142
  }
9798
10143
  });
9799
10144
  this.nativeFanout.start();
10145
+ this.clearNoFrameDeadlineTimer();
10146
+ if (this.nativeStreamNoFrameDeadlineMs > 0) {
10147
+ this.noFrameDeadlineTimer = setTimeout(() => {
10148
+ this.noFrameDeadlineTimer = void 0;
10149
+ if (!this.firstFrameReceived && this.nativeStreamActive) {
10150
+ this.logger.info(
10151
+ `[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
10152
+ );
10153
+ void this.stopNativeStream();
10154
+ }
10155
+ }, this.nativeStreamNoFrameDeadlineMs);
10156
+ this.noFrameDeadlineTimer?.unref?.();
10157
+ }
9800
10158
  this.clearNoClientAutoStopTimer();
9801
10159
  if (this.nativeStreamPrimeIdleStopMs > 0) {
9802
10160
  this.noClientAutoStopTimer = setTimeout(() => {
@@ -9813,6 +10171,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9813
10171
  markFirstFrameReceived() {
9814
10172
  if (!this.firstFrameReceived && this.firstFrameResolve) {
9815
10173
  this.firstFrameReceived = true;
10174
+ this.clearNoFrameDeadlineTimer();
9816
10175
  this.rtspDebugLog(
9817
10176
  `First frame received from camera for profile ${this.profile}`
9818
10177
  );
@@ -9839,6 +10198,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9839
10198
  );
9840
10199
  this.flow.stopKeepAlive();
9841
10200
  this.clearNoClientAutoStopTimer();
10201
+ this.clearNoFrameDeadlineTimer();
9842
10202
  this.nativeStreamActive = false;
9843
10203
  this.firstFrameReceived = false;
9844
10204
  this.firstFramePromise = null;
@@ -10055,149 +10415,17 @@ init_BcMediaAnnexBDecoder();
10055
10415
  // src/baichuan/stream/MpegTsMuxer.ts
10056
10416
  var TS_PACKET_SIZE = 188;
10057
10417
  var TS_SYNC_BYTE = 71;
10058
- var PAT_PID = 0;
10059
- var PMT_PID = 4096;
10060
- var VIDEO_PID = 256;
10418
+ var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
10419
+ var PID_PAT = 0;
10420
+ var PID_PMT = 4096;
10421
+ var PID_VIDEO = 256;
10422
+ var PID_AUDIO = 257;
10061
10423
  var STREAM_TYPE_H264 = 27;
10062
10424
  var STREAM_TYPE_H265 = 36;
10063
- var patCc = 0;
10064
- var pmtCc = 0;
10065
- var videoCc = 0;
10066
- function createPat() {
10067
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
10068
- packet[0] = TS_SYNC_BYTE;
10069
- packet[1] = 64 | PAT_PID >> 8 & 31;
10070
- packet[2] = PAT_PID & 255;
10071
- packet[3] = 16 | patCc & 15;
10072
- patCc = patCc + 1 & 15;
10073
- packet[4] = 0;
10074
- let idx = 5;
10075
- packet[idx++] = 0;
10076
- packet[idx++] = 176;
10077
- packet[idx++] = 13;
10078
- packet[idx++] = 0;
10079
- packet[idx++] = 1;
10080
- packet[idx++] = 193;
10081
- packet[idx++] = 0;
10082
- packet[idx++] = 0;
10083
- packet[idx++] = 0;
10084
- packet[idx++] = 1;
10085
- packet[idx++] = 224 | PMT_PID >> 8 & 31;
10086
- packet[idx++] = PMT_PID & 255;
10087
- const crc = crc32Mpeg(packet.subarray(5, idx));
10088
- packet.writeUInt32BE(crc, idx);
10089
- return packet;
10090
- }
10091
- function createPmt(streamType) {
10092
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
10093
- packet[0] = TS_SYNC_BYTE;
10094
- packet[1] = 64 | PMT_PID >> 8 & 31;
10095
- packet[2] = PMT_PID & 255;
10096
- packet[3] = 16 | pmtCc & 15;
10097
- pmtCc = pmtCc + 1 & 15;
10098
- packet[4] = 0;
10099
- let idx = 5;
10100
- packet[idx++] = 2;
10101
- packet[idx++] = 176;
10102
- packet[idx++] = 18;
10103
- packet[idx++] = 0;
10104
- packet[idx++] = 1;
10105
- packet[idx++] = 193;
10106
- packet[idx++] = 0;
10107
- packet[idx++] = 0;
10108
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
10109
- packet[idx++] = VIDEO_PID & 255;
10110
- packet[idx++] = 240;
10111
- packet[idx++] = 0;
10112
- packet[idx++] = streamType;
10113
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
10114
- packet[idx++] = VIDEO_PID & 255;
10115
- packet[idx++] = 240;
10116
- packet[idx++] = 0;
10117
- const crc = crc32Mpeg(packet.subarray(5, idx));
10118
- packet.writeUInt32BE(crc, idx);
10119
- return packet;
10120
- }
10121
- function createVideoPes(data, pts, isKeyframe) {
10122
- const packets = [];
10123
- const pts90k = Math.floor(pts * 9e4 / 1e6);
10124
- const pesHeaderLen = 14;
10125
- const pesHeader = Buffer.alloc(pesHeaderLen);
10126
- let idx = 0;
10127
- pesHeader[idx++] = 0;
10128
- pesHeader[idx++] = 0;
10129
- pesHeader[idx++] = 1;
10130
- pesHeader[idx++] = 224;
10131
- pesHeader[idx++] = 0;
10132
- pesHeader[idx++] = 0;
10133
- pesHeader[idx++] = 128;
10134
- pesHeader[idx++] = 128;
10135
- pesHeader[idx++] = 5;
10136
- pesHeader[idx++] = 33 | pts90k >> 29 & 14;
10137
- pesHeader[idx++] = pts90k >> 22 & 255;
10138
- pesHeader[idx++] = 1 | pts90k >> 14 & 254;
10139
- pesHeader[idx++] = pts90k >> 7 & 255;
10140
- pesHeader[idx++] = 1 | pts90k << 1 & 254;
10141
- const pesData = Buffer.concat([pesHeader, data]);
10142
- let pesOffset = 0;
10143
- let isFirst = true;
10144
- while (pesOffset < pesData.length) {
10145
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
10146
- let pktIdx = 0;
10147
- packet[pktIdx++] = TS_SYNC_BYTE;
10148
- packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
10149
- packet[pktIdx++] = VIDEO_PID & 255;
10150
- const remaining = pesData.length - pesOffset;
10151
- const maxPayload = TS_PACKET_SIZE - 4;
10152
- if (remaining >= maxPayload) {
10153
- packet[pktIdx++] = 16 | videoCc & 15;
10154
- videoCc = videoCc + 1 & 15;
10155
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
10156
- pesOffset += maxPayload;
10157
- } else {
10158
- const adaptLen = maxPayload - remaining - 1;
10159
- if (adaptLen < 0) {
10160
- packet[pktIdx++] = 48 | videoCc & 15;
10161
- videoCc = videoCc + 1 & 15;
10162
- packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
10163
- if (isFirst && isKeyframe) {
10164
- packet[pktIdx++] = 64;
10165
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
10166
- packet[i] = 255;
10167
- }
10168
- } else {
10169
- packet[pktIdx++] = 0;
10170
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
10171
- packet[i] = 255;
10172
- }
10173
- }
10174
- pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
10175
- pesOffset += remaining;
10176
- } else {
10177
- packet[pktIdx++] = 48 | videoCc & 15;
10178
- videoCc = videoCc + 1 & 15;
10179
- if (adaptLen === 0) {
10180
- packet[pktIdx++] = 0;
10181
- } else {
10182
- packet[pktIdx++] = adaptLen;
10183
- if (isFirst && isKeyframe) {
10184
- packet[pktIdx++] = 64;
10185
- } else {
10186
- packet[pktIdx++] = 0;
10187
- }
10188
- for (let i = 0; i < adaptLen - 1; i++) {
10189
- packet[pktIdx++] = 255;
10190
- }
10191
- }
10192
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
10193
- pesOffset += remaining;
10194
- }
10195
- }
10196
- packets.push(packet);
10197
- isFirst = false;
10198
- }
10199
- return packets;
10200
- }
10425
+ var STREAM_TYPE_AAC = 15;
10426
+ var PES_STREAM_ID_VIDEO = 224;
10427
+ var PES_STREAM_ID_AUDIO = 192;
10428
+ var PAT_PMT_INTERVAL = 40;
10201
10429
  function crc32Mpeg(data) {
10202
10430
  let crc = 4294967295;
10203
10431
  for (let i = 0; i < data.length; i++) {
@@ -10212,45 +10440,218 @@ function crc32Mpeg(data) {
10212
10440
  }
10213
10441
  return crc >>> 0;
10214
10442
  }
10443
+ function usToPts(us) {
10444
+ return Math.floor(us * 90 / 1e3) & 8589934591;
10445
+ }
10446
+ function encodePts(buf, offset, pts, prefix) {
10447
+ buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
10448
+ buf[offset + 1] = pts >>> 22 & 255;
10449
+ buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
10450
+ buf[offset + 3] = pts >>> 7 & 255;
10451
+ buf[offset + 4] = (pts & 127) << 1 | 1;
10452
+ }
10453
+ function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
10454
+ buf[0] = TS_SYNC_BYTE;
10455
+ buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
10456
+ buf[2] = pid & 255;
10457
+ buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
10458
+ }
10459
+ function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
10460
+ const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
10461
+ const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
10462
+ let pesOffset = 0;
10463
+ let outOffset = 0;
10464
+ let isFirst = true;
10465
+ while (pesOffset < pesData.length) {
10466
+ const remaining = pesData.length - pesOffset;
10467
+ const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
10468
+ outOffset += TS_PACKET_SIZE;
10469
+ if (remaining >= TS_PAYLOAD_SIZE) {
10470
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
10471
+ ccRef.cc = ccRef.cc + 1 & 15;
10472
+ pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
10473
+ pesOffset += TS_PAYLOAD_SIZE;
10474
+ } else {
10475
+ const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
10476
+ if (paddingNeeded === 1) {
10477
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
10478
+ ccRef.cc = ccRef.cc + 1 & 15;
10479
+ packet[4] = 0;
10480
+ pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
10481
+ } else {
10482
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
10483
+ ccRef.cc = ccRef.cc + 1 & 15;
10484
+ const adaptLen = paddingNeeded - 1;
10485
+ packet[4] = adaptLen;
10486
+ packet[5] = isFirst && isKeyframe ? 64 : 0;
10487
+ packet.fill(255, 6, 4 + paddingNeeded);
10488
+ pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
10489
+ }
10490
+ pesOffset += remaining;
10491
+ }
10492
+ isFirst = false;
10493
+ }
10494
+ return out;
10495
+ }
10496
+ function buildPat(cc) {
10497
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
10498
+ pkt[0] = TS_SYNC_BYTE;
10499
+ pkt[1] = 64 | PID_PAT >> 8 & 31;
10500
+ pkt[2] = PID_PAT & 255;
10501
+ pkt[3] = 16 | cc & 15;
10502
+ pkt[4] = 0;
10503
+ const sectionStart = 5;
10504
+ let i = sectionStart;
10505
+ pkt[i++] = 0;
10506
+ pkt[i++] = 176;
10507
+ pkt[i++] = 13;
10508
+ pkt[i++] = 0;
10509
+ pkt[i++] = 1;
10510
+ pkt[i++] = 193;
10511
+ pkt[i++] = 0;
10512
+ pkt[i++] = 0;
10513
+ pkt[i++] = 0;
10514
+ pkt[i++] = 1;
10515
+ pkt[i++] = 224 | PID_PMT >> 8 & 31;
10516
+ pkt[i++] = PID_PMT & 255;
10517
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
10518
+ pkt.writeUInt32BE(crc, i);
10519
+ return pkt;
10520
+ }
10521
+ function buildPmt(videoStreamType, includeAudio, cc) {
10522
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
10523
+ pkt[0] = TS_SYNC_BYTE;
10524
+ pkt[1] = 64 | PID_PMT >> 8 & 31;
10525
+ pkt[2] = PID_PMT & 255;
10526
+ pkt[3] = 16 | cc & 15;
10527
+ pkt[4] = 0;
10528
+ const sectionStart = 5;
10529
+ let i = sectionStart;
10530
+ pkt[i++] = 2;
10531
+ pkt[i++] = 176;
10532
+ const sectionLenPos = i;
10533
+ i += 1;
10534
+ pkt[i++] = 0;
10535
+ pkt[i++] = 1;
10536
+ pkt[i++] = 193;
10537
+ pkt[i++] = 0;
10538
+ pkt[i++] = 0;
10539
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
10540
+ pkt[i++] = PID_VIDEO & 255;
10541
+ pkt[i++] = 240;
10542
+ pkt[i++] = 0;
10543
+ pkt[i++] = videoStreamType;
10544
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
10545
+ pkt[i++] = PID_VIDEO & 255;
10546
+ pkt[i++] = 240;
10547
+ pkt[i++] = 0;
10548
+ if (includeAudio) {
10549
+ pkt[i++] = STREAM_TYPE_AAC;
10550
+ pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
10551
+ pkt[i++] = PID_AUDIO & 255;
10552
+ pkt[i++] = 240;
10553
+ pkt[i++] = 0;
10554
+ }
10555
+ const sectionLen = i - sectionStart - 3 + 4;
10556
+ pkt[sectionLenPos] = sectionLen;
10557
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
10558
+ pkt.writeUInt32BE(crc, i);
10559
+ return pkt;
10560
+ }
10561
+ function buildVideoPes(annexBData, ptsUs, isKeyframe) {
10562
+ const pts = usToPts(ptsUs);
10563
+ const pesHeader = Buffer.allocUnsafe(14);
10564
+ pesHeader[0] = 0;
10565
+ pesHeader[1] = 0;
10566
+ pesHeader[2] = 1;
10567
+ pesHeader[3] = PES_STREAM_ID_VIDEO;
10568
+ pesHeader[4] = 0;
10569
+ pesHeader[5] = 0;
10570
+ pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
10571
+ pesHeader[7] = 128;
10572
+ pesHeader[8] = 5;
10573
+ encodePts(pesHeader, 9, pts, 2);
10574
+ return Buffer.concat([pesHeader, annexBData]);
10575
+ }
10576
+ function buildAudioPes(adtsData, ptsUs) {
10577
+ const pts = usToPts(ptsUs);
10578
+ const pesPayloadLen = 8 + adtsData.length;
10579
+ const pesHeader = Buffer.allocUnsafe(14);
10580
+ pesHeader[0] = 0;
10581
+ pesHeader[1] = 0;
10582
+ pesHeader[2] = 1;
10583
+ pesHeader[3] = PES_STREAM_ID_AUDIO;
10584
+ pesHeader[4] = pesPayloadLen >> 8 & 255;
10585
+ pesHeader[5] = pesPayloadLen & 255;
10586
+ pesHeader[6] = 128;
10587
+ pesHeader[7] = 128;
10588
+ pesHeader[8] = 5;
10589
+ encodePts(pesHeader, 9, pts, 2);
10590
+ return Buffer.concat([pesHeader, adtsData]);
10591
+ }
10215
10592
  var MpegTsMuxer = class {
10216
- streamType;
10217
- patSent = false;
10218
- pmtSent = false;
10219
- patPmtInterval = 0;
10220
- patPmtIntervalMax = 40;
10221
- // Send PAT/PMT every ~40 frames
10593
+ videoStreamType;
10594
+ includeAudio;
10595
+ // Per-instance continuity counters (4-bit, wrap at 16)
10596
+ patCc = 0;
10597
+ pmtCc = 0;
10598
+ videoCc = 0;
10599
+ audioCc = 0;
10600
+ framesSinceTableSend = 0;
10601
+ tablesSent = false;
10222
10602
  constructor(options) {
10223
- this.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
10603
+ this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
10604
+ this.includeAudio = options.includeAudio ?? true;
10224
10605
  }
10225
10606
  /**
10226
- * Reset continuity counters (call when starting a new stream).
10607
+ * Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
10608
+ * PAT and PMT are emitted before keyframes and periodically.
10609
+ *
10610
+ * @param annexBData - Annex-B video data (with start codes)
10611
+ * @param ptsUs - Presentation timestamp in microseconds
10612
+ * @param isKeyframe - Whether this is an IDR / IRAP frame
10227
10613
  */
10228
- static resetCounters() {
10229
- patCc = 0;
10230
- pmtCc = 0;
10231
- videoCc = 0;
10232
- }
10233
- /**
10234
- * Mux a video frame into MPEG-TS packets.
10614
+ muxVideo(annexBData, ptsUs, isKeyframe) {
10615
+ const chunks = [];
10616
+ const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
10617
+ if (needTables) {
10618
+ chunks.push(buildPat(this.patCc));
10619
+ this.patCc = this.patCc + 1 & 15;
10620
+ chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
10621
+ this.pmtCc = this.pmtCc + 1 & 15;
10622
+ this.tablesSent = true;
10623
+ this.framesSinceTableSend = 0;
10624
+ }
10625
+ this.framesSinceTableSend++;
10626
+ const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
10627
+ const ccRef = { cc: this.videoCc };
10628
+ chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
10629
+ this.videoCc = ccRef.cc;
10630
+ return Buffer.concat(chunks);
10631
+ }
10632
+ /**
10633
+ * Mux an audio frame (ADTS AAC) into MPEG-TS packets.
10634
+ * Returns an empty Buffer when includeAudio is false.
10235
10635
  *
10236
- * @param data - Annex-B video data (with start codes)
10237
- * @param microseconds - Frame timestamp in microseconds
10238
- * @param isKeyframe - Whether this is a keyframe
10239
- * @returns Buffer containing all TS packets for this frame
10240
- */
10241
- mux(data, microseconds, isKeyframe) {
10242
- const packets = [];
10243
- if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
10244
- packets.push(createPat());
10245
- packets.push(createPmt(this.streamType));
10246
- this.patSent = true;
10247
- this.pmtSent = true;
10248
- this.patPmtInterval = 0;
10249
- }
10250
- this.patPmtInterval++;
10251
- const videoPackets = createVideoPes(data, microseconds, isKeyframe);
10252
- packets.push(...videoPackets);
10253
- return Buffer.concat(packets);
10636
+ * @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
10637
+ * @param ptsUs - Presentation timestamp in microseconds
10638
+ */
10639
+ muxAudio(adtsData, ptsUs) {
10640
+ if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
10641
+ const pes = buildAudioPes(adtsData, ptsUs);
10642
+ const ccRef = { cc: this.audioCc };
10643
+ const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
10644
+ this.audioCc = ccRef.cc;
10645
+ return result;
10646
+ }
10647
+ /** Reset all continuity counters and table state (e.g. after stream restart). */
10648
+ reset() {
10649
+ this.patCc = 0;
10650
+ this.pmtCc = 0;
10651
+ this.videoCc = 0;
10652
+ this.audioCc = 0;
10653
+ this.framesSinceTableSend = 0;
10654
+ this.tablesSent = false;
10254
10655
  }
10255
10656
  };
10256
10657
 
@@ -15526,6 +15927,53 @@ var getAiStateViaGetAiAlarm = async (params) => {
15526
15927
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "getAiState failed"));
15527
15928
  };
15528
15929
 
15930
+ // src/reolink/baichuan/utils/sleepInference.ts
15931
+ function decideSleepInferenceTransition(input) {
15932
+ const { inferred, committed, pending, hysteresisPolls } = input;
15933
+ if (committed === void 0) {
15934
+ return {
15935
+ emit: inferred === "sleeping" ? "sleeping" : null,
15936
+ nextCommitted: inferred,
15937
+ nextPending: void 0
15938
+ };
15939
+ }
15940
+ if (inferred === committed) {
15941
+ return {
15942
+ emit: null,
15943
+ nextCommitted: committed,
15944
+ nextPending: void 0
15945
+ };
15946
+ }
15947
+ const effectivePolls = Math.max(1, hysteresisPolls);
15948
+ if (!pending || pending.state !== inferred) {
15949
+ if (effectivePolls <= 1) {
15950
+ return {
15951
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
15952
+ nextCommitted: inferred,
15953
+ nextPending: void 0
15954
+ };
15955
+ }
15956
+ return {
15957
+ emit: null,
15958
+ nextCommitted: committed,
15959
+ nextPending: { state: inferred, count: 1 }
15960
+ };
15961
+ }
15962
+ const nextCount = pending.count + 1;
15963
+ if (nextCount < effectivePolls) {
15964
+ return {
15965
+ emit: null,
15966
+ nextCommitted: committed,
15967
+ nextPending: { state: inferred, count: nextCount }
15968
+ };
15969
+ }
15970
+ return {
15971
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
15972
+ nextCommitted: inferred,
15973
+ nextPending: void 0
15974
+ };
15975
+ }
15976
+
15529
15977
  // src/reolink/baichuan/utils/channelInfoPush.ts
15530
15978
  init_xml();
15531
15979
  var parseOptionalInt = (value) => {
@@ -17802,7 +18250,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17802
18250
  statePollingInterval;
17803
18251
  udpSleepInferenceInterval;
17804
18252
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
18253
+ /**
18254
+ * Per-channel pending sleep-state candidate for hysteresis.
18255
+ * When the inference flips to a new state we require N consecutive polls
18256
+ * of that same state before committing it — this filters out transient
18257
+ * flapping caused by non-waking traffic drifting in/out of the 10 s
18258
+ * getSleepStatus() observation window during stream teardown.
18259
+ */
18260
+ udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
17805
18261
  udpSleepInferenceIntervalMs = 2e3;
18262
+ /** Consecutive inference polls required to commit a new sleeping/awake state. */
18263
+ udpSleepInferenceHysteresisPolls = 2;
17806
18264
  lastMotionState;
17807
18265
  lastAiState;
17808
18266
  aiStatePollingDisabled = false;
@@ -18269,6 +18727,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18269
18727
  */
18270
18728
  attachD2cDiscListener(client) {
18271
18729
  client.on("d2c_disc", () => this.notifyD2cDisc());
18730
+ client.on("error", () => {
18731
+ });
18272
18732
  }
18273
18733
  /**
18274
18734
  * Acquire a socket from the pool by tag.
@@ -18387,6 +18847,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18387
18847
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
18388
18848
  const newClient = new BaichuanClient(clientOpts);
18389
18849
  this.attachD2cDiscListener(newClient);
18850
+ newClient.on("error", (err) => {
18851
+ log?.debug?.(
18852
+ `[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
18853
+ );
18854
+ });
18390
18855
  await newClient.login();
18391
18856
  const existingCooldown = this.socketPoolCooldowns.get(this.host);
18392
18857
  if (existingCooldown) {
@@ -18902,6 +19367,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18902
19367
  * Only counts sessions from our own IP address.
18903
19368
  */
18904
19369
  async maybeRebootOnTooManySessions() {
19370
+ if (!this.client.isSocketConnected?.()) return;
18905
19371
  const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
18906
19372
  if (this.sessionGuardRebootInFlight) return;
18907
19373
  const cooldownMs = 10 * 6e4;
@@ -19343,6 +19809,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19343
19809
  }
19344
19810
  async renewSimpleEventSubscription() {
19345
19811
  if (this.simpleEventListeners.size === 0) return;
19812
+ if (!this.client.isSocketConnected?.()) return;
19346
19813
  if (this.simpleEventResubscribeInFlight)
19347
19814
  return await this.simpleEventResubscribeInFlight;
19348
19815
  this.simpleEventResubscribeInFlight = (async () => {
@@ -23110,23 +23577,32 @@ ${stderr}`)
23110
23577
  return;
23111
23578
  }
23112
23579
  const channel = this.client.getConfiguredChannel?.() ?? 0;
23580
+ if (!this.client.isSocketConnected?.()) {
23581
+ this.udpPendingSleepStateByChannel.delete(channel);
23582
+ return;
23583
+ }
23113
23584
  const status = this.getSleepStatus({ channel });
23114
23585
  if (status.state === "unknown") return;
23115
- const prev = this.udpLastInferredSleepStateByChannel.get(channel);
23116
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
23117
- if (prev === void 0) {
23118
- if (status.state === "sleeping") {
23119
- this.dispatchSimpleEvent({
23120
- type: "sleeping",
23121
- channel,
23122
- timestamp: Date.now()
23123
- });
23124
- }
23125
- return;
23586
+ const committed = this.udpLastInferredSleepStateByChannel.get(channel);
23587
+ const pending = this.udpPendingSleepStateByChannel.get(channel);
23588
+ const decision = decideSleepInferenceTransition({
23589
+ inferred: status.state,
23590
+ committed,
23591
+ pending,
23592
+ hysteresisPolls: this.udpSleepInferenceHysteresisPolls
23593
+ });
23594
+ this.udpLastInferredSleepStateByChannel.set(
23595
+ channel,
23596
+ decision.nextCommitted
23597
+ );
23598
+ if (decision.nextPending === void 0) {
23599
+ this.udpPendingSleepStateByChannel.delete(channel);
23600
+ } else {
23601
+ this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
23126
23602
  }
23127
- if (prev !== status.state) {
23603
+ if (decision.emit) {
23128
23604
  this.dispatchSimpleEvent({
23129
- type: status.state === "sleeping" ? "sleeping" : "awake",
23605
+ type: decision.emit,
23130
23606
  channel,
23131
23607
  timestamp: Date.now()
23132
23608
  });
@@ -23149,6 +23625,7 @@ ${stderr}`)
23149
23625
  this.udpSleepInferenceInterval = void 0;
23150
23626
  }
23151
23627
  this.udpLastInferredSleepStateByChannel.clear();
23628
+ this.udpPendingSleepStateByChannel.clear();
23152
23629
  }
23153
23630
  /**
23154
23631
  * GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
@@ -25579,6 +26056,373 @@ ${xml}`
25579
26056
  await this.cgiApi.login();
25580
26057
  return await this.cgiApi.getAllChannelsEvents(options);
25581
26058
  }
26059
+ // ====================================================================
26060
+ // Native Baichuan tunable-settings setters
26061
+ //
26062
+ // Replace the CGI passthroughs above with on-wire Baichuan binary
26063
+ // calls. Mirrors the @http_cmd-decorated methods in reolink_aio's
26064
+ // baichuan.py — every command has a documented `cmd_id` (read) and
26065
+ // `cmd_id` (write) pair. The pattern is:
26066
+ //
26067
+ // 1. read XML via `sendXml({ cmdId: GET, channel })`
26068
+ // 2. patch fields via regex (camera firmware is XML-strict; using
26069
+ // the parser would force us to rebuild the document and risk
26070
+ // losing unmodified attributes / element order).
26071
+ // 3. write back via `sendXml({ cmdId: SET, channel, payloadXml })`
26072
+ //
26073
+ // All getters parse via `parseXmlFragmentToJson` so the consumer gets
26074
+ // a clean JSON object instead of XML.
26075
+ // ====================================================================
26076
+ /**
26077
+ * GetEnc via Baichuan (cmdId=56). Returns the `<Compression>` block:
26078
+ * per-stream `mainStream` / `subStream` / `thirdStream` with `audio`
26079
+ * flag, `width`, `height`, `frame` (NOT `frameRate`), `bitRate`,
26080
+ * `videoEncType` (0=h264, 1=h265), `encoderProfile`, `gop`. Mirrors
26081
+ * reolink_aio's `GetEnc` — note the wire payload wraps everything
26082
+ * in `Compression`, not `Enc`.
26083
+ */
26084
+ async getEnc(channel, options) {
26085
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26086
+ cmdId: BC_CMD_ID_GET_ENC,
26087
+ ...channel != null ? { channel } : {},
26088
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26089
+ });
26090
+ return parseXmlFragmentToJson(xml);
26091
+ }
26092
+ /**
26093
+ * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
26094
+ * unspecified fields. Mirrors reolink_aio's `SetEnc`.
26095
+ *
26096
+ * @param channel - Channel number (0-based)
26097
+ * @param patch - Fields to update on `mainStream` and/or `subStream`,
26098
+ * plus a top-level `audio` toggle (0/1). Pass only what you want
26099
+ * to change.
26100
+ */
26101
+ async setEnc(channel, patch, options) {
26102
+ const ch = this.normalizeChannel(channel);
26103
+ let xml = await this.sendXml({
26104
+ cmdId: BC_CMD_ID_GET_ENC,
26105
+ channel: ch,
26106
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26107
+ });
26108
+ if (patch.audio !== void 0) {
26109
+ xml = xml.replace(
26110
+ /<audio>[^<]*<\/audio>/g,
26111
+ `<audio>${patch.audio}</audio>`
26112
+ );
26113
+ }
26114
+ xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
26115
+ xml = applyStreamPatch(xml, "subStream", patch.subStream);
26116
+ await this.sendXml({
26117
+ cmdId: BC_CMD_ID_SET_ENC,
26118
+ channel: ch,
26119
+ payloadXml: ensureXmlHeader(xml),
26120
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26121
+ });
26122
+ }
26123
+ /**
26124
+ * SetImage via Baichuan (cmdId=25, read via cmdId=26). Patches the
26125
+ * `<VideoInput>` block: bright / contrast / saturation / hue /
26126
+ * sharpen. Mirrors reolink_aio's `SetImage`.
26127
+ */
26128
+ async setImage(channel, patch, options) {
26129
+ const ch = this.normalizeChannel(channel);
26130
+ let xml = await this.sendXml({
26131
+ cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
26132
+ channel: ch,
26133
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26134
+ });
26135
+ xml = applyXmlTagPatch(xml, "bright", patch.bright);
26136
+ xml = applyXmlTagPatch(xml, "contrast", patch.contrast);
26137
+ xml = applyXmlTagPatch(xml, "saturation", patch.saturation);
26138
+ xml = applyXmlTagPatch(xml, "hue", patch.hue);
26139
+ xml = applyXmlTagPatch(xml, "sharpen", patch.sharpen);
26140
+ await this.sendXml({
26141
+ cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
26142
+ channel: ch,
26143
+ payloadXml: ensureXmlHeader(xml),
26144
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26145
+ });
26146
+ }
26147
+ /**
26148
+ * SetIsp via Baichuan (cmdId=25 for image side, cmdId=297 for
26149
+ * dayNightThreshold). Patches the `<InputAdvanceCfg>` block:
26150
+ * `DayNight/mode`, `Exposure/mode`, `binning_mode`, `hdrSwitch`.
26151
+ * Mirrors reolink_aio's `SetIsp`.
26152
+ *
26153
+ * @param channel - Channel number (0-based)
26154
+ * @param patch - Fields to update. `dayNight` accepts the camera's
26155
+ * raw enum (`color`, `auto`, `blackAndWhite`, …) — pass it as the
26156
+ * camera reports it (PascalCase / dotted forms get normalized
26157
+ * server-side).
26158
+ */
26159
+ async setIsp(channel, patch, options) {
26160
+ const ch = this.normalizeChannel(channel);
26161
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26162
+ const wantsImageWrite = patch.dayNight !== void 0 || patch.exposure !== void 0 || patch.binningMode !== void 0 || patch.hdr !== void 0;
26163
+ if (wantsImageWrite) {
26164
+ let xml = await this.sendXml({
26165
+ cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
26166
+ channel: ch,
26167
+ ...timeoutOpts
26168
+ });
26169
+ if (patch.dayNight !== void 0) {
26170
+ const normalized = normalizeDayNightMode(patch.dayNight);
26171
+ xml = patchNestedTag(xml, "DayNight", "mode", normalized);
26172
+ }
26173
+ if (patch.exposure !== void 0) {
26174
+ xml = patchNestedTag(
26175
+ xml,
26176
+ "Exposure",
26177
+ "mode",
26178
+ patch.exposure.toLowerCase()
26179
+ );
26180
+ }
26181
+ if (patch.binningMode !== void 0) {
26182
+ xml = applyXmlTagPatch(xml, "binning_mode", patch.binningMode);
26183
+ }
26184
+ if (patch.hdr !== void 0) {
26185
+ xml = applyXmlTagPatch(xml, "hdrSwitch", patch.hdr);
26186
+ }
26187
+ await this.sendXml({
26188
+ cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
26189
+ channel: ch,
26190
+ payloadXml: ensureXmlHeader(xml),
26191
+ ...timeoutOpts
26192
+ });
26193
+ }
26194
+ if (patch.dayNightThreshold !== void 0) {
26195
+ let xml = await this.sendXml({
26196
+ cmdId: BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
26197
+ channel: ch,
26198
+ ...timeoutOpts
26199
+ });
26200
+ xml = applyXmlTagPatch(xml, "cur", patch.dayNightThreshold);
26201
+ await this.sendXml({
26202
+ cmdId: BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
26203
+ channel: ch,
26204
+ payloadXml: ensureXmlHeader(xml),
26205
+ ...timeoutOpts
26206
+ });
26207
+ }
26208
+ }
26209
+ /**
26210
+ * GetIsp via Baichuan (cmdId=26). Convenience alias of
26211
+ * `getVideoInput()` so callers that switched from CGI keep the
26212
+ * familiar name. Both return the merged VideoInput +
26213
+ * InputAdvanceCfg blob.
26214
+ */
26215
+ async getIsp(channel, options) {
26216
+ return this.getVideoInput(channel, options);
26217
+ }
26218
+ /** GetImage via Baichuan (cmdId=26). Same payload as `getIsp` —
26219
+ * Reolink merged VideoInput + InputAdvanceCfg under one cmdId. */
26220
+ async getImage(channel, options) {
26221
+ return this.getVideoInput(channel, options);
26222
+ }
26223
+ /**
26224
+ * GetIrLights via Baichuan (cmdId=208). Returns LedState block:
26225
+ * `IRLedBrightness`, `state` (ir on/off), `lightState` (status LED
26226
+ * open/close), `doorbellLightState`. Mirrors reolink_aio's
26227
+ * `get_status_led`.
26228
+ */
26229
+ async getIrLights(channel, options) {
26230
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26231
+ cmdId: BC_CMD_ID_GET_LED_STATE,
26232
+ ...channel != null ? { channel } : {},
26233
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26234
+ });
26235
+ return parseXmlFragmentToJson(xml);
26236
+ }
26237
+ /**
26238
+ * SetIrLights via Baichuan (cmdId=209, read via cmdId=208). Patches
26239
+ * IR LED + status LED + doorbell LED + IR brightness. Mirrors
26240
+ * reolink_aio's `set_status_led`.
26241
+ *
26242
+ * @param channel - Channel number (0-based)
26243
+ * @param patch - `irState` ("On" | "Off" | "Auto"), `lightState`
26244
+ * (status LED), `doorbellLightState`, `irBrightness` (0..255).
26245
+ * Camera-side accepts lowercase strings (`open`/`close`); the
26246
+ * helper normalizes from the friendly variants.
26247
+ */
26248
+ async setIrLights(channel, patch, options) {
26249
+ const ch = this.normalizeChannel(channel);
26250
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26251
+ let xml = await this.sendXml({
26252
+ cmdId: BC_CMD_ID_GET_LED_STATE,
26253
+ channel: ch,
26254
+ ...timeoutOpts
26255
+ });
26256
+ if (patch.lightState !== void 0) {
26257
+ xml = applyXmlTagPatch(
26258
+ xml,
26259
+ "lightState",
26260
+ patch.lightState === "On" ? "open" : "close"
26261
+ );
26262
+ }
26263
+ if (patch.doorbellLightState !== void 0) {
26264
+ xml = applyXmlTagPatch(
26265
+ xml,
26266
+ "doorbellLightState",
26267
+ normalizeOpenClose(patch.doorbellLightState)
26268
+ );
26269
+ }
26270
+ if (patch.irState !== void 0) {
26271
+ const v = String(patch.irState);
26272
+ const out = v === "Off" ? "close" : v.toLowerCase();
26273
+ xml = applyXmlTagPatch(xml, "state", out);
26274
+ }
26275
+ if (patch.irBrightness !== void 0) {
26276
+ xml = applyXmlTagPatch(xml, "IRLedBrightness", patch.irBrightness);
26277
+ }
26278
+ await this.sendXml({
26279
+ cmdId: BC_CMD_ID_SET_LED_STATE,
26280
+ channel: ch,
26281
+ payloadXml: ensureXmlHeader(xml),
26282
+ ...timeoutOpts
26283
+ });
26284
+ }
26285
+ /**
26286
+ * SetAudioCfg via Baichuan (cmdId=265, read via cmdId=264). Patches
26287
+ * volume / talk-and-reply / visitor settings. Mirrors reolink_aio's
26288
+ * `SetAudioCfg`.
26289
+ */
26290
+ async setAudioCfg(channel, patch, options) {
26291
+ const ch = this.normalizeChannel(channel);
26292
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26293
+ let xml = await this.sendXml({
26294
+ cmdId: BC_CMD_ID_GET_AUDIO_CFG,
26295
+ channel: ch,
26296
+ ...timeoutOpts
26297
+ });
26298
+ xml = applyXmlTagPatch(xml, "volume", patch.volume);
26299
+ xml = applyXmlTagPatch(
26300
+ xml,
26301
+ "talkAndReplyVolume",
26302
+ patch.talkAndReplyVolume
26303
+ );
26304
+ xml = applyXmlTagPatch(xml, "visitorVolume", patch.visitorVolume);
26305
+ xml = applyXmlTagPatch(xml, "visitorLoudspeaker", patch.visitorLoudspeaker);
26306
+ await this.sendXml({
26307
+ cmdId: BC_CMD_ID_SET_AUDIO_CFG,
26308
+ channel: ch,
26309
+ payloadXml: ensureXmlHeader(xml),
26310
+ ...timeoutOpts
26311
+ });
26312
+ }
26313
+ /**
26314
+ * GetMask (privacy mask) via Baichuan (cmdId=52). Returns the
26315
+ * `<Shelter>` block — `enable` flag + `shelterList`. Mirrors
26316
+ * reolink_aio's `GetMask`.
26317
+ */
26318
+ async getMask(channel, options) {
26319
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26320
+ cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
26321
+ ...channel != null ? { channel } : {},
26322
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26323
+ });
26324
+ return parseXmlFragmentToJson(xml);
26325
+ }
26326
+ /**
26327
+ * SetMask (privacy mask) via Baichuan (cmdId=53, read via cmdId=52).
26328
+ * Toggles the `<Shelter><enable>` flag. Mirrors reolink_aio's
26329
+ * `SetMask` (which only touches enable too — shelter zone editing
26330
+ * goes through a separate flow).
26331
+ */
26332
+ async setMask(channel, patch, options) {
26333
+ const ch = this.normalizeChannel(channel);
26334
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26335
+ let xml = await this.sendXml({
26336
+ cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
26337
+ channel: ch,
26338
+ ...timeoutOpts
26339
+ });
26340
+ if (patch.enable !== void 0) {
26341
+ xml = applyXmlTagPatch(xml, "enable", patch.enable ? 1 : 0);
26342
+ }
26343
+ await this.sendXml({
26344
+ cmdId: BC_CMD_ID_SET_PRIVACY_MASK,
26345
+ channel: ch,
26346
+ payloadXml: ensureXmlHeader(xml),
26347
+ ...timeoutOpts
26348
+ });
26349
+ }
26350
+ /**
26351
+ * GetAudioNoise via Baichuan (cmdId=439). Reads `enable` + `level`
26352
+ * from the aiDenoise block. Mirrors reolink_aio's `GetAudioNoise`.
26353
+ *
26354
+ * Note: `getAiDenoise` already returns the same payload typed as
26355
+ * `AiDenoiseConfig`. This getter exists for naming parity with
26356
+ * reolink_aio + the reolink CGI.
26357
+ */
26358
+ async getAudioNoise(channel, options) {
26359
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26360
+ cmdId: BC_CMD_ID_GET_AI_DENOISE,
26361
+ ...channel != null ? { channel } : {},
26362
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26363
+ });
26364
+ return parseXmlFragmentToJson(xml);
26365
+ }
26366
+ /**
26367
+ * SetAudioNoise via Baichuan (cmdId=440, read via cmdId=439).
26368
+ * Mirrors reolink_aio's `SetAudioNoise` — `level <= 0` flips the
26369
+ * enable flag off; positive values turn it on and update the level.
26370
+ */
26371
+ async setAudioNoise(channel, level, options) {
26372
+ const ch = this.normalizeChannel(channel);
26373
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26374
+ let xml = await this.sendXml({
26375
+ cmdId: BC_CMD_ID_GET_AI_DENOISE,
26376
+ channel: ch,
26377
+ ...timeoutOpts
26378
+ });
26379
+ xml = applyXmlTagPatch(xml, "enable", level > 0 ? 1 : 0);
26380
+ if (level > 0) {
26381
+ xml = applyXmlTagPatch(xml, "level", level);
26382
+ }
26383
+ await this.sendXml({
26384
+ cmdId: BC_CMD_ID_SET_AI_DENOISE,
26385
+ channel: ch,
26386
+ payloadXml: ensureXmlHeader(xml),
26387
+ ...timeoutOpts
26388
+ });
26389
+ }
26390
+ /**
26391
+ * GetAutoFocus via Baichuan (cmdId=224). Returns the `<AutoFocus>`
26392
+ * block — only `disable` (0 = AF on, 1 = AF off). Mirrors
26393
+ * reolink_aio's `GetAutoFocus`.
26394
+ */
26395
+ async getAutoFocus(channel, options) {
26396
+ const ch = this.normalizeChannel(channel);
26397
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26398
+ cmdId: BC_CMD_ID_GET_AUTO_FOCUS,
26399
+ channel: ch,
26400
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26401
+ });
26402
+ return parseXmlFragmentToJson(xml);
26403
+ }
26404
+ /**
26405
+ * SetAutoFocus via Baichuan (cmdId=225). Mirrors reolink_aio's
26406
+ * `SetAutoFocus`. Note: write-only command — the payload is built
26407
+ * from scratch (no read-modify-write needed).
26408
+ */
26409
+ async setAutoFocus(channel, disable, options) {
26410
+ const ch = this.normalizeChannel(channel);
26411
+ const disableVal = disable ? 1 : 0;
26412
+ const payloadXml = `<?xml version="1.0" encoding="UTF-8" ?>
26413
+ <body>
26414
+ <AutoFocus version="1.1">
26415
+ <channelId>${ch}</channelId>
26416
+ <disable>${disableVal}</disable>
26417
+ </AutoFocus>
26418
+ </body>`;
26419
+ await this.sendXml({
26420
+ cmdId: BC_CMD_ID_SET_AUTO_FOCUS,
26421
+ channel: ch,
26422
+ payloadXml,
26423
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26424
+ });
26425
+ }
25582
26426
  /**
25583
26427
  * Passthrough to ReolinkCgiApi.getAllChannelsBatteryInfo.
25584
26428
  * Fetches battery info for all channels via CGI (merged with channel status sleep flag).
@@ -26747,8 +27591,8 @@ ${scheduleItems}
26747
27591
  );
26748
27592
  let args;
26749
27593
  if (useMpegTsMuxer) {
26750
- MpegTsMuxer.resetCounters();
26751
- tsMuxer = new MpegTsMuxer({ videoType });
27594
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
27595
+ tsMuxer.reset();
26752
27596
  args = [
26753
27597
  "-hide_banner",
26754
27598
  "-loglevel",
@@ -26912,7 +27756,7 @@ ${scheduleItems}
26912
27756
  startFfmpeg(videoType);
26913
27757
  frameCount++;
26914
27758
  if (useMpegTsMuxer && tsMuxer) {
26915
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
27759
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
26916
27760
  input.write(tsData);
26917
27761
  } else {
26918
27762
  if (videoType === "H264") input.write(H264_AUD);
@@ -27201,8 +28045,8 @@ ${scheduleItems}
27201
28045
  logger?.log?.(
27202
28046
  `[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
27203
28047
  );
27204
- MpegTsMuxer.resetCounters();
27205
- tsMuxer = new MpegTsMuxer({ videoType });
28048
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
28049
+ tsMuxer.reset();
27206
28050
  const args = [
27207
28051
  "-hide_banner",
27208
28052
  "-loglevel",
@@ -27367,7 +28211,7 @@ ${scheduleItems}
27367
28211
  startFfmpeg(videoType);
27368
28212
  frameCount++;
27369
28213
  if (tsMuxer) {
27370
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
28214
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
27371
28215
  input.write(tsData);
27372
28216
  }
27373
28217
  if (frameCount === 1) {
@@ -28202,9 +29046,10 @@ async function autoDetectDeviceType(inputs) {
28202
29046
  const msg = fmtErr(e);
28203
29047
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
28204
29048
  };
28205
- const withRetries = async (label, max, op, shouldRetry) => {
29049
+ const withRetries = async (label, max, op, shouldRetry, isAborted) => {
28206
29050
  let lastErr;
28207
29051
  for (let attempt = 1; attempt <= max; attempt++) {
29052
+ if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
28208
29053
  try {
28209
29054
  if (attempt > 1) {
28210
29055
  logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
@@ -28213,7 +29058,7 @@ async function autoDetectDeviceType(inputs) {
28213
29058
  } catch (e) {
28214
29059
  lastErr = e;
28215
29060
  const msg = fmtErr(e);
28216
- const retryable = attempt < max && shouldRetry(e);
29061
+ const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
28217
29062
  logger?.log?.(
28218
29063
  `[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
28219
29064
  );
@@ -28223,6 +29068,31 @@ async function autoDetectDeviceType(inputs) {
28223
29068
  }
28224
29069
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
28225
29070
  };
29071
+ const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
29072
+ let raceWon = false;
29073
+ const methodErrors = /* @__PURE__ */ new Map();
29074
+ try {
29075
+ return await Promise.any(
29076
+ methods.map(async (m) => {
29077
+ try {
29078
+ const result = await loginAndDetect(m, () => raceWon);
29079
+ raceWon = true;
29080
+ return result;
29081
+ } catch (e) {
29082
+ if (!raceWon) methodErrors.set(m, fmtErr(e));
29083
+ logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
29084
+ throw e;
29085
+ }
29086
+ })
29087
+ );
29088
+ } catch (e) {
29089
+ if (e instanceof AggregateError) {
29090
+ const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
29091
+ throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
29092
+ }
29093
+ throw e;
29094
+ }
29095
+ };
28226
29096
  const effectiveUid = normalizeUid(uid);
28227
29097
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
28228
29098
  const isReachable = await pingHost(host);
@@ -28252,9 +29122,9 @@ async function autoDetectDeviceType(inputs) {
28252
29122
  normalizedUid = normalizedDiscovered;
28253
29123
  }
28254
29124
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
28255
- const udpErrors = [];
28256
- for (const m of methodsToTry) {
28257
- try {
29125
+ return await runUdpMethodsParallel(
29126
+ methodsToTry,
29127
+ async (m, isAborted) => {
28258
29128
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
28259
29129
  const udpApi = await withRetries(
28260
29130
  `UDP(${m})`,
@@ -28277,11 +29147,14 @@ async function autoDetectDeviceType(inputs) {
28277
29147
  throw e;
28278
29148
  }
28279
29149
  },
28280
- shouldRetryUdp
29150
+ shouldRetryUdp,
29151
+ isAborted
28281
29152
  );
28282
- const deviceInfo = await udpApi.getInfo();
28283
- const capabilities = await udpApi.getDeviceCapabilities();
28284
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
29153
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
29154
+ udpApi.getInfo(),
29155
+ udpApi.getDeviceCapabilities(),
29156
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
29157
+ ]);
28285
29158
  const channelNum = capabilities?.support?.channelNum ?? 1;
28286
29159
  const model = deviceInfo.type?.trim();
28287
29160
  const normalizedModel = model ? model.trim() : void 0;
@@ -28320,14 +29193,8 @@ async function autoDetectDeviceType(inputs) {
28320
29193
  channelNum: 1,
28321
29194
  api: udpApi
28322
29195
  };
28323
- } catch (e) {
28324
- const msg = fmtErr(e);
28325
- udpErrors.push(`${m}: ${msg}`);
28326
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
28327
- }
28328
- }
28329
- throw new Error(
28330
- `Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
29196
+ },
29197
+ "Forced UDP autodetect failed for all methods."
28331
29198
  );
28332
29199
  }
28333
29200
  let tcpApi;
@@ -28380,54 +29247,57 @@ async function autoDetectDeviceType(inputs) {
28380
29247
  }
28381
29248
  return void 0;
28382
29249
  };
28383
- const infoProbe = await runProbeVariants(
28384
- "getInfo",
28385
- [
29250
+ const [infoProbe, supportProbe] = await Promise.all([
29251
+ runProbeVariants(
29252
+ "getInfo",
29253
+ [
29254
+ {
29255
+ variant: "cmd80 class=0x6414",
29256
+ op: () => api.getInfo(void 0, {
29257
+ timeoutMs: 2500,
29258
+ messageClass: BC_CLASS_MODERN_24
29259
+ })
29260
+ },
29261
+ {
29262
+ variant: "cmd80 class=0x6614",
29263
+ op: () => api.getInfo(void 0, {
29264
+ timeoutMs: 3e3,
29265
+ messageClass: BC_CLASS_MODERN_20
29266
+ })
29267
+ },
29268
+ {
29269
+ variant: "cmd318(ch0) class=0x6414",
29270
+ op: () => api.getInfo(0, {
29271
+ timeoutMs: 3e3,
29272
+ messageClass: BC_CLASS_MODERN_24
29273
+ })
29274
+ },
29275
+ {
29276
+ variant: "cmd318(ch0) class=0x6614",
29277
+ op: () => api.getInfo(0, {
29278
+ timeoutMs: 3500,
29279
+ messageClass: BC_CLASS_MODERN_20
29280
+ })
29281
+ }
29282
+ ]
29283
+ ),
29284
+ // Support probes (cmd 199). Some firmwares may not support it or are slow.
29285
+ runProbeVariants("getSupportInfo", [
28386
29286
  {
28387
- variant: "cmd80 class=0x6414",
28388
- op: () => api.getInfo(void 0, {
29287
+ variant: "cmd199 class=0x6414",
29288
+ op: () => api.getSupportInfo({
28389
29289
  timeoutMs: 2500,
28390
29290
  messageClass: BC_CLASS_MODERN_24
28391
29291
  })
28392
29292
  },
28393
29293
  {
28394
- variant: "cmd80 class=0x6614",
28395
- op: () => api.getInfo(void 0, {
28396
- timeoutMs: 3e3,
28397
- messageClass: BC_CLASS_MODERN_20
28398
- })
28399
- },
28400
- {
28401
- variant: "cmd318(ch0) class=0x6414",
28402
- op: () => api.getInfo(0, {
28403
- timeoutMs: 3e3,
28404
- messageClass: BC_CLASS_MODERN_24
28405
- })
28406
- },
28407
- {
28408
- variant: "cmd318(ch0) class=0x6614",
28409
- op: () => api.getInfo(0, {
29294
+ variant: "cmd199 class=0x6614",
29295
+ op: () => api.getSupportInfo({
28410
29296
  timeoutMs: 3500,
28411
29297
  messageClass: BC_CLASS_MODERN_20
28412
29298
  })
28413
29299
  }
28414
- ]
28415
- );
28416
- const supportProbe = await runProbeVariants("getSupportInfo", [
28417
- {
28418
- variant: "cmd199 class=0x6414",
28419
- op: () => api.getSupportInfo({
28420
- timeoutMs: 2500,
28421
- messageClass: BC_CLASS_MODERN_24
28422
- })
28423
- },
28424
- {
28425
- variant: "cmd199 class=0x6614",
28426
- op: () => api.getSupportInfo({
28427
- timeoutMs: 3500,
28428
- messageClass: BC_CLASS_MODERN_20
28429
- })
28430
- }
29300
+ ])
28431
29301
  ]);
28432
29302
  const deviceInfo = infoProbe?.value;
28433
29303
  const support = supportProbe?.value;
@@ -28519,9 +29389,11 @@ async function autoDetectDeviceType(inputs) {
28519
29389
  }
28520
29390
  try {
28521
29391
  const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
28522
- const deviceInfo = await udpApi.getInfo();
28523
- const capabilities = await udpApi.getDeviceCapabilities();
28524
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
29392
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
29393
+ udpApi.getInfo(),
29394
+ udpApi.getDeviceCapabilities(),
29395
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
29396
+ ]);
28525
29397
  const channelNum = capabilities?.support?.channelNum ?? 1;
28526
29398
  const model = deviceInfo.type?.trim();
28527
29399
  const normalizedModel = model ? model.trim() : void 0;
@@ -28565,21 +29437,17 @@ async function autoDetectDeviceType(inputs) {
28565
29437
  };
28566
29438
  };
28567
29439
  const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
28568
- const udpErrors = [];
28569
- for (const m of methodsToTry) {
28570
- try {
29440
+ const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
29441
+ return await runUdpMethodsParallel(
29442
+ viableMethods,
29443
+ async (m, isAborted) => {
28571
29444
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
28572
29445
  const udpApi = await withRetries(
28573
29446
  `UDP(${m})`,
28574
29447
  maxRetries,
28575
29448
  async (attempt) => {
28576
- const apiInputs = {
28577
- ...inputs,
28578
- udpDiscoveryMethod: m
28579
- };
28580
- if (normalizedUid) {
28581
- apiInputs.uid = normalizedUid;
28582
- }
29449
+ const apiInputs = { ...inputs, udpDiscoveryMethod: m };
29450
+ if (normalizedUid) apiInputs.uid = normalizedUid;
28583
29451
  const api = createBaichuanApi(apiInputs, "udp");
28584
29452
  try {
28585
29453
  await api.login();
@@ -28594,20 +29462,12 @@ async function autoDetectDeviceType(inputs) {
28594
29462
  throw e;
28595
29463
  }
28596
29464
  },
28597
- shouldRetryUdp
29465
+ shouldRetryUdp,
29466
+ isAborted
28598
29467
  );
28599
- return await detectOverUdpApi(udpApi, m);
28600
- } catch (e) {
28601
- const msg = e?.message || e?.toString?.() || String(e);
28602
- udpErrors.push(`${m}: ${msg}`);
28603
- try {
28604
- } catch {
28605
- }
28606
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
28607
- }
28608
- }
28609
- throw new Error(
28610
- `UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
29468
+ return detectOverUdpApi(udpApi, m);
29469
+ },
29470
+ "UDP discovery failed for all methods."
28611
29471
  );
28612
29472
  } catch (udpError) {
28613
29473
  logger?.log?.(