@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.
- package/dist/{DiagnosticsTools-K4MF2VXZ.js → DiagnosticsTools-QJ3CRYGA.js} +2 -2
- package/dist/{chunk-XDVBNZGR.js → chunk-IQVVVSXO.js} +48 -16
- package/dist/{chunk-XDVBNZGR.js.map → chunk-IQVVVSXO.js.map} +1 -1
- package/dist/{chunk-EAHRVNEX.js → chunk-Q4AXRV2G.js} +214 -36
- package/dist/chunk-Q4AXRV2G.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +250 -48
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +261 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +72 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-EAHRVNEX.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-Q4AXRV2G.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
|
});
|
|
@@ -8410,6 +8442,7 @@ __export(index_exports, {
|
|
|
8410
8442
|
ReolinkHttpClient: () => ReolinkHttpClient,
|
|
8411
8443
|
Rfc4571Muxer: () => Rfc4571Muxer,
|
|
8412
8444
|
RtspBackchannel: () => RtspBackchannel,
|
|
8445
|
+
_clearP2pLookupDedupForTests: () => _clearP2pLookupDedupForTests,
|
|
8413
8446
|
_resetEmailPushBusForTests: () => _resetEmailPushBusForTests,
|
|
8414
8447
|
abilitiesHasAny: () => abilitiesHasAny,
|
|
8415
8448
|
aesDecrypt: () => aesDecrypt,
|
|
@@ -8522,6 +8555,7 @@ __export(index_exports, {
|
|
|
8522
8555
|
isH265Irap: () => isH265Irap,
|
|
8523
8556
|
isH265KeyframeAnnexB: () => isH265KeyframeAnnexB,
|
|
8524
8557
|
isNvrHubModel: () => isNvrHubModel,
|
|
8558
|
+
isSameSubnetAsAnyLocalIface: () => isSameSubnetAsAnyLocalIface,
|
|
8525
8559
|
isTcpFailureThatShouldFallbackToUdp: () => isTcpFailureThatShouldFallbackToUdp,
|
|
8526
8560
|
isUnroutableForP2P: () => isUnroutableForP2P,
|
|
8527
8561
|
isValidH264AnnexBAccessUnit: () => isValidH264AnnexBAccessUnit,
|
|
@@ -8550,6 +8584,7 @@ __export(index_exports, {
|
|
|
8550
8584
|
patchNestedTag: () => patchNestedTag,
|
|
8551
8585
|
patchShelterXml: () => patchShelterXml,
|
|
8552
8586
|
printNvrDiagnostics: () => printNvrDiagnostics,
|
|
8587
|
+
probeEgressForHost: () => probeEgressForHost,
|
|
8553
8588
|
runAllDiagnosticsConsecutively: () => runAllDiagnosticsConsecutively,
|
|
8554
8589
|
runMultifocalDiagnosticsConsecutively: () => runMultifocalDiagnosticsConsecutively,
|
|
8555
8590
|
sampleStreams: () => sampleStreams,
|
|
@@ -8625,35 +8660,88 @@ function decodeHeader(buf) {
|
|
|
8625
8660
|
return { header, headerLen, messageKey };
|
|
8626
8661
|
}
|
|
8627
8662
|
var BaichuanFrameParser = class {
|
|
8663
|
+
/** Retained-but-unconsumed contiguous bytes from previous push() calls. */
|
|
8628
8664
|
buffer = Buffer.alloc(0);
|
|
8665
|
+
/** Chunks received since the last materialization, not yet concatenated. */
|
|
8666
|
+
pending = [];
|
|
8667
|
+
/** Total bytes held in `pending` (kept in sync to avoid re-summing). */
|
|
8668
|
+
pendingLen = 0;
|
|
8669
|
+
/**
|
|
8670
|
+
* Total contiguous bytes (`buffer` + `pending`) required before the next
|
|
8671
|
+
* parse attempt can make progress. While buffered bytes stay below this,
|
|
8672
|
+
* incoming chunks are merely stashed in `pending` with no copy. This is
|
|
8673
|
+
* the mechanism that turns the worst case (a large frame fragmented over
|
|
8674
|
+
* many small TCP chunks) from O(n²) into O(n): we concatenate once, when
|
|
8675
|
+
* enough bytes have arrived, instead of on every chunk.
|
|
8676
|
+
*
|
|
8677
|
+
* Starts at 4 — the minimum needed to inspect the magic header.
|
|
8678
|
+
*/
|
|
8679
|
+
needed = 4;
|
|
8680
|
+
/**
|
|
8681
|
+
* Collapse `this.buffer` + all `pending` chunks into a single contiguous
|
|
8682
|
+
* buffer. The retained leftover is copied at most once per materialize(),
|
|
8683
|
+
* and materialize() only runs when `needed` bytes are available — so a
|
|
8684
|
+
* fragmented frame is assembled with a single concat, not one per chunk.
|
|
8685
|
+
*/
|
|
8686
|
+
materialize() {
|
|
8687
|
+
if (this.pendingLen === 0) return;
|
|
8688
|
+
if (this.buffer.length === 0 && this.pending.length === 1) {
|
|
8689
|
+
this.buffer = this.pending[0];
|
|
8690
|
+
} else {
|
|
8691
|
+
const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
|
|
8692
|
+
this.buffer = Buffer.concat(parts);
|
|
8693
|
+
}
|
|
8694
|
+
this.pending = [];
|
|
8695
|
+
this.pendingLen = 0;
|
|
8696
|
+
}
|
|
8697
|
+
/** Total buffered bytes, whether materialized or still pending. */
|
|
8698
|
+
get available() {
|
|
8699
|
+
return this.buffer.length + this.pendingLen;
|
|
8700
|
+
}
|
|
8629
8701
|
push(chunk) {
|
|
8630
8702
|
if (chunk.length === 0) return [];
|
|
8631
|
-
|
|
8632
|
-
this.
|
|
8703
|
+
this.pending.push(chunk);
|
|
8704
|
+
this.pendingLen += chunk.length;
|
|
8705
|
+
if (this.available < this.needed) return [];
|
|
8706
|
+
this.materialize();
|
|
8633
8707
|
const out = [];
|
|
8634
8708
|
while (true) {
|
|
8635
|
-
if (this.buffer.length < 4)
|
|
8709
|
+
if (this.buffer.length < 4) {
|
|
8710
|
+
this.needed = 4;
|
|
8711
|
+
break;
|
|
8712
|
+
}
|
|
8636
8713
|
if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
|
|
8637
8714
|
const idx = this.buffer.indexOf(BC_MAGIC);
|
|
8638
8715
|
const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
|
|
8639
8716
|
const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
|
|
8640
8717
|
if (next === -1) {
|
|
8641
8718
|
this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
|
|
8719
|
+
this.needed = 4;
|
|
8642
8720
|
break;
|
|
8643
8721
|
}
|
|
8644
8722
|
this.buffer = this.buffer.subarray(next);
|
|
8645
|
-
if (this.buffer.length < 20)
|
|
8723
|
+
if (this.buffer.length < 20) {
|
|
8724
|
+
this.needed = 20;
|
|
8725
|
+
break;
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
if (this.buffer.length < 20) {
|
|
8729
|
+
this.needed = 20;
|
|
8730
|
+
break;
|
|
8646
8731
|
}
|
|
8647
|
-
if (this.buffer.length < 20) break;
|
|
8648
8732
|
let headerInfo;
|
|
8649
8733
|
try {
|
|
8650
8734
|
headerInfo = decodeHeader(this.buffer);
|
|
8651
8735
|
} catch {
|
|
8736
|
+
this.needed = 24;
|
|
8652
8737
|
break;
|
|
8653
8738
|
}
|
|
8654
8739
|
const { header, headerLen, messageKey } = headerInfo;
|
|
8655
8740
|
const frameLen = headerLen + header.bodyLen;
|
|
8656
|
-
if (this.buffer.length < frameLen)
|
|
8741
|
+
if (this.buffer.length < frameLen) {
|
|
8742
|
+
this.needed = frameLen;
|
|
8743
|
+
break;
|
|
8744
|
+
}
|
|
8657
8745
|
const raw = this.buffer.subarray(0, frameLen);
|
|
8658
8746
|
const body = raw.subarray(headerLen);
|
|
8659
8747
|
let extLen = 0;
|
|
@@ -8665,6 +8753,7 @@ var BaichuanFrameParser = class {
|
|
|
8665
8753
|
const payload = body.subarray(extLen);
|
|
8666
8754
|
out.push({ header, body, extension, payload, messageKey, raw });
|
|
8667
8755
|
this.buffer = this.buffer.subarray(frameLen);
|
|
8756
|
+
this.needed = 4;
|
|
8668
8757
|
}
|
|
8669
8758
|
return out;
|
|
8670
8759
|
}
|
|
@@ -9079,8 +9168,14 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9079
9168
|
headers: { Accept: "application/json" }
|
|
9080
9169
|
});
|
|
9081
9170
|
if (!res.ok) {
|
|
9171
|
+
let bodyPreview;
|
|
9172
|
+
try {
|
|
9173
|
+
const text = await res.text();
|
|
9174
|
+
bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
|
|
9175
|
+
} catch {
|
|
9176
|
+
}
|
|
9082
9177
|
logger?.log?.(
|
|
9083
|
-
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
|
|
9178
|
+
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
|
|
9084
9179
|
);
|
|
9085
9180
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9086
9181
|
return void 0;
|
|
@@ -9196,6 +9291,68 @@ function parseServerBindingResponse(raw) {
|
|
|
9196
9291
|
}
|
|
9197
9292
|
|
|
9198
9293
|
// src/bcudp/BcUdpStream.ts
|
|
9294
|
+
async function probeEgressForHost(destHost, destPort) {
|
|
9295
|
+
return await new Promise((resolve, reject) => {
|
|
9296
|
+
const probe = import_node_dgram.default.createSocket("udp4");
|
|
9297
|
+
let settled = false;
|
|
9298
|
+
const finish = (err, out) => {
|
|
9299
|
+
if (settled) return;
|
|
9300
|
+
settled = true;
|
|
9301
|
+
try {
|
|
9302
|
+
probe.close();
|
|
9303
|
+
} catch {
|
|
9304
|
+
}
|
|
9305
|
+
if (err || !out) reject(err ?? new Error("egress probe failed"));
|
|
9306
|
+
else resolve(out);
|
|
9307
|
+
};
|
|
9308
|
+
probe.on("error", (e) => finish(e));
|
|
9309
|
+
try {
|
|
9310
|
+
probe.connect(destPort, destHost, () => {
|
|
9311
|
+
try {
|
|
9312
|
+
const a = probe.address();
|
|
9313
|
+
if (typeof a === "string") return finish(new Error("probe address is string"));
|
|
9314
|
+
finish(void 0, {
|
|
9315
|
+
localAddress: a.address,
|
|
9316
|
+
localPort: a.port
|
|
9317
|
+
});
|
|
9318
|
+
} catch (e) {
|
|
9319
|
+
finish(e);
|
|
9320
|
+
}
|
|
9321
|
+
});
|
|
9322
|
+
} catch (e) {
|
|
9323
|
+
finish(e);
|
|
9324
|
+
}
|
|
9325
|
+
});
|
|
9326
|
+
}
|
|
9327
|
+
function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
|
|
9328
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
|
|
9329
|
+
const dest = destHost.split(".").map((s) => Number(s));
|
|
9330
|
+
if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
|
|
9331
|
+
return "unknown";
|
|
9332
|
+
const ifaces = (0, import_node_os.networkInterfaces)();
|
|
9333
|
+
let ownerSubnet;
|
|
9334
|
+
for (const name of Object.keys(ifaces)) {
|
|
9335
|
+
const entries = ifaces[name];
|
|
9336
|
+
if (!entries) continue;
|
|
9337
|
+
for (const e of entries) {
|
|
9338
|
+
if (e.family !== "IPv4" || e.internal) continue;
|
|
9339
|
+
if (e.address !== srcInfo.localAddress) continue;
|
|
9340
|
+
const addr = e.address.split(".").map((s) => Number(s));
|
|
9341
|
+
const mask = e.netmask.split(".").map((s) => Number(s));
|
|
9342
|
+
if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
|
|
9343
|
+
continue;
|
|
9344
|
+
ownerSubnet = { addr, mask };
|
|
9345
|
+
break;
|
|
9346
|
+
}
|
|
9347
|
+
if (ownerSubnet) break;
|
|
9348
|
+
}
|
|
9349
|
+
if (!ownerSubnet) return "unknown";
|
|
9350
|
+
for (let i = 0; i < 4; i++) {
|
|
9351
|
+
if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
|
|
9352
|
+
return "mismatch";
|
|
9353
|
+
}
|
|
9354
|
+
return "same";
|
|
9355
|
+
}
|
|
9199
9356
|
var AckLatency = class {
|
|
9200
9357
|
currentValues = [];
|
|
9201
9358
|
lastReceiveTime = null;
|
|
@@ -9274,6 +9431,16 @@ function isUnroutableForP2P(ip) {
|
|
|
9274
9431
|
var P2P_LOOKUP_PORT = 9999;
|
|
9275
9432
|
var P2P_MAX_WAIT_MS = 15e3;
|
|
9276
9433
|
var P2P_RESEND_WAIT_MS = 500;
|
|
9434
|
+
var inflightP2pLookups = /* @__PURE__ */ new Map();
|
|
9435
|
+
var cachedP2pLookups = /* @__PURE__ */ new Map();
|
|
9436
|
+
var negCachedP2pLookups = /* @__PURE__ */ new Map();
|
|
9437
|
+
var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
|
|
9438
|
+
var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
|
|
9439
|
+
function _clearP2pLookupDedupForTests() {
|
|
9440
|
+
inflightP2pLookups.clear();
|
|
9441
|
+
cachedP2pLookups.clear();
|
|
9442
|
+
negCachedP2pLookups.clear();
|
|
9443
|
+
}
|
|
9277
9444
|
var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
9278
9445
|
opts;
|
|
9279
9446
|
/**
|
|
@@ -9362,31 +9529,14 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9362
9529
|
});
|
|
9363
9530
|
sock.on("error", (e) => this.emit("error", e));
|
|
9364
9531
|
sock.on("close", () => this.emit("close"));
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
await new Promise((resolve, reject) => {
|
|
9374
|
-
sock.once("error", reject);
|
|
9375
|
-
sock.bind(port, "0.0.0.0", () => {
|
|
9376
|
-
sock.removeListener("error", reject);
|
|
9377
|
-
resolve();
|
|
9378
|
-
});
|
|
9379
|
-
});
|
|
9380
|
-
bound = true;
|
|
9381
|
-
break;
|
|
9382
|
-
} catch {
|
|
9383
|
-
}
|
|
9384
|
-
}
|
|
9385
|
-
if (!bound) {
|
|
9386
|
-
await new Promise(
|
|
9387
|
-
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
9388
|
-
);
|
|
9389
|
-
}
|
|
9532
|
+
await new Promise((resolve, reject) => {
|
|
9533
|
+
const onErr = (e) => reject(e);
|
|
9534
|
+
sock.once("error", onErr);
|
|
9535
|
+
sock.bind(0, "0.0.0.0", () => {
|
|
9536
|
+
sock.removeListener("error", onErr);
|
|
9537
|
+
resolve();
|
|
9538
|
+
});
|
|
9539
|
+
});
|
|
9390
9540
|
if (this.opts.mode === "direct") {
|
|
9391
9541
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
9392
9542
|
this.clientId = this.opts.clientId;
|
|
@@ -9457,6 +9607,49 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9457
9607
|
this.remote = { host: connected.rhost, port: connected.rport };
|
|
9458
9608
|
}
|
|
9459
9609
|
async p2pUidLookup(sock, uid) {
|
|
9610
|
+
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
9611
|
+
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
9612
|
+
const cached = cachedP2pLookups.get(uid);
|
|
9613
|
+
if (cached && cached.expires > Date.now()) {
|
|
9614
|
+
log(
|
|
9615
|
+
`UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
|
|
9616
|
+
);
|
|
9617
|
+
return cached.result;
|
|
9618
|
+
}
|
|
9619
|
+
const negCached = negCachedP2pLookups.get(uid);
|
|
9620
|
+
if (negCached && negCached.expires > Date.now()) {
|
|
9621
|
+
const remaining = negCached.expires - Date.now();
|
|
9622
|
+
log(
|
|
9623
|
+
`UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
|
|
9624
|
+
);
|
|
9625
|
+
throw negCached.error;
|
|
9626
|
+
}
|
|
9627
|
+
const inflight = inflightP2pLookups.get(uid);
|
|
9628
|
+
if (inflight) {
|
|
9629
|
+
log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
|
|
9630
|
+
return await inflight;
|
|
9631
|
+
}
|
|
9632
|
+
const work = this._doP2pUidLookupWork(sock, uid);
|
|
9633
|
+
inflightP2pLookups.set(uid, work);
|
|
9634
|
+
try {
|
|
9635
|
+
const result = await work;
|
|
9636
|
+
cachedP2pLookups.set(uid, {
|
|
9637
|
+
result,
|
|
9638
|
+
expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
|
|
9639
|
+
});
|
|
9640
|
+
return result;
|
|
9641
|
+
} catch (e) {
|
|
9642
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
9643
|
+
negCachedP2pLookups.set(uid, {
|
|
9644
|
+
error: err,
|
|
9645
|
+
expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
|
|
9646
|
+
});
|
|
9647
|
+
throw err;
|
|
9648
|
+
} finally {
|
|
9649
|
+
inflightP2pLookups.delete(uid);
|
|
9650
|
+
}
|
|
9651
|
+
}
|
|
9652
|
+
async _doP2pUidLookupWork(sock, uid) {
|
|
9460
9653
|
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
9461
9654
|
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
9462
9655
|
const t0 = Date.now();
|
|
@@ -9924,6 +10117,23 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9924
10117
|
log(
|
|
9925
10118
|
`local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
|
|
9926
10119
|
);
|
|
10120
|
+
if (directHost && localMode === "local-direct") {
|
|
10121
|
+
try {
|
|
10122
|
+
const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
|
|
10123
|
+
const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
|
|
10124
|
+
if (sameSubnet === "mismatch") {
|
|
10125
|
+
log(
|
|
10126
|
+
`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.`
|
|
10127
|
+
);
|
|
10128
|
+
} else {
|
|
10129
|
+
log(
|
|
10130
|
+
`egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
|
|
10131
|
+
);
|
|
10132
|
+
}
|
|
10133
|
+
} catch (e) {
|
|
10134
|
+
this.emit("debug", "egress_probe_failed", e);
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
9927
10137
|
let bytesSent = 0;
|
|
9928
10138
|
let pktsRecv = 0;
|
|
9929
10139
|
sock.on("message", () => {
|
|
@@ -43477,6 +43687,7 @@ function buildInitialStatus(config) {
|
|
|
43477
43687
|
ReolinkHttpClient,
|
|
43478
43688
|
Rfc4571Muxer,
|
|
43479
43689
|
RtspBackchannel,
|
|
43690
|
+
_clearP2pLookupDedupForTests,
|
|
43480
43691
|
_resetEmailPushBusForTests,
|
|
43481
43692
|
abilitiesHasAny,
|
|
43482
43693
|
aesDecrypt,
|
|
@@ -43589,6 +43800,7 @@ function buildInitialStatus(config) {
|
|
|
43589
43800
|
isH265Irap,
|
|
43590
43801
|
isH265KeyframeAnnexB,
|
|
43591
43802
|
isNvrHubModel,
|
|
43803
|
+
isSameSubnetAsAnyLocalIface,
|
|
43592
43804
|
isTcpFailureThatShouldFallbackToUdp,
|
|
43593
43805
|
isUnroutableForP2P,
|
|
43594
43806
|
isValidH264AnnexBAccessUnit,
|
|
@@ -43617,6 +43829,7 @@ function buildInitialStatus(config) {
|
|
|
43617
43829
|
patchNestedTag,
|
|
43618
43830
|
patchShelterXml,
|
|
43619
43831
|
printNvrDiagnostics,
|
|
43832
|
+
probeEgressForHost,
|
|
43620
43833
|
runAllDiagnosticsConsecutively,
|
|
43621
43834
|
runMultifocalDiagnosticsConsecutively,
|
|
43622
43835
|
sampleStreams,
|