@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.js
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
BaichuanRtspServer,
|
|
4
4
|
ReolinkBaichuanApi,
|
|
5
5
|
autoDetectDeviceType
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-D4TKRGUP.js";
|
|
7
|
+
import "../chunk-IQVVVSXO.js";
|
|
8
8
|
import {
|
|
9
9
|
__require
|
|
10
10
|
} from "../chunk-MZUSWKF3.js";
|
package/dist/index.cjs
CHANGED
|
@@ -268,6 +268,41 @@ var init_crypto = __esm({
|
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
// src/protocol/xml.ts
|
|
271
|
+
function xmlTextRe(tag) {
|
|
272
|
+
let re = xmlTextReCache.get(tag);
|
|
273
|
+
if (re === void 0) {
|
|
274
|
+
re = new RegExp(`<${tag}>([^<]*)</${tag}>`);
|
|
275
|
+
xmlTextReCache.set(tag, re);
|
|
276
|
+
}
|
|
277
|
+
return re;
|
|
278
|
+
}
|
|
279
|
+
function xmlTagRe(tag) {
|
|
280
|
+
let re = xmlTagReCache.get(tag);
|
|
281
|
+
if (re === void 0) {
|
|
282
|
+
re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
283
|
+
xmlTagReCache.set(tag, re);
|
|
284
|
+
}
|
|
285
|
+
return re;
|
|
286
|
+
}
|
|
287
|
+
function xmlNestedRe(parent, child) {
|
|
288
|
+
const key = `${parent}\0${child}`;
|
|
289
|
+
let re = xmlNestedReCache.get(key);
|
|
290
|
+
if (re === void 0) {
|
|
291
|
+
re = new RegExp(
|
|
292
|
+
`(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
|
|
293
|
+
);
|
|
294
|
+
xmlNestedReCache.set(key, re);
|
|
295
|
+
}
|
|
296
|
+
return re;
|
|
297
|
+
}
|
|
298
|
+
function xmlStreamBlockRe(streamTag) {
|
|
299
|
+
let re = xmlStreamBlockReCache.get(streamTag);
|
|
300
|
+
if (re === void 0) {
|
|
301
|
+
re = new RegExp(`(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`);
|
|
302
|
+
xmlStreamBlockReCache.set(streamTag, re);
|
|
303
|
+
}
|
|
304
|
+
return re;
|
|
305
|
+
}
|
|
271
306
|
function xmlEscape(text) {
|
|
272
307
|
if (text === void 0 || text === null || typeof text !== "string") {
|
|
273
308
|
const error = new Error(
|
|
@@ -388,8 +423,7 @@ function buildPreviewStopXmlV11(params) {
|
|
|
388
423
|
</body>`;
|
|
389
424
|
}
|
|
390
425
|
function getXmlText(xml, tagName) {
|
|
391
|
-
const
|
|
392
|
-
const m = re.exec(xml);
|
|
426
|
+
const m = xmlTextRe(tagName).exec(xml);
|
|
393
427
|
return m?.[1];
|
|
394
428
|
}
|
|
395
429
|
function buildPtzControlXml(channelId, command, speed) {
|
|
@@ -493,13 +527,12 @@ ${xml}`;
|
|
|
493
527
|
function applyXmlTagPatch(xml, tag, value) {
|
|
494
528
|
if (value === void 0) return xml;
|
|
495
529
|
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
496
|
-
|
|
497
|
-
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
530
|
+
return xml.replace(xmlTagRe(tag), `<${tag}>${v}</${tag}>`);
|
|
498
531
|
}
|
|
499
532
|
function upsertXmlTag(xml, tag, value) {
|
|
500
533
|
if (value === void 0) return xml;
|
|
501
534
|
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
502
|
-
const re =
|
|
535
|
+
const re = xmlTagRe(tag);
|
|
503
536
|
if (re.test(xml)) {
|
|
504
537
|
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
505
538
|
}
|
|
@@ -508,16 +541,11 @@ function upsertXmlTag(xml, tag, value) {
|
|
|
508
541
|
function patchNestedTag(xml, parent, child, value) {
|
|
509
542
|
if (value === void 0) return xml;
|
|
510
543
|
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
511
|
-
|
|
512
|
-
`(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
|
|
513
|
-
);
|
|
514
|
-
return xml.replace(re, `$1${v}$2`);
|
|
544
|
+
return xml.replace(xmlNestedRe(parent, child), `$1${v}$2`);
|
|
515
545
|
}
|
|
516
546
|
function applyStreamPatch(xml, streamTag, patch) {
|
|
517
547
|
if (!patch) return xml;
|
|
518
|
-
const re =
|
|
519
|
-
`(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`
|
|
520
|
-
);
|
|
548
|
+
const re = xmlStreamBlockRe(streamTag);
|
|
521
549
|
return xml.replace(re, (_match, open, body, close) => {
|
|
522
550
|
let next = body;
|
|
523
551
|
if (patch.audio !== void 0) {
|
|
@@ -547,10 +575,9 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
547
575
|
next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
|
|
548
576
|
}
|
|
549
577
|
if (patch.gop !== void 0) {
|
|
550
|
-
|
|
551
|
-
if (gopBlockRe.test(next)) {
|
|
578
|
+
if (GOP_BLOCK_RE.test(next)) {
|
|
552
579
|
next = next.replace(
|
|
553
|
-
|
|
580
|
+
GOP_BLOCK_RE,
|
|
554
581
|
(_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
|
|
555
582
|
);
|
|
556
583
|
} else {
|
|
@@ -579,10 +606,15 @@ function buildAbilityInfoExtensionXml(username) {
|
|
|
579
606
|
<token>system, streaming, PTZ, IO, security, replay, disk, network, alarm, record, video, image</token>
|
|
580
607
|
</Extension>`;
|
|
581
608
|
}
|
|
582
|
-
var XML_HEADER;
|
|
609
|
+
var xmlTextReCache, xmlTagReCache, xmlNestedReCache, GOP_BLOCK_RE, xmlStreamBlockReCache, XML_HEADER;
|
|
583
610
|
var init_xml = __esm({
|
|
584
611
|
"src/protocol/xml.ts"() {
|
|
585
612
|
"use strict";
|
|
613
|
+
xmlTextReCache = /* @__PURE__ */ new Map();
|
|
614
|
+
xmlTagReCache = /* @__PURE__ */ new Map();
|
|
615
|
+
xmlNestedReCache = /* @__PURE__ */ new Map();
|
|
616
|
+
GOP_BLOCK_RE = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
|
|
617
|
+
xmlStreamBlockReCache = /* @__PURE__ */ new Map();
|
|
586
618
|
XML_HEADER = `<?xml version="1.0" encoding="UTF-8" ?>`;
|
|
587
619
|
}
|
|
588
620
|
});
|
|
@@ -8625,35 +8657,88 @@ function decodeHeader(buf) {
|
|
|
8625
8657
|
return { header, headerLen, messageKey };
|
|
8626
8658
|
}
|
|
8627
8659
|
var BaichuanFrameParser = class {
|
|
8660
|
+
/** Retained-but-unconsumed contiguous bytes from previous push() calls. */
|
|
8628
8661
|
buffer = Buffer.alloc(0);
|
|
8662
|
+
/** Chunks received since the last materialization, not yet concatenated. */
|
|
8663
|
+
pending = [];
|
|
8664
|
+
/** Total bytes held in `pending` (kept in sync to avoid re-summing). */
|
|
8665
|
+
pendingLen = 0;
|
|
8666
|
+
/**
|
|
8667
|
+
* Total contiguous bytes (`buffer` + `pending`) required before the next
|
|
8668
|
+
* parse attempt can make progress. While buffered bytes stay below this,
|
|
8669
|
+
* incoming chunks are merely stashed in `pending` with no copy. This is
|
|
8670
|
+
* the mechanism that turns the worst case (a large frame fragmented over
|
|
8671
|
+
* many small TCP chunks) from O(n²) into O(n): we concatenate once, when
|
|
8672
|
+
* enough bytes have arrived, instead of on every chunk.
|
|
8673
|
+
*
|
|
8674
|
+
* Starts at 4 — the minimum needed to inspect the magic header.
|
|
8675
|
+
*/
|
|
8676
|
+
needed = 4;
|
|
8677
|
+
/**
|
|
8678
|
+
* Collapse `this.buffer` + all `pending` chunks into a single contiguous
|
|
8679
|
+
* buffer. The retained leftover is copied at most once per materialize(),
|
|
8680
|
+
* and materialize() only runs when `needed` bytes are available — so a
|
|
8681
|
+
* fragmented frame is assembled with a single concat, not one per chunk.
|
|
8682
|
+
*/
|
|
8683
|
+
materialize() {
|
|
8684
|
+
if (this.pendingLen === 0) return;
|
|
8685
|
+
if (this.buffer.length === 0 && this.pending.length === 1) {
|
|
8686
|
+
this.buffer = this.pending[0];
|
|
8687
|
+
} else {
|
|
8688
|
+
const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
|
|
8689
|
+
this.buffer = Buffer.concat(parts);
|
|
8690
|
+
}
|
|
8691
|
+
this.pending = [];
|
|
8692
|
+
this.pendingLen = 0;
|
|
8693
|
+
}
|
|
8694
|
+
/** Total buffered bytes, whether materialized or still pending. */
|
|
8695
|
+
get available() {
|
|
8696
|
+
return this.buffer.length + this.pendingLen;
|
|
8697
|
+
}
|
|
8629
8698
|
push(chunk) {
|
|
8630
8699
|
if (chunk.length === 0) return [];
|
|
8631
|
-
|
|
8632
|
-
this.
|
|
8700
|
+
this.pending.push(chunk);
|
|
8701
|
+
this.pendingLen += chunk.length;
|
|
8702
|
+
if (this.available < this.needed) return [];
|
|
8703
|
+
this.materialize();
|
|
8633
8704
|
const out = [];
|
|
8634
8705
|
while (true) {
|
|
8635
|
-
if (this.buffer.length < 4)
|
|
8706
|
+
if (this.buffer.length < 4) {
|
|
8707
|
+
this.needed = 4;
|
|
8708
|
+
break;
|
|
8709
|
+
}
|
|
8636
8710
|
if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
|
|
8637
8711
|
const idx = this.buffer.indexOf(BC_MAGIC);
|
|
8638
8712
|
const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
|
|
8639
8713
|
const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
|
|
8640
8714
|
if (next === -1) {
|
|
8641
8715
|
this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
|
|
8716
|
+
this.needed = 4;
|
|
8642
8717
|
break;
|
|
8643
8718
|
}
|
|
8644
8719
|
this.buffer = this.buffer.subarray(next);
|
|
8645
|
-
if (this.buffer.length < 20)
|
|
8720
|
+
if (this.buffer.length < 20) {
|
|
8721
|
+
this.needed = 20;
|
|
8722
|
+
break;
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
if (this.buffer.length < 20) {
|
|
8726
|
+
this.needed = 20;
|
|
8727
|
+
break;
|
|
8646
8728
|
}
|
|
8647
|
-
if (this.buffer.length < 20) break;
|
|
8648
8729
|
let headerInfo;
|
|
8649
8730
|
try {
|
|
8650
8731
|
headerInfo = decodeHeader(this.buffer);
|
|
8651
8732
|
} catch {
|
|
8733
|
+
this.needed = 24;
|
|
8652
8734
|
break;
|
|
8653
8735
|
}
|
|
8654
8736
|
const { header, headerLen, messageKey } = headerInfo;
|
|
8655
8737
|
const frameLen = headerLen + header.bodyLen;
|
|
8656
|
-
if (this.buffer.length < frameLen)
|
|
8738
|
+
if (this.buffer.length < frameLen) {
|
|
8739
|
+
this.needed = frameLen;
|
|
8740
|
+
break;
|
|
8741
|
+
}
|
|
8657
8742
|
const raw = this.buffer.subarray(0, frameLen);
|
|
8658
8743
|
const body = raw.subarray(headerLen);
|
|
8659
8744
|
let extLen = 0;
|
|
@@ -8665,6 +8750,7 @@ var BaichuanFrameParser = class {
|
|
|
8665
8750
|
const payload = body.subarray(extLen);
|
|
8666
8751
|
out.push({ header, body, extension, payload, messageKey, raw });
|
|
8667
8752
|
this.buffer = this.buffer.subarray(frameLen);
|
|
8753
|
+
this.needed = 4;
|
|
8668
8754
|
}
|
|
8669
8755
|
return out;
|
|
8670
8756
|
}
|
|
@@ -9047,12 +9133,28 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9047
9133
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
9048
9134
|
const logger = options.logger;
|
|
9049
9135
|
if (typeof fetchImpl !== "function") {
|
|
9050
|
-
logger?.
|
|
9136
|
+
logger?.log?.(
|
|
9051
9137
|
`[server-binding] global fetch unavailable; skipping cloud lookup`
|
|
9052
9138
|
);
|
|
9053
9139
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9054
9140
|
return void 0;
|
|
9055
9141
|
}
|
|
9142
|
+
try {
|
|
9143
|
+
const apiHostname = new URL(baseUrl).hostname;
|
|
9144
|
+
const dns2 = await import("dns/promises");
|
|
9145
|
+
const answers = await dns2.lookup(apiHostname, { family: 4, all: true });
|
|
9146
|
+
const sinkholed = answers.find(
|
|
9147
|
+
(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 ?? "")
|
|
9148
|
+
);
|
|
9149
|
+
if (sinkholed) {
|
|
9150
|
+
logger?.log?.(
|
|
9151
|
+
`[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.`
|
|
9152
|
+
);
|
|
9153
|
+
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9154
|
+
return void 0;
|
|
9155
|
+
}
|
|
9156
|
+
} catch {
|
|
9157
|
+
}
|
|
9056
9158
|
const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
|
|
9057
9159
|
const controller = new AbortController();
|
|
9058
9160
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -9063,8 +9165,8 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9063
9165
|
headers: { Accept: "application/json" }
|
|
9064
9166
|
});
|
|
9065
9167
|
if (!res.ok) {
|
|
9066
|
-
logger?.
|
|
9067
|
-
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
|
|
9168
|
+
logger?.log?.(
|
|
9169
|
+
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
|
|
9068
9170
|
);
|
|
9069
9171
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9070
9172
|
return void 0;
|
|
@@ -9072,8 +9174,15 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9072
9174
|
const json = await res.json();
|
|
9073
9175
|
const parsed = parseServerBindingResponse(json);
|
|
9074
9176
|
if (!parsed) {
|
|
9075
|
-
logger?.
|
|
9076
|
-
`[server-binding] ${uid}: response shape did not match expectations`
|
|
9177
|
+
logger?.log?.(
|
|
9178
|
+
`[server-binding] ${uid}: response shape did not match expectations (Reolink schema change?)`
|
|
9179
|
+
);
|
|
9180
|
+
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9181
|
+
return void 0;
|
|
9182
|
+
}
|
|
9183
|
+
if (parsed.availableZones.length === 0) {
|
|
9184
|
+
logger?.log?.(
|
|
9185
|
+
`[server-binding] ${uid}: cloud returned 0 zones \u2014 UID not registered with Reolink cloud (or wrong region)`
|
|
9077
9186
|
);
|
|
9078
9187
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9079
9188
|
return void 0;
|
|
@@ -9092,9 +9201,23 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9092
9201
|
);
|
|
9093
9202
|
return parsed;
|
|
9094
9203
|
} catch (e) {
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
)
|
|
9204
|
+
const msg = e?.message ?? String(e);
|
|
9205
|
+
const errName = e?.name;
|
|
9206
|
+
if (errName === "AbortError" || msg.includes("aborted")) {
|
|
9207
|
+
logger?.log?.(
|
|
9208
|
+
`[server-binding] ${uid}: timed out after ${timeoutMs}ms (cloud unreachable)`
|
|
9209
|
+
);
|
|
9210
|
+
} else if (msg.includes("ENOTFOUND") || msg.includes("EAI_AGAIN")) {
|
|
9211
|
+
logger?.log?.(
|
|
9212
|
+
`[server-binding] ${uid}: DNS failed (${msg}) \u2014 apis.reolink.com may be blocked at resolver`
|
|
9213
|
+
);
|
|
9214
|
+
} else if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH")) {
|
|
9215
|
+
logger?.log?.(
|
|
9216
|
+
`[server-binding] ${uid}: network unreachable (${msg}) \u2014 cloud port blocked`
|
|
9217
|
+
);
|
|
9218
|
+
} else {
|
|
9219
|
+
logger?.log?.(`[server-binding] ${uid}: fetch failed \u2014 ${msg}`);
|
|
9220
|
+
}
|
|
9098
9221
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9099
9222
|
return void 0;
|
|
9100
9223
|
} finally {
|
|
@@ -9530,12 +9653,19 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9530
9653
|
const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
|
|
9531
9654
|
const xml = buildC2mQ({ uid });
|
|
9532
9655
|
const pkt = encodeDiscoveryPacket(tid, xml);
|
|
9656
|
+
const counters = { sentBytes: 0, rxBytes: 0 };
|
|
9533
9657
|
return await new Promise((resolve, reject) => {
|
|
9534
9658
|
const deadline = setTimeout(() => {
|
|
9535
9659
|
cleanup();
|
|
9536
|
-
|
|
9660
|
+
const err = new Error(
|
|
9661
|
+
`P2P UID lookup timeout (${dest.host}:${dest.port}) \u2014 sent=${counters.sentBytes}B rx=${counters.rxBytes}B`
|
|
9662
|
+
);
|
|
9663
|
+
err.sentBytes = counters.sentBytes;
|
|
9664
|
+
err.rxBytes = counters.rxBytes;
|
|
9665
|
+
reject(err);
|
|
9537
9666
|
}, timeoutMs);
|
|
9538
9667
|
const onMsg = (msg) => {
|
|
9668
|
+
counters.rxBytes += msg.length;
|
|
9539
9669
|
try {
|
|
9540
9670
|
const p = decodeBcUdpPacket(msg);
|
|
9541
9671
|
if (p.kind !== "discovery") return;
|
|
@@ -9543,13 +9673,19 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9543
9673
|
const qr = parseM2cQr(p.xml);
|
|
9544
9674
|
if (!qr?.reg || !qr?.relay) return;
|
|
9545
9675
|
cleanup();
|
|
9546
|
-
resolve({
|
|
9676
|
+
resolve({
|
|
9677
|
+
reg: qr.reg,
|
|
9678
|
+
relay: qr.relay,
|
|
9679
|
+
sentBytes: counters.sentBytes,
|
|
9680
|
+
rxBytes: counters.rxBytes
|
|
9681
|
+
});
|
|
9547
9682
|
} catch {
|
|
9548
9683
|
}
|
|
9549
9684
|
};
|
|
9550
9685
|
const send = () => {
|
|
9551
9686
|
try {
|
|
9552
9687
|
sock.send(pkt, dest.port, dest.host);
|
|
9688
|
+
counters.sentBytes += pkt.length;
|
|
9553
9689
|
} catch {
|
|
9554
9690
|
}
|
|
9555
9691
|
};
|