@apocaliss92/nodelink-js 0.6.0 → 0.6.2

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
  });
@@ -11318,12 +11350,28 @@ async function getServerBinding(uid, options = {}) {
11318
11350
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
11319
11351
  const logger = options.logger;
11320
11352
  if (typeof fetchImpl !== "function") {
11321
- logger?.debug?.(
11353
+ logger?.log?.(
11322
11354
  `[server-binding] global fetch unavailable; skipping cloud lookup`
11323
11355
  );
11324
11356
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11325
11357
  return void 0;
11326
11358
  }
11359
+ try {
11360
+ const apiHostname = new URL(baseUrl).hostname;
11361
+ const dns2 = await import("dns/promises");
11362
+ const answers = await dns2.lookup(apiHostname, { family: 4, all: true });
11363
+ const sinkholed = answers.find(
11364
+ (a) => a.address?.startsWith("127.") || a.address === "0.0.0.0" || a.address?.startsWith("10.") || a.address?.startsWith("192.168.") || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(a.address ?? "")
11365
+ );
11366
+ if (sinkholed) {
11367
+ logger?.log?.(
11368
+ `[server-binding] ${uid}: DNS for ${apiHostname} resolves to ${sinkholed.address} (sinkhole / /etc/hosts override). Cloud directory unreachable \u2014 falling back to the 22-hostname P2P sweep. Whitelist ${apiHostname} to enable.`
11369
+ );
11370
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11371
+ return void 0;
11372
+ }
11373
+ } catch {
11374
+ }
11327
11375
  const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
11328
11376
  const controller = new AbortController();
11329
11377
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -11334,8 +11382,8 @@ async function getServerBinding(uid, options = {}) {
11334
11382
  headers: { Accept: "application/json" }
11335
11383
  });
11336
11384
  if (!res.ok) {
11337
- logger?.debug?.(
11338
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
11385
+ logger?.log?.(
11386
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
11339
11387
  );
11340
11388
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11341
11389
  return void 0;
@@ -11343,8 +11391,15 @@ async function getServerBinding(uid, options = {}) {
11343
11391
  const json = await res.json();
11344
11392
  const parsed = parseServerBindingResponse(json);
11345
11393
  if (!parsed) {
11346
- logger?.debug?.(
11347
- `[server-binding] ${uid}: response shape did not match expectations`
11394
+ logger?.log?.(
11395
+ `[server-binding] ${uid}: response shape did not match expectations (Reolink schema change?)`
11396
+ );
11397
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11398
+ return void 0;
11399
+ }
11400
+ if (parsed.availableZones.length === 0) {
11401
+ logger?.log?.(
11402
+ `[server-binding] ${uid}: cloud returned 0 zones \u2014 UID not registered with Reolink cloud (or wrong region)`
11348
11403
  );
11349
11404
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11350
11405
  return void 0;
@@ -11363,9 +11418,23 @@ async function getServerBinding(uid, options = {}) {
11363
11418
  );
11364
11419
  return parsed;
11365
11420
  } catch (e) {
11366
- logger?.debug?.(
11367
- `[server-binding] ${uid}: ${e?.message ?? String(e)}`
11368
- );
11421
+ const msg = e?.message ?? String(e);
11422
+ const errName = e?.name;
11423
+ if (errName === "AbortError" || msg.includes("aborted")) {
11424
+ logger?.log?.(
11425
+ `[server-binding] ${uid}: timed out after ${timeoutMs}ms (cloud unreachable)`
11426
+ );
11427
+ } else if (msg.includes("ENOTFOUND") || msg.includes("EAI_AGAIN")) {
11428
+ logger?.log?.(
11429
+ `[server-binding] ${uid}: DNS failed (${msg}) \u2014 apis.reolink.com may be blocked at resolver`
11430
+ );
11431
+ } else if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH")) {
11432
+ logger?.log?.(
11433
+ `[server-binding] ${uid}: network unreachable (${msg}) \u2014 cloud port blocked`
11434
+ );
11435
+ } else {
11436
+ logger?.log?.(`[server-binding] ${uid}: fetch failed \u2014 ${msg}`);
11437
+ }
11369
11438
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11370
11439
  return void 0;
11371
11440
  } finally {
@@ -11801,12 +11870,19 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11801
11870
  const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
11802
11871
  const xml = buildC2mQ({ uid });
11803
11872
  const pkt = encodeDiscoveryPacket(tid, xml);
11873
+ const counters = { sentBytes: 0, rxBytes: 0 };
11804
11874
  return await new Promise((resolve, reject) => {
11805
11875
  const deadline = setTimeout(() => {
11806
11876
  cleanup();
11807
- reject(new Error(`P2P UID lookup timeout (${dest.host}:${dest.port})`));
11877
+ const err = new Error(
11878
+ `P2P UID lookup timeout (${dest.host}:${dest.port}) \u2014 sent=${counters.sentBytes}B rx=${counters.rxBytes}B`
11879
+ );
11880
+ err.sentBytes = counters.sentBytes;
11881
+ err.rxBytes = counters.rxBytes;
11882
+ reject(err);
11808
11883
  }, timeoutMs);
11809
11884
  const onMsg = (msg) => {
11885
+ counters.rxBytes += msg.length;
11810
11886
  try {
11811
11887
  const p = decodeBcUdpPacket(msg);
11812
11888
  if (p.kind !== "discovery") return;
@@ -11814,13 +11890,19 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11814
11890
  const qr = parseM2cQr(p.xml);
11815
11891
  if (!qr?.reg || !qr?.relay) return;
11816
11892
  cleanup();
11817
- resolve({ reg: qr.reg, relay: qr.relay });
11893
+ resolve({
11894
+ reg: qr.reg,
11895
+ relay: qr.relay,
11896
+ sentBytes: counters.sentBytes,
11897
+ rxBytes: counters.rxBytes
11898
+ });
11818
11899
  } catch {
11819
11900
  }
11820
11901
  };
11821
11902
  const send = () => {
11822
11903
  try {
11823
11904
  sock.send(pkt, dest.port, dest.host);
11905
+ counters.sentBytes += pkt.length;
11824
11906
  } catch {
11825
11907
  }
11826
11908
  };
@@ -12811,35 +12893,88 @@ function decodeHeader(buf) {
12811
12893
  return { header, headerLen, messageKey };
12812
12894
  }
12813
12895
  var BaichuanFrameParser = class {
12896
+ /** Retained-but-unconsumed contiguous bytes from previous push() calls. */
12814
12897
  buffer = Buffer.alloc(0);
12898
+ /** Chunks received since the last materialization, not yet concatenated. */
12899
+ pending = [];
12900
+ /** Total bytes held in `pending` (kept in sync to avoid re-summing). */
12901
+ pendingLen = 0;
12902
+ /**
12903
+ * Total contiguous bytes (`buffer` + `pending`) required before the next
12904
+ * parse attempt can make progress. While buffered bytes stay below this,
12905
+ * incoming chunks are merely stashed in `pending` with no copy. This is
12906
+ * the mechanism that turns the worst case (a large frame fragmented over
12907
+ * many small TCP chunks) from O(n²) into O(n): we concatenate once, when
12908
+ * enough bytes have arrived, instead of on every chunk.
12909
+ *
12910
+ * Starts at 4 — the minimum needed to inspect the magic header.
12911
+ */
12912
+ needed = 4;
12913
+ /**
12914
+ * Collapse `this.buffer` + all `pending` chunks into a single contiguous
12915
+ * buffer. The retained leftover is copied at most once per materialize(),
12916
+ * and materialize() only runs when `needed` bytes are available — so a
12917
+ * fragmented frame is assembled with a single concat, not one per chunk.
12918
+ */
12919
+ materialize() {
12920
+ if (this.pendingLen === 0) return;
12921
+ if (this.buffer.length === 0 && this.pending.length === 1) {
12922
+ this.buffer = this.pending[0];
12923
+ } else {
12924
+ const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
12925
+ this.buffer = Buffer.concat(parts);
12926
+ }
12927
+ this.pending = [];
12928
+ this.pendingLen = 0;
12929
+ }
12930
+ /** Total buffered bytes, whether materialized or still pending. */
12931
+ get available() {
12932
+ return this.buffer.length + this.pendingLen;
12933
+ }
12815
12934
  push(chunk) {
12816
12935
  if (chunk.length === 0) return [];
12817
- const c = chunk;
12818
- this.buffer = this.buffer.length === 0 ? c : Buffer.concat([this.buffer, c]);
12936
+ this.pending.push(chunk);
12937
+ this.pendingLen += chunk.length;
12938
+ if (this.available < this.needed) return [];
12939
+ this.materialize();
12819
12940
  const out = [];
12820
12941
  while (true) {
12821
- if (this.buffer.length < 4) break;
12942
+ if (this.buffer.length < 4) {
12943
+ this.needed = 4;
12944
+ break;
12945
+ }
12822
12946
  if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
12823
12947
  const idx = this.buffer.indexOf(BC_MAGIC);
12824
12948
  const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
12825
12949
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
12826
12950
  if (next === -1) {
12827
12951
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
12952
+ this.needed = 4;
12828
12953
  break;
12829
12954
  }
12830
12955
  this.buffer = this.buffer.subarray(next);
12831
- if (this.buffer.length < 20) break;
12956
+ if (this.buffer.length < 20) {
12957
+ this.needed = 20;
12958
+ break;
12959
+ }
12960
+ }
12961
+ if (this.buffer.length < 20) {
12962
+ this.needed = 20;
12963
+ break;
12832
12964
  }
12833
- if (this.buffer.length < 20) break;
12834
12965
  let headerInfo;
12835
12966
  try {
12836
12967
  headerInfo = decodeHeader(this.buffer);
12837
12968
  } catch {
12969
+ this.needed = 24;
12838
12970
  break;
12839
12971
  }
12840
12972
  const { header, headerLen, messageKey } = headerInfo;
12841
12973
  const frameLen = headerLen + header.bodyLen;
12842
- if (this.buffer.length < frameLen) break;
12974
+ if (this.buffer.length < frameLen) {
12975
+ this.needed = frameLen;
12976
+ break;
12977
+ }
12843
12978
  const raw = this.buffer.subarray(0, frameLen);
12844
12979
  const body = raw.subarray(headerLen);
12845
12980
  let extLen = 0;
@@ -12851,6 +12986,7 @@ var BaichuanFrameParser = class {
12851
12986
  const payload = body.subarray(extLen);
12852
12987
  out.push({ header, body, extension, payload, messageKey, raw });
12853
12988
  this.buffer = this.buffer.subarray(frameLen);
12989
+ this.needed = 4;
12854
12990
  }
12855
12991
  return out;
12856
12992
  }