@apocaliss92/nodelink-js 0.4.4 → 0.4.6

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/README.md CHANGED
@@ -93,6 +93,8 @@ Devices with captured fixtures (verified API compatibility):
93
93
  | --- | --- | --- |
94
94
  | E1 Outdoor PoE | Wired camera | v3.1.0.5223 |
95
95
  | E1 Zoom | Wired camera (H.265, PTZ) | v3.2.0.4741 |
96
+ | RLC-810A | Wired camera (8MP) | v3.1.0.1162 |
97
+ | B400 | Wired camera (4MP) | v3.0.0.183 |
96
98
  | Argus 3E | Battery camera (via Home Hub) | v3.0.0.3623 |
97
99
  | Argus PT Ultra | Battery camera with PTZ (via Home Hub) | v3.0.0.3911 |
98
100
  | Reolink Home Hub | NVR / Hub | v3.3.0.456 |
@@ -12,7 +12,7 @@ import {
12
12
  sampleStreams,
13
13
  sanitizeFixtureData,
14
14
  testChannelStreams
15
- } from "./chunk-DEOMUWBN.js";
15
+ } from "./chunk-TR3V5FTO.js";
16
16
  export {
17
17
  captureModelFixtures,
18
18
  collectCgiDiagnostics,
@@ -28,4 +28,4 @@ export {
28
28
  sanitizeFixtureData,
29
29
  testChannelStreams
30
30
  };
31
- //# sourceMappingURL=DiagnosticsTools-55PR4WFD.js.map
31
+ //# sourceMappingURL=DiagnosticsTools-UMN4C7SY.js.map
@@ -144,7 +144,7 @@ import {
144
144
  talkTraceLog,
145
145
  traceLog,
146
146
  xmlEscape
147
- } from "./chunk-DEOMUWBN.js";
147
+ } from "./chunk-TR3V5FTO.js";
148
148
 
149
149
  // src/protocol/framing.ts
150
150
  function encodeHeader(h) {
@@ -654,6 +654,9 @@ var BcUdpStream = class extends EventEmitter {
654
654
  resendTimer;
655
655
  hbTimer;
656
656
  discoveryTid;
657
+ // Track discovery-phase timers so close() can cancel them even if
658
+ // discovery is still in progress (prevents ERR_SOCKET_DGRAM_NOT_RUNNING).
659
+ discoveryTimers = [];
657
660
  acceptSent = false;
658
661
  lastAcceptAtMs;
659
662
  ackScheduled = false;
@@ -702,9 +705,31 @@ var BcUdpStream = class extends EventEmitter {
702
705
  });
703
706
  sock.on("error", (e) => this.emit("error", e));
704
707
  sock.on("close", () => this.emit("close"));
705
- await new Promise(
706
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
707
- );
708
+ const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
709
+ for (let i = portRange.length - 1; i > 0; i--) {
710
+ const j = Math.floor(Math.random() * (i + 1));
711
+ [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
712
+ }
713
+ let bound = false;
714
+ for (const port of portRange) {
715
+ try {
716
+ await new Promise((resolve, reject) => {
717
+ sock.once("error", reject);
718
+ sock.bind(port, "0.0.0.0", () => {
719
+ sock.removeListener("error", reject);
720
+ resolve();
721
+ });
722
+ });
723
+ bound = true;
724
+ break;
725
+ } catch {
726
+ }
727
+ }
728
+ if (!bound) {
729
+ await new Promise(
730
+ (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
731
+ );
732
+ }
708
733
  if (this.opts.mode === "direct") {
709
734
  this.remote = { host: this.opts.host, port: this.opts.port };
710
735
  this.clientId = this.opts.clientId;
@@ -1124,7 +1149,24 @@ var BcUdpStream = class extends EventEmitter {
1124
1149
  BCUDP_DISCOVERY_PORT_LOCAL_ANY,
1125
1150
  BCUDP_DISCOVERY_PORT_LOCAL_UID
1126
1151
  ];
1127
- const broadcastHost = "255.255.255.255";
1152
+ const broadcastHosts = ["255.255.255.255"];
1153
+ const ifaces = networkInterfaces();
1154
+ for (const name of Object.keys(ifaces)) {
1155
+ const entries = ifaces[name];
1156
+ if (!entries) continue;
1157
+ for (const addr2 of entries) {
1158
+ if (addr2.family === "IPv4" && !addr2.internal && addr2.cidr) {
1159
+ const ipParts = addr2.address.split(".").map(Number);
1160
+ const maskParts = addr2.netmask.split(".").map(Number);
1161
+ if (ipParts.length === 4 && maskParts.length === 4) {
1162
+ const bcast = ipParts.map((octet, i) => octet | ~maskParts[i] & 255).join(".");
1163
+ if (!broadcastHosts.includes(bcast)) {
1164
+ broadcastHosts.push(bcast);
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1128
1170
  const directHost = (this.opts.directHost ?? "").trim();
1129
1171
  const localMode = opts?.localMode ?? "local-broadcast";
1130
1172
  const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
@@ -1151,6 +1193,7 @@ var BcUdpStream = class extends EventEmitter {
1151
1193
  )
1152
1194
  );
1153
1195
  }, discoveryTimeout);
1196
+ this.discoveryTimers.push(timeout);
1154
1197
  let retryTimer;
1155
1198
  let retryCount = 0;
1156
1199
  let discoveredSid;
@@ -1327,11 +1370,11 @@ var BcUdpStream = class extends EventEmitter {
1327
1370
  if (directHost) {
1328
1371
  if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
1329
1372
  return [directHost];
1330
- return [directHost, broadcastHost];
1373
+ return [directHost, ...broadcastHosts];
1331
1374
  }
1332
- return [broadcastHost];
1375
+ return broadcastHosts;
1333
1376
  }
1334
- return [broadcastHost];
1377
+ return broadcastHosts;
1335
1378
  })();
1336
1379
  for (const host of Array.from(new Set(hosts))) {
1337
1380
  for (const port of ports) {
@@ -1339,8 +1382,7 @@ var BcUdpStream = class extends EventEmitter {
1339
1382
  sock.send(packet, port, host);
1340
1383
  retryCount++;
1341
1384
  this.emit("debug", "discovery_send", { retryCount, host, port });
1342
- } catch (e) {
1343
- this.emit("error", e instanceof Error ? e : new Error(String(e)));
1385
+ } catch {
1344
1386
  }
1345
1387
  }
1346
1388
  }
@@ -1349,6 +1391,7 @@ var BcUdpStream = class extends EventEmitter {
1349
1391
  retryTimer = setIntervalNode(() => {
1350
1392
  sendDiscovery();
1351
1393
  }, retryInterval);
1394
+ this.discoveryTimers.push(retryTimer);
1352
1395
  });
1353
1396
  this.clientId = reply.cid;
1354
1397
  this.cameraId = reply.did;
@@ -1665,6 +1708,10 @@ var BcUdpStream = class extends EventEmitter {
1665
1708
  this.ackTimer = void 0;
1666
1709
  this.resendTimer = void 0;
1667
1710
  this.hbTimer = void 0;
1711
+ for (const t of this.discoveryTimers) {
1712
+ clearInterval(t);
1713
+ }
1714
+ this.discoveryTimers = [];
1668
1715
  const s = this.sock;
1669
1716
  this.sock = void 0;
1670
1717
  if (!s) return;
@@ -1806,6 +1853,14 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1806
1853
  * even if the current client instance is idle/disconnected.
1807
1854
  */
1808
1855
  static streamingRegistry = /* @__PURE__ */ new Map();
1856
+ /**
1857
+ * Per-host D2C_DISC backoff state that persists across client instance recreation.
1858
+ *
1859
+ * Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
1860
+ * and creates a new one. Instance-level backoff variables would reset to zero,
1861
+ * allowing immediate reconnection and perpetuating the storm.
1862
+ */
1863
+ static d2cDiscBackoff = /* @__PURE__ */ new Map();
1809
1864
  /**
1810
1865
  * Global (process-wide) CoverPreview serialization.
1811
1866
  *
@@ -2501,7 +2556,12 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2501
2556
  }
2502
2557
  async waitForUdpReconnectCooldown() {
2503
2558
  const now = Date.now();
2504
- const waitMs = this.udpReconnectCooldownUntilMs - now;
2559
+ const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
2560
+ const effectiveCooldownUntil = Math.max(
2561
+ this.udpReconnectCooldownUntilMs,
2562
+ staticEntry?.cooldownUntilMs ?? 0
2563
+ );
2564
+ const waitMs = effectiveCooldownUntil - now;
2505
2565
  if (waitMs <= 0) return;
2506
2566
  const sid = this.socketSessionId;
2507
2567
  const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
@@ -2510,7 +2570,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2510
2570
  host: this.opts.host,
2511
2571
  sid,
2512
2572
  uid: shortUid,
2513
- waitMs
2573
+ waitMs,
2574
+ persistent: staticEntry != null
2514
2575
  });
2515
2576
  await new Promise((resolve) => setTimeout(resolve, waitMs));
2516
2577
  }
@@ -2753,21 +2814,30 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2753
2814
  uid: shortUid2,
2754
2815
  message: err.message
2755
2816
  });
2756
- const withinWindow = now - this.udpReconnectLastD2cDiscAtMs < 6e4;
2817
+ const hostKey = this.opts.host;
2818
+ const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
2819
+ const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
2757
2820
  const baseMs = 2e3;
2758
2821
  const maxMs = 3e4;
2759
2822
  const nextBackoffMs = withinWindow ? Math.min(
2760
2823
  maxMs,
2761
2824
  Math.max(
2762
2825
  baseMs,
2763
- this.udpReconnectBackoffMs > 0 ? this.udpReconnectBackoffMs * 2 : baseMs
2826
+ prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
2764
2827
  )
2765
2828
  ) : baseMs;
2766
- this.udpReconnectLastD2cDiscAtMs = now;
2767
- this.udpReconnectBackoffMs = nextBackoffMs;
2829
+ const cooldownUntilMs = Math.max(
2830
+ prev?.cooldownUntilMs ?? 0,
2831
+ now + nextBackoffMs
2832
+ );
2833
+ _BaichuanClient.d2cDiscBackoff.set(hostKey, {
2834
+ backoffMs: nextBackoffMs,
2835
+ lastAtMs: now,
2836
+ cooldownUntilMs
2837
+ });
2768
2838
  this.udpReconnectCooldownUntilMs = Math.max(
2769
2839
  this.udpReconnectCooldownUntilMs,
2770
- now + nextBackoffMs
2840
+ cooldownUntilMs
2771
2841
  );
2772
2842
  this.logDebug("d2c_disc_backoff", {
2773
2843
  transport: "udp",
@@ -2775,7 +2845,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2775
2845
  sid: sid2,
2776
2846
  uid: shortUid2,
2777
2847
  backoffMs: nextBackoffMs,
2778
- cooldownUntilMs: this.udpReconnectCooldownUntilMs
2848
+ cooldownUntilMs,
2849
+ persistent: true
2779
2850
  });
2780
2851
  this.stopKeepAlive();
2781
2852
  this.loggedIn = false;
@@ -2784,6 +2855,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2784
2855
  this.videoSubscriptions.clear();
2785
2856
  this.recomputeGlobalStreamingContribution();
2786
2857
  }
2858
+ this.emit("d2c_disc", { host: this.opts.host, atMs: now });
2787
2859
  }
2788
2860
  this.emit("error", err);
2789
2861
  });
@@ -3082,6 +3154,13 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
3082
3154
  }
3083
3155
  }
3084
3156
  this.emit("push", frame);
3157
+ if (frame.header.cmdId === 252 && frame.body.length > 0) {
3158
+ try {
3159
+ this.emit("batteryPush", frame);
3160
+ } catch (error) {
3161
+ this.logDebug("battery_push_error", error);
3162
+ }
3163
+ }
3085
3164
  if (frame.header.cmdId === 33) {
3086
3165
  try {
3087
3166
  const sid = this.socketSessionId;
@@ -10149,6 +10228,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10149
10228
  }
10150
10229
  }
10151
10230
  const newClient = new BaichuanClient(this.clientOptions);
10231
+ this.attachD2cDiscListener(newClient);
10152
10232
  this.socketPool.set("general", {
10153
10233
  client: newClient,
10154
10234
  refCount: 1,
@@ -10194,6 +10274,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10194
10274
  if (!mapped) return;
10195
10275
  this.dispatchSimpleEvent(mapped);
10196
10276
  });
10277
+ client.on("batteryPush", (frame) => {
10278
+ try {
10279
+ const xml = this.client.tryDecryptXml(
10280
+ frame.body,
10281
+ frame.header.channelId,
10282
+ this.client.enc
10283
+ );
10284
+ if (!xml) return;
10285
+ const channel = frame.header.channelId;
10286
+ const battery = this.parseBatteryInfoXml(xml, channel);
10287
+ if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
10288
+ this.dispatchSimpleEvent({
10289
+ type: "battery",
10290
+ channel,
10291
+ timestamp: Date.now(),
10292
+ battery
10293
+ });
10294
+ }
10295
+ } catch (e) {
10296
+ this.logger.debug?.(
10297
+ "[ReolinkBaichuanApi] Error parsing battery push",
10298
+ formatErrorForLog(e)
10299
+ );
10300
+ }
10301
+ });
10197
10302
  client.on("channelInfo", (xml) => {
10198
10303
  try {
10199
10304
  this.parseAndStoreChannelInfo(xml);
@@ -10329,6 +10434,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10329
10434
  rtspServers = /* @__PURE__ */ new Set();
10330
10435
  // Track all RTSP servers for cleanup
10331
10436
  activeVideoMsgNums = /* @__PURE__ */ new Map();
10437
+ // ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
10438
+ // Tracked on the API instance (survives BaichuanClient recreation).
10439
+ /** Timestamp of the most recent D2C_DISC from any client for this device. */
10440
+ lastD2cDiscAtMs = 0;
10441
+ /** Sliding window of recent D2C_DISC timestamps for storm detection. */
10442
+ d2cDiscTimestamps = [];
10443
+ /** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
10444
+ * Prevents reconnect attempts while the camera is transitioning to sleep. */
10445
+ static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
10446
+ /** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
10447
+ static D2C_DISC_STORM_THRESHOLD = 3;
10448
+ /** Sliding window size (ms) for storm detection. */
10449
+ static D2C_DISC_STORM_WINDOW_MS = 6e4;
10450
+ /** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
10451
+ static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
10332
10452
  nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
10333
10453
  /**
10334
10454
  * Cached device capabilities per channel.
@@ -10653,6 +10773,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10653
10773
  const prefix = basename.substring(0, 10).toUpperCase();
10654
10774
  return prefix.includes("S") ? "subStream" : "mainStream";
10655
10775
  }
10776
+ /**
10777
+ * Stream profiles that the device explicitly rejected (response_code 400).
10778
+ * Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
10779
+ * it is excluded from `buildVideoStreamOptions()` results and no further
10780
+ * start attempts are made until the API instance is recreated.
10781
+ */
10782
+ _rejectedStreamProfiles = /* @__PURE__ */ new Set();
10783
+ /**
10784
+ * Check whether a stream profile was rejected by the device at runtime
10785
+ * (e.g. ext returned response_code 400).
10786
+ */
10787
+ isStreamProfileRejected(channel, profile) {
10788
+ return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
10789
+ }
10656
10790
  /**
10657
10791
  * Cache for buildVideoStreamOptions.
10658
10792
  *
@@ -10742,6 +10876,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10742
10876
  }
10743
10877
  return "general";
10744
10878
  }
10879
+ /**
10880
+ * Attach a D2C_DISC listener to a BaichuanClient so that the API-level
10881
+ * grace period and storm detection are updated regardless of which
10882
+ * pool socket receives the disconnect.
10883
+ */
10884
+ attachD2cDiscListener(client) {
10885
+ client.on("d2c_disc", () => this.notifyD2cDisc());
10886
+ }
10745
10887
  /**
10746
10888
  * Acquire a socket from the pool by tag.
10747
10889
  * Creates a new socket if needed, or reuses an existing one.
@@ -10762,10 +10904,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10762
10904
  );
10763
10905
  } else if (now < cooldownEntry.cooldownUntil) {
10764
10906
  const remainingMs = cooldownEntry.cooldownUntil - now;
10907
+ const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
10908
+ const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
10765
10909
  const error = new Error(
10766
- `[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to repeated login failures. tag=${tag}`
10910
+ `[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
10767
10911
  );
10768
- log?.warn?.(error.message);
10912
+ log?.debug?.(error.message);
10769
10913
  throw error;
10770
10914
  }
10771
10915
  }
@@ -10856,12 +11000,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10856
11000
  try {
10857
11001
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
10858
11002
  const newClient = new BaichuanClient(clientOpts);
11003
+ this.attachD2cDiscListener(newClient);
10859
11004
  await newClient.login();
10860
- if (this.socketPoolCooldowns.has(this.host)) {
10861
- log?.debug?.(
10862
- `[SocketPool] Clearing cooldown for host=${this.host} after successful login`
10863
- );
10864
- this.socketPoolCooldowns.delete(this.host);
11005
+ const existingCooldown = this.socketPoolCooldowns.get(this.host);
11006
+ if (existingCooldown) {
11007
+ const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
11008
+ if (!isStormCooldown) {
11009
+ log?.debug?.(
11010
+ `[SocketPool] Clearing cooldown for host=${this.host} after successful login`
11011
+ );
11012
+ this.socketPoolCooldowns.delete(this.host);
11013
+ } else {
11014
+ log?.debug?.(
11015
+ `[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
11016
+ );
11017
+ }
10865
11018
  }
10866
11019
  entry.client = newClient;
10867
11020
  entry.refCount = 1;
@@ -11163,6 +11316,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11163
11316
  ...opts.channel !== void 0 ? { channel: opts.channel } : {}
11164
11317
  };
11165
11318
  const generalClient = new BaichuanClient(opts);
11319
+ this.attachD2cDiscListener(generalClient);
11166
11320
  this.socketPool.set("general", {
11167
11321
  client: generalClient,
11168
11322
  refCount: 1,
@@ -15757,6 +15911,16 @@ ${stderr}`)
15757
15911
  }
15758
15912
  if (!frame) frame = await targetClient.sendFrame(baseParams);
15759
15913
  if (frame.header.responseCode !== 200) {
15914
+ if (frame.header.responseCode === 400) {
15915
+ const rejKey = `${ch}:${profile}`;
15916
+ if (!this._rejectedStreamProfiles.has(rejKey)) {
15917
+ this._rejectedStreamProfiles.add(rejKey);
15918
+ this.videoStreamOptionsCache.clear();
15919
+ this.logger?.warn?.(
15920
+ `[ReolinkBaichuanApi] Stream profile rejected by device: channel=${ch} profile=${profile} (response_code 400). This profile will be excluded from available streams. The camera may not support this stream profile with the current firmware.`
15921
+ );
15922
+ }
15923
+ }
15760
15924
  throw new Error(
15761
15925
  `Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
15762
15926
  );
@@ -16227,6 +16391,49 @@ ${stderr}`)
16227
16391
  if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
16228
16392
  return out;
16229
16393
  }
16394
+ /**
16395
+ * Called when any BaichuanClient for this device receives a D2C_DISC.
16396
+ *
16397
+ * Two-tier response:
16398
+ * 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
16399
+ * (10 s) to prevent reconnect attempts while the camera transitions to sleep.
16400
+ * 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
16401
+ */
16402
+ notifyD2cDisc() {
16403
+ const now = Date.now();
16404
+ this.lastD2cDiscAtMs = now;
16405
+ const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
16406
+ const existing = this.socketPoolCooldowns.get(this.host);
16407
+ if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
16408
+ this.socketPoolCooldowns.set(this.host, {
16409
+ failureCount: existing?.failureCount ?? 1,
16410
+ lastFailureAt: now,
16411
+ cooldownUntil: immediateCooldownUntil
16412
+ });
16413
+ this.logger?.log?.(
16414
+ `[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
16415
+ );
16416
+ }
16417
+ this.d2cDiscTimestamps.push(now);
16418
+ const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
16419
+ while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
16420
+ this.d2cDiscTimestamps.shift();
16421
+ }
16422
+ if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
16423
+ const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
16424
+ const currentEntry = this.socketPoolCooldowns.get(this.host);
16425
+ if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
16426
+ this.socketPoolCooldowns.set(this.host, {
16427
+ failureCount: this.d2cDiscTimestamps.length,
16428
+ lastFailureAt: now,
16429
+ cooldownUntil: stormCooldownUntil
16430
+ });
16431
+ this.logger?.warn?.(
16432
+ `[D2C_DISC] Storm detected: ${this.d2cDiscTimestamps.length} disconnects in ${_ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS / 1e3}s \u2192 socket pool cooldown ${_ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS / 1e3}s`
16433
+ );
16434
+ }
16435
+ }
16436
+ }
16230
16437
  /**
16231
16438
  * Best-effort sleeping inference for battery/BCUDP cameras.
16232
16439
  *
@@ -16257,6 +16464,8 @@ ${stderr}`)
16257
16464
  const socketConnected = this.client.isSocketConnected?.() ?? false;
16258
16465
  const now = Date.now();
16259
16466
  const cutoff = now - windowMs;
16467
+ const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
16468
+ const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
16260
16469
  const rx = (this.client.getRxHistory?.() ?? []).filter(
16261
16470
  (h) => h.atMs >= cutoff
16262
16471
  );
@@ -16264,6 +16473,12 @@ ${stderr}`)
16264
16473
  (h) => h.atMs >= cutoff
16265
16474
  );
16266
16475
  if (rx.length === 0 && tx.length === 0) {
16476
+ if (recentD2cDisc) {
16477
+ return {
16478
+ state: "sleeping",
16479
+ reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
16480
+ };
16481
+ }
16267
16482
  return {
16268
16483
  state: "sleeping",
16269
16484
  reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
@@ -16287,6 +16502,12 @@ ${stderr}`)
16287
16502
  idleMs: now - firstWakingTx.atMs
16288
16503
  };
16289
16504
  }
16505
+ if (recentD2cDisc) {
16506
+ return {
16507
+ state: "sleeping",
16508
+ reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
16509
+ };
16510
+ }
16290
16511
  return {
16291
16512
  state: "sleeping",
16292
16513
  reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
@@ -17721,6 +17942,8 @@ ${xml}`
17721
17942
  for (const metadata of params.metadatas) {
17722
17943
  const profile = metadata.profile;
17723
17944
  if (isMultiFocal && profile === "ext") continue;
17945
+ if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
17946
+ continue;
17724
17947
  if (params.includeRtsp && profile !== "ext") {
17725
17948
  const streamName = profile === "main" ? "main" : "sub";
17726
17949
  pushRtsp({
@@ -17882,7 +18105,7 @@ ${xml}`
17882
18105
  * @returns Test results for all stream types and profiles
17883
18106
  */
17884
18107
  async testChannelStreams(channel, logger) {
17885
- const { testChannelStreams } = await import("./DiagnosticsTools-55PR4WFD.js");
18108
+ const { testChannelStreams } = await import("./DiagnosticsTools-UMN4C7SY.js");
17886
18109
  return await testChannelStreams({
17887
18110
  api: this,
17888
18111
  channel: this.normalizeChannel(channel),
@@ -17898,7 +18121,7 @@ ${xml}`
17898
18121
  * @returns Complete diagnostics for all channels and streams
17899
18122
  */
17900
18123
  async collectMultifocalDiagnostics(logger) {
17901
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-55PR4WFD.js");
18124
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-UMN4C7SY.js");
17902
18125
  return await collectMultifocalDiagnostics({
17903
18126
  api: this,
17904
18127
  logger
@@ -21580,4 +21803,4 @@ export {
21580
21803
  isTcpFailureThatShouldFallbackToUdp,
21581
21804
  autoDetectDeviceType
21582
21805
  };
21583
- //# sourceMappingURL=chunk-UHFJPQA4.js.map
21806
+ //# sourceMappingURL=chunk-F2Y5U3YP.js.map