@apocaliss92/nodelink-js 0.6.1 → 0.6.3

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.
@@ -2574,6 +2574,41 @@ var init_BaichuanVideoStream = __esm({
2574
2574
  });
2575
2575
 
2576
2576
  // src/protocol/xml.ts
2577
+ function xmlTextRe(tag) {
2578
+ let re = xmlTextReCache.get(tag);
2579
+ if (re === void 0) {
2580
+ re = new RegExp(`<${tag}>([^<]*)</${tag}>`);
2581
+ xmlTextReCache.set(tag, re);
2582
+ }
2583
+ return re;
2584
+ }
2585
+ function xmlTagRe(tag) {
2586
+ let re = xmlTagReCache.get(tag);
2587
+ if (re === void 0) {
2588
+ re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2589
+ xmlTagReCache.set(tag, re);
2590
+ }
2591
+ return re;
2592
+ }
2593
+ function xmlNestedRe(parent, child) {
2594
+ const key = `${parent}\0${child}`;
2595
+ let re = xmlNestedReCache.get(key);
2596
+ if (re === void 0) {
2597
+ re = new RegExp(
2598
+ `(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
2599
+ );
2600
+ xmlNestedReCache.set(key, re);
2601
+ }
2602
+ return re;
2603
+ }
2604
+ function xmlStreamBlockRe(streamTag) {
2605
+ let re = xmlStreamBlockReCache.get(streamTag);
2606
+ if (re === void 0) {
2607
+ re = new RegExp(`(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`);
2608
+ xmlStreamBlockReCache.set(streamTag, re);
2609
+ }
2610
+ return re;
2611
+ }
2577
2612
  function xmlEscape(text) {
2578
2613
  if (text === void 0 || text === null || typeof text !== "string") {
2579
2614
  const error = new Error(
@@ -2694,8 +2729,7 @@ function buildPreviewStopXmlV11(params) {
2694
2729
  </body>`;
2695
2730
  }
2696
2731
  function getXmlText(xml, tagName) {
2697
- const re = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);
2698
- const m = re.exec(xml);
2732
+ const m = xmlTextRe(tagName).exec(xml);
2699
2733
  return m?.[1];
2700
2734
  }
2701
2735
  function buildPtzControlXml(channelId, command, speed) {
@@ -2796,13 +2830,12 @@ ${xml}`;
2796
2830
  function applyXmlTagPatch(xml, tag, value) {
2797
2831
  if (value === void 0) return xml;
2798
2832
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2799
- const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2800
- return xml.replace(re, `<${tag}>${v}</${tag}>`);
2833
+ return xml.replace(xmlTagRe(tag), `<${tag}>${v}</${tag}>`);
2801
2834
  }
2802
2835
  function upsertXmlTag(xml, tag, value) {
2803
2836
  if (value === void 0) return xml;
2804
2837
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2805
- const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2838
+ const re = xmlTagRe(tag);
2806
2839
  if (re.test(xml)) {
2807
2840
  return xml.replace(re, `<${tag}>${v}</${tag}>`);
2808
2841
  }
@@ -2811,16 +2844,11 @@ function upsertXmlTag(xml, tag, value) {
2811
2844
  function patchNestedTag(xml, parent, child, value) {
2812
2845
  if (value === void 0) return xml;
2813
2846
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2814
- const re = new RegExp(
2815
- `(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
2816
- );
2817
- return xml.replace(re, `$1${v}$2`);
2847
+ return xml.replace(xmlNestedRe(parent, child), `$1${v}$2`);
2818
2848
  }
2819
2849
  function applyStreamPatch(xml, streamTag, patch) {
2820
2850
  if (!patch) return xml;
2821
- const re = new RegExp(
2822
- `(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`
2823
- );
2851
+ const re = xmlStreamBlockRe(streamTag);
2824
2852
  return xml.replace(re, (_match, open, body, close) => {
2825
2853
  let next = body;
2826
2854
  if (patch.audio !== void 0) {
@@ -2850,10 +2878,9 @@ function applyStreamPatch(xml, streamTag, patch) {
2850
2878
  next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
2851
2879
  }
2852
2880
  if (patch.gop !== void 0) {
2853
- const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
2854
- if (gopBlockRe.test(next)) {
2881
+ if (GOP_BLOCK_RE.test(next)) {
2855
2882
  next = next.replace(
2856
- gopBlockRe,
2883
+ GOP_BLOCK_RE,
2857
2884
  (_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
2858
2885
  );
2859
2886
  } else {
@@ -2882,10 +2909,15 @@ function buildAbilityInfoExtensionXml(username) {
2882
2909
  <token>system, streaming, PTZ, IO, security, replay, disk, network, alarm, record, video, image</token>
2883
2910
  </Extension>`;
2884
2911
  }
2885
- var XML_HEADER;
2912
+ var xmlTextReCache, xmlTagReCache, xmlNestedReCache, GOP_BLOCK_RE, xmlStreamBlockReCache, XML_HEADER;
2886
2913
  var init_xml = __esm({
2887
2914
  "src/protocol/xml.ts"() {
2888
2915
  "use strict";
2916
+ xmlTextReCache = /* @__PURE__ */ new Map();
2917
+ xmlTagReCache = /* @__PURE__ */ new Map();
2918
+ xmlNestedReCache = /* @__PURE__ */ new Map();
2919
+ GOP_BLOCK_RE = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
2920
+ xmlStreamBlockReCache = /* @__PURE__ */ new Map();
2889
2921
  XML_HEADER = `<?xml version="1.0" encoding="UTF-8" ?>`;
2890
2922
  }
2891
2923
  });
@@ -11350,8 +11382,14 @@ async function getServerBinding(uid, options = {}) {
11350
11382
  headers: { Accept: "application/json" }
11351
11383
  });
11352
11384
  if (!res.ok) {
11385
+ let bodyPreview;
11386
+ try {
11387
+ const text = await res.text();
11388
+ bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
11389
+ } catch {
11390
+ }
11353
11391
  logger?.log?.(
11354
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
11392
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
11355
11393
  );
11356
11394
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11357
11395
  return void 0;
@@ -11467,6 +11505,68 @@ function parseServerBindingResponse(raw) {
11467
11505
  }
11468
11506
 
11469
11507
  // src/bcudp/BcUdpStream.ts
11508
+ async function probeEgressForHost(destHost, destPort) {
11509
+ return await new Promise((resolve, reject) => {
11510
+ const probe = import_node_dgram.default.createSocket("udp4");
11511
+ let settled = false;
11512
+ const finish = (err, out) => {
11513
+ if (settled) return;
11514
+ settled = true;
11515
+ try {
11516
+ probe.close();
11517
+ } catch {
11518
+ }
11519
+ if (err || !out) reject(err ?? new Error("egress probe failed"));
11520
+ else resolve(out);
11521
+ };
11522
+ probe.on("error", (e) => finish(e));
11523
+ try {
11524
+ probe.connect(destPort, destHost, () => {
11525
+ try {
11526
+ const a = probe.address();
11527
+ if (typeof a === "string") return finish(new Error("probe address is string"));
11528
+ finish(void 0, {
11529
+ localAddress: a.address,
11530
+ localPort: a.port
11531
+ });
11532
+ } catch (e) {
11533
+ finish(e);
11534
+ }
11535
+ });
11536
+ } catch (e) {
11537
+ finish(e);
11538
+ }
11539
+ });
11540
+ }
11541
+ function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
11542
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
11543
+ const dest = destHost.split(".").map((s) => Number(s));
11544
+ if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
11545
+ return "unknown";
11546
+ const ifaces = (0, import_node_os.networkInterfaces)();
11547
+ let ownerSubnet;
11548
+ for (const name of Object.keys(ifaces)) {
11549
+ const entries = ifaces[name];
11550
+ if (!entries) continue;
11551
+ for (const e of entries) {
11552
+ if (e.family !== "IPv4" || e.internal) continue;
11553
+ if (e.address !== srcInfo.localAddress) continue;
11554
+ const addr = e.address.split(".").map((s) => Number(s));
11555
+ const mask = e.netmask.split(".").map((s) => Number(s));
11556
+ if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
11557
+ continue;
11558
+ ownerSubnet = { addr, mask };
11559
+ break;
11560
+ }
11561
+ if (ownerSubnet) break;
11562
+ }
11563
+ if (!ownerSubnet) return "unknown";
11564
+ for (let i = 0; i < 4; i++) {
11565
+ if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
11566
+ return "mismatch";
11567
+ }
11568
+ return "same";
11569
+ }
11470
11570
  var AckLatency = class {
11471
11571
  currentValues = [];
11472
11572
  lastReceiveTime = null;
@@ -11545,6 +11645,11 @@ function isUnroutableForP2P(ip) {
11545
11645
  var P2P_LOOKUP_PORT = 9999;
11546
11646
  var P2P_MAX_WAIT_MS = 15e3;
11547
11647
  var P2P_RESEND_WAIT_MS = 500;
11648
+ var inflightP2pLookups = /* @__PURE__ */ new Map();
11649
+ var cachedP2pLookups = /* @__PURE__ */ new Map();
11650
+ var negCachedP2pLookups = /* @__PURE__ */ new Map();
11651
+ var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
11652
+ var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
11548
11653
  var BcUdpStream = class extends import_node_events3.EventEmitter {
11549
11654
  opts;
11550
11655
  /**
@@ -11633,31 +11738,14 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11633
11738
  });
11634
11739
  sock.on("error", (e) => this.emit("error", e));
11635
11740
  sock.on("close", () => this.emit("close"));
11636
- const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
11637
- for (let i = portRange.length - 1; i > 0; i--) {
11638
- const j = Math.floor(Math.random() * (i + 1));
11639
- [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
11640
- }
11641
- let bound = false;
11642
- for (const port of portRange) {
11643
- try {
11644
- await new Promise((resolve, reject) => {
11645
- sock.once("error", reject);
11646
- sock.bind(port, "0.0.0.0", () => {
11647
- sock.removeListener("error", reject);
11648
- resolve();
11649
- });
11650
- });
11651
- bound = true;
11652
- break;
11653
- } catch {
11654
- }
11655
- }
11656
- if (!bound) {
11657
- await new Promise(
11658
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
11659
- );
11660
- }
11741
+ await new Promise((resolve, reject) => {
11742
+ const onErr = (e) => reject(e);
11743
+ sock.once("error", onErr);
11744
+ sock.bind(0, "0.0.0.0", () => {
11745
+ sock.removeListener("error", onErr);
11746
+ resolve();
11747
+ });
11748
+ });
11661
11749
  if (this.opts.mode === "direct") {
11662
11750
  this.remote = { host: this.opts.host, port: this.opts.port };
11663
11751
  this.clientId = this.opts.clientId;
@@ -11728,6 +11816,49 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11728
11816
  this.remote = { host: connected.rhost, port: connected.rport };
11729
11817
  }
11730
11818
  async p2pUidLookup(sock, uid) {
11819
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
11820
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
11821
+ const cached = cachedP2pLookups.get(uid);
11822
+ if (cached && cached.expires > Date.now()) {
11823
+ log(
11824
+ `UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
11825
+ );
11826
+ return cached.result;
11827
+ }
11828
+ const negCached = negCachedP2pLookups.get(uid);
11829
+ if (negCached && negCached.expires > Date.now()) {
11830
+ const remaining = negCached.expires - Date.now();
11831
+ log(
11832
+ `UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
11833
+ );
11834
+ throw negCached.error;
11835
+ }
11836
+ const inflight = inflightP2pLookups.get(uid);
11837
+ if (inflight) {
11838
+ log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
11839
+ return await inflight;
11840
+ }
11841
+ const work = this._doP2pUidLookupWork(sock, uid);
11842
+ inflightP2pLookups.set(uid, work);
11843
+ try {
11844
+ const result = await work;
11845
+ cachedP2pLookups.set(uid, {
11846
+ result,
11847
+ expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
11848
+ });
11849
+ return result;
11850
+ } catch (e) {
11851
+ const err = e instanceof Error ? e : new Error(String(e));
11852
+ negCachedP2pLookups.set(uid, {
11853
+ error: err,
11854
+ expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
11855
+ });
11856
+ throw err;
11857
+ } finally {
11858
+ inflightP2pLookups.delete(uid);
11859
+ }
11860
+ }
11861
+ async _doP2pUidLookupWork(sock, uid) {
11731
11862
  const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
11732
11863
  const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
11733
11864
  const t0 = Date.now();
@@ -12195,6 +12326,23 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
12195
12326
  log(
12196
12327
  `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
12197
12328
  );
12329
+ if (directHost && localMode === "local-direct") {
12330
+ try {
12331
+ const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
12332
+ const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
12333
+ if (sameSubnet === "mismatch") {
12334
+ log(
12335
+ `WARN: kernel-chosen source IP ${egress.localAddress} is NOT in the same subnet as ${directHost}. Some Reolink battery cams silently drop discovery packets with off-subnet source IPs. If discovery fails, check your routing table \u2014 likely a Tailscale / VPN / secondary NIC stealing the default route.`
12336
+ );
12337
+ } else {
12338
+ log(
12339
+ `egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
12340
+ );
12341
+ }
12342
+ } catch (e) {
12343
+ this.emit("debug", "egress_probe_failed", e);
12344
+ }
12345
+ }
12198
12346
  let bytesSent = 0;
12199
12347
  let pktsRecv = 0;
12200
12348
  sock.on("message", () => {
@@ -12861,35 +13009,88 @@ function decodeHeader(buf) {
12861
13009
  return { header, headerLen, messageKey };
12862
13010
  }
12863
13011
  var BaichuanFrameParser = class {
13012
+ /** Retained-but-unconsumed contiguous bytes from previous push() calls. */
12864
13013
  buffer = Buffer.alloc(0);
13014
+ /** Chunks received since the last materialization, not yet concatenated. */
13015
+ pending = [];
13016
+ /** Total bytes held in `pending` (kept in sync to avoid re-summing). */
13017
+ pendingLen = 0;
13018
+ /**
13019
+ * Total contiguous bytes (`buffer` + `pending`) required before the next
13020
+ * parse attempt can make progress. While buffered bytes stay below this,
13021
+ * incoming chunks are merely stashed in `pending` with no copy. This is
13022
+ * the mechanism that turns the worst case (a large frame fragmented over
13023
+ * many small TCP chunks) from O(n²) into O(n): we concatenate once, when
13024
+ * enough bytes have arrived, instead of on every chunk.
13025
+ *
13026
+ * Starts at 4 — the minimum needed to inspect the magic header.
13027
+ */
13028
+ needed = 4;
13029
+ /**
13030
+ * Collapse `this.buffer` + all `pending` chunks into a single contiguous
13031
+ * buffer. The retained leftover is copied at most once per materialize(),
13032
+ * and materialize() only runs when `needed` bytes are available — so a
13033
+ * fragmented frame is assembled with a single concat, not one per chunk.
13034
+ */
13035
+ materialize() {
13036
+ if (this.pendingLen === 0) return;
13037
+ if (this.buffer.length === 0 && this.pending.length === 1) {
13038
+ this.buffer = this.pending[0];
13039
+ } else {
13040
+ const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
13041
+ this.buffer = Buffer.concat(parts);
13042
+ }
13043
+ this.pending = [];
13044
+ this.pendingLen = 0;
13045
+ }
13046
+ /** Total buffered bytes, whether materialized or still pending. */
13047
+ get available() {
13048
+ return this.buffer.length + this.pendingLen;
13049
+ }
12865
13050
  push(chunk) {
12866
13051
  if (chunk.length === 0) return [];
12867
- const c = chunk;
12868
- this.buffer = this.buffer.length === 0 ? c : Buffer.concat([this.buffer, c]);
13052
+ this.pending.push(chunk);
13053
+ this.pendingLen += chunk.length;
13054
+ if (this.available < this.needed) return [];
13055
+ this.materialize();
12869
13056
  const out = [];
12870
13057
  while (true) {
12871
- if (this.buffer.length < 4) break;
13058
+ if (this.buffer.length < 4) {
13059
+ this.needed = 4;
13060
+ break;
13061
+ }
12872
13062
  if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
12873
13063
  const idx = this.buffer.indexOf(BC_MAGIC);
12874
13064
  const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
12875
13065
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
12876
13066
  if (next === -1) {
12877
13067
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
13068
+ this.needed = 4;
12878
13069
  break;
12879
13070
  }
12880
13071
  this.buffer = this.buffer.subarray(next);
12881
- if (this.buffer.length < 20) break;
13072
+ if (this.buffer.length < 20) {
13073
+ this.needed = 20;
13074
+ break;
13075
+ }
13076
+ }
13077
+ if (this.buffer.length < 20) {
13078
+ this.needed = 20;
13079
+ break;
12882
13080
  }
12883
- if (this.buffer.length < 20) break;
12884
13081
  let headerInfo;
12885
13082
  try {
12886
13083
  headerInfo = decodeHeader(this.buffer);
12887
13084
  } catch {
13085
+ this.needed = 24;
12888
13086
  break;
12889
13087
  }
12890
13088
  const { header, headerLen, messageKey } = headerInfo;
12891
13089
  const frameLen = headerLen + header.bodyLen;
12892
- if (this.buffer.length < frameLen) break;
13090
+ if (this.buffer.length < frameLen) {
13091
+ this.needed = frameLen;
13092
+ break;
13093
+ }
12893
13094
  const raw = this.buffer.subarray(0, frameLen);
12894
13095
  const body = raw.subarray(headerLen);
12895
13096
  let extLen = 0;
@@ -12901,6 +13102,7 @@ var BaichuanFrameParser = class {
12901
13102
  const payload = body.subarray(extLen);
12902
13103
  out.push({ header, body, extension, payload, messageKey, raw });
12903
13104
  this.buffer = this.buffer.subarray(frameLen);
13105
+ this.needed = 4;
12904
13106
  }
12905
13107
  return out;
12906
13108
  }