@apocaliss92/nodelink-js 0.4.5 → 0.4.7

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.
@@ -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;
@@ -10083,6 +10130,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10083
10130
  * - "replay:XXX" - dedicated per replay session
10084
10131
  */
10085
10132
  socketPool = /* @__PURE__ */ new Map();
10133
+ /**
10134
+ * Consecutive stream-start (cmdId=3) timeout counter per socket tag.
10135
+ * When a streaming socket has N consecutive timeouts, the socket is force-closed
10136
+ * so the next attempt creates a fresh connection. Resets on success.
10137
+ */
10138
+ consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
10139
+ static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
10086
10140
  /** BaichuanClientOptions to use when creating new sockets */
10087
10141
  clientOptions;
10088
10142
  /**
@@ -10237,14 +10291,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10237
10291
  if (!xml) return;
10238
10292
  const channel = frame.header.channelId;
10239
10293
  const battery = this.parseBatteryInfoXml(xml, channel);
10240
- if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
10241
- this.dispatchSimpleEvent({
10242
- type: "battery",
10243
- channel,
10244
- timestamp: Date.now(),
10245
- battery
10246
- });
10294
+ if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
10295
+ return;
10247
10296
  }
10297
+ const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
10298
+ if (this.lastBatteryPushKey.get(channel) === key) {
10299
+ return;
10300
+ }
10301
+ this.lastBatteryPushKey.set(channel, key);
10302
+ this.dispatchSimpleEvent({
10303
+ type: "battery",
10304
+ channel,
10305
+ timestamp: Date.now(),
10306
+ battery
10307
+ });
10248
10308
  } catch (e) {
10249
10309
  this.logger.debug?.(
10250
10310
  "[ReolinkBaichuanApi] Error parsing battery push",
@@ -10410,6 +10470,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10410
10470
  deviceCapabilitiesCache = /* @__PURE__ */ new Map();
10411
10471
  static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
10412
10472
  // 5 minutes
10473
+ /**
10474
+ * Dedupe key for battery push events (cmd_id 252), per channel.
10475
+ * Cameras emit BatteryInfoList frequently while streaming (every few
10476
+ * seconds). We only forward an event when the meaningful fields change
10477
+ * (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
10478
+ * consumers and the UI event log.
10479
+ */
10480
+ lastBatteryPushKey = /* @__PURE__ */ new Map();
10413
10481
  // ─────────────────────────────────────────────────────────────────────────────
10414
10482
  // SOCKET POOL CONSTANTS
10415
10483
  // ─────────────────────────────────────────────────────────────────────────────
@@ -10726,6 +10794,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10726
10794
  const prefix = basename.substring(0, 10).toUpperCase();
10727
10795
  return prefix.includes("S") ? "subStream" : "mainStream";
10728
10796
  }
10797
+ /**
10798
+ * Stream profiles that the device explicitly rejected (response_code 400).
10799
+ * Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
10800
+ * it is excluded from `buildVideoStreamOptions()` results and no further
10801
+ * start attempts are made until the API instance is recreated.
10802
+ */
10803
+ _rejectedStreamProfiles = /* @__PURE__ */ new Set();
10804
+ /**
10805
+ * Check whether a stream profile was rejected by the device at runtime
10806
+ * (e.g. ext returned response_code 400).
10807
+ */
10808
+ isStreamProfileRejected(channel, profile) {
10809
+ return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
10810
+ }
10729
10811
  /**
10730
10812
  * Cache for buildVideoStreamOptions.
10731
10813
  *
@@ -15850,6 +15932,16 @@ ${stderr}`)
15850
15932
  }
15851
15933
  if (!frame) frame = await targetClient.sendFrame(baseParams);
15852
15934
  if (frame.header.responseCode !== 200) {
15935
+ if (frame.header.responseCode === 400) {
15936
+ const rejKey = `${ch}:${profile}`;
15937
+ if (!this._rejectedStreamProfiles.has(rejKey)) {
15938
+ this._rejectedStreamProfiles.add(rejKey);
15939
+ this.videoStreamOptionsCache.clear();
15940
+ this.logger?.warn?.(
15941
+ `[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.`
15942
+ );
15943
+ }
15944
+ }
15853
15945
  throw new Error(
15854
15946
  `Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
15855
15947
  );
@@ -15858,6 +15950,7 @@ ${stderr}`)
15858
15950
  `${ch}:${profile}:${variant}`,
15859
15951
  frame.header.msgNum
15860
15952
  );
15953
+ this.resetStreamTimeoutCounter(targetClient);
15861
15954
  return;
15862
15955
  } catch (error) {
15863
15956
  lastError = error;
@@ -15872,6 +15965,10 @@ ${stderr}`)
15872
15965
  }
15873
15966
  }
15874
15967
  }
15968
+ const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
15969
+ if (isTimeout) {
15970
+ this.trackStreamTimeout(targetClient);
15971
+ }
15875
15972
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
15876
15973
  }
15877
15974
  /**
@@ -16331,6 +16428,18 @@ ${stderr}`)
16331
16428
  notifyD2cDisc() {
16332
16429
  const now = Date.now();
16333
16430
  this.lastD2cDiscAtMs = now;
16431
+ const streamingTags = Array.from(this.socketPool.keys()).filter(
16432
+ (tag) => tag.startsWith("streaming:")
16433
+ );
16434
+ if (streamingTags.length > 0) {
16435
+ this.logger?.log?.(
16436
+ `[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
16437
+ );
16438
+ for (const tag of streamingTags) {
16439
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
16440
+ });
16441
+ }
16442
+ }
16334
16443
  const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
16335
16444
  const existing = this.socketPoolCooldowns.get(this.host);
16336
16445
  if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
@@ -16363,6 +16472,43 @@ ${stderr}`)
16363
16472
  }
16364
16473
  }
16365
16474
  }
16475
+ /**
16476
+ * Find the socket pool tag for a given BaichuanClient instance.
16477
+ * Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
16478
+ */
16479
+ findSocketTagForClient(client) {
16480
+ for (const [tag, entry] of this.socketPool) {
16481
+ if (entry.client === client) return tag;
16482
+ }
16483
+ return void 0;
16484
+ }
16485
+ /**
16486
+ * Reset the consecutive stream-start timeout counter for a streaming socket.
16487
+ * Called on successful stream start.
16488
+ */
16489
+ resetStreamTimeoutCounter(client) {
16490
+ const tag = this.findSocketTagForClient(client);
16491
+ if (tag) this.consecutiveStreamTimeouts.delete(tag);
16492
+ }
16493
+ /**
16494
+ * Track a stream-start timeout on a streaming socket.
16495
+ * After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
16496
+ * socket so the next attempt creates a fresh connection.
16497
+ */
16498
+ trackStreamTimeout(client) {
16499
+ const tag = this.findSocketTagForClient(client);
16500
+ if (!tag || !tag.startsWith("streaming:")) return;
16501
+ const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
16502
+ this.consecutiveStreamTimeouts.set(tag, count);
16503
+ if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
16504
+ this.logger?.warn?.(
16505
+ `[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
16506
+ );
16507
+ this.consecutiveStreamTimeouts.delete(tag);
16508
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
16509
+ });
16510
+ }
16511
+ }
16366
16512
  /**
16367
16513
  * Best-effort sleeping inference for battery/BCUDP cameras.
16368
16514
  *
@@ -17871,6 +18017,8 @@ ${xml}`
17871
18017
  for (const metadata of params.metadatas) {
17872
18018
  const profile = metadata.profile;
17873
18019
  if (isMultiFocal && profile === "ext") continue;
18020
+ if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
18021
+ continue;
17874
18022
  if (params.includeRtsp && profile !== "ext") {
17875
18023
  const streamName = profile === "main" ? "main" : "sub";
17876
18024
  pushRtsp({
@@ -18032,7 +18180,7 @@ ${xml}`
18032
18180
  * @returns Test results for all stream types and profiles
18033
18181
  */
18034
18182
  async testChannelStreams(channel, logger) {
18035
- const { testChannelStreams } = await import("./DiagnosticsTools-55PR4WFD.js");
18183
+ const { testChannelStreams } = await import("./DiagnosticsTools-UMN4C7SY.js");
18036
18184
  return await testChannelStreams({
18037
18185
  api: this,
18038
18186
  channel: this.normalizeChannel(channel),
@@ -18048,7 +18196,7 @@ ${xml}`
18048
18196
  * @returns Complete diagnostics for all channels and streams
18049
18197
  */
18050
18198
  async collectMultifocalDiagnostics(logger) {
18051
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-55PR4WFD.js");
18199
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-UMN4C7SY.js");
18052
18200
  return await collectMultifocalDiagnostics({
18053
18201
  api: this,
18054
18202
  logger
@@ -21175,16 +21323,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
21175
21323
  return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("socket hang up") || message.includes("TCP connection timeout") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
21176
21324
  }
21177
21325
  async function pingHost(host, timeoutMs = 3e3) {
21326
+ const { exec } = await import("child_process");
21327
+ const platform2 = process.platform;
21328
+ const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
21329
+ // macOS: -W is in milliseconds (Linux: seconds)
21330
+ `ping -c 1 -W ${timeoutMs} ${host}`
21331
+ ) : (
21332
+ // Linux/BSD-ish: -W is in seconds on most distros
21333
+ `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
21334
+ );
21178
21335
  return new Promise((resolve) => {
21179
- const { exec } = __require("child_process");
21180
- const platform2 = process.platform;
21181
- const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
21182
- // macOS: -W is in milliseconds (Linux: seconds)
21183
- `ping -c 1 -W ${timeoutMs} ${host}`
21184
- ) : (
21185
- // Linux/BSD-ish: -W is in seconds on most distros
21186
- `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
21187
- );
21188
21336
  exec(pingCmd, (error) => {
21189
21337
  resolve(!error);
21190
21338
  });
@@ -21730,4 +21878,4 @@ export {
21730
21878
  isTcpFailureThatShouldFallbackToUdp,
21731
21879
  autoDetectDeviceType
21732
21880
  };
21733
- //# sourceMappingURL=chunk-WDFKIHM5.js.map
21881
+ //# sourceMappingURL=chunk-GKLOJJ34.js.map