@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.
- package/dist/{DiagnosticsTools-K4MF2VXZ.js → DiagnosticsTools-QJ3CRYGA.js} +2 -2
- package/dist/{chunk-7HSTETZR.js → chunk-D4TKRGUP.js} +124 -20
- package/dist/chunk-D4TKRGUP.js.map +1 -0
- package/dist/{chunk-XDVBNZGR.js → chunk-IQVVVSXO.js} +48 -16
- package/dist/{chunk-XDVBNZGR.js.map → chunk-IQVVVSXO.js.map} +1 -1
- package/dist/cli/rtsp-server.cjs +168 -32
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +168 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-7HSTETZR.js.map +0 -1
- /package/dist/{DiagnosticsTools-K4MF2VXZ.js.map → DiagnosticsTools-QJ3CRYGA.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2854
|
-
if (gopBlockRe.test(next)) {
|
|
2881
|
+
if (GOP_BLOCK_RE.test(next)) {
|
|
2855
2882
|
next = next.replace(
|
|
2856
|
-
|
|
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?.
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
11367
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
12818
|
-
this.
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
}
|