@apocaliss92/nodelink-js 0.6.2 → 0.6.4
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/{chunk-D4TKRGUP.js → chunk-UL34MR4L.js} +153 -28
- package/dist/chunk-UL34MR4L.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +144 -27
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +155 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -2
- package/dist/index.d.ts +59 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-D4TKRGUP.js.map +0 -1
package/dist/cli/rtsp-server.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -8442,6 +8442,7 @@ __export(index_exports, {
|
|
|
8442
8442
|
ReolinkHttpClient: () => ReolinkHttpClient,
|
|
8443
8443
|
Rfc4571Muxer: () => Rfc4571Muxer,
|
|
8444
8444
|
RtspBackchannel: () => RtspBackchannel,
|
|
8445
|
+
_clearP2pLookupDedupForTests: () => _clearP2pLookupDedupForTests,
|
|
8445
8446
|
_resetEmailPushBusForTests: () => _resetEmailPushBusForTests,
|
|
8446
8447
|
abilitiesHasAny: () => abilitiesHasAny,
|
|
8447
8448
|
aesDecrypt: () => aesDecrypt,
|
|
@@ -8554,6 +8555,7 @@ __export(index_exports, {
|
|
|
8554
8555
|
isH265Irap: () => isH265Irap,
|
|
8555
8556
|
isH265KeyframeAnnexB: () => isH265KeyframeAnnexB,
|
|
8556
8557
|
isNvrHubModel: () => isNvrHubModel,
|
|
8558
|
+
isSameSubnetAsAnyLocalIface: () => isSameSubnetAsAnyLocalIface,
|
|
8557
8559
|
isTcpFailureThatShouldFallbackToUdp: () => isTcpFailureThatShouldFallbackToUdp,
|
|
8558
8560
|
isUnroutableForP2P: () => isUnroutableForP2P,
|
|
8559
8561
|
isValidH264AnnexBAccessUnit: () => isValidH264AnnexBAccessUnit,
|
|
@@ -8582,6 +8584,7 @@ __export(index_exports, {
|
|
|
8582
8584
|
patchNestedTag: () => patchNestedTag,
|
|
8583
8585
|
patchShelterXml: () => patchShelterXml,
|
|
8584
8586
|
printNvrDiagnostics: () => printNvrDiagnostics,
|
|
8587
|
+
probeEgressForHost: () => probeEgressForHost,
|
|
8585
8588
|
runAllDiagnosticsConsecutively: () => runAllDiagnosticsConsecutively,
|
|
8586
8589
|
runMultifocalDiagnosticsConsecutively: () => runMultifocalDiagnosticsConsecutively,
|
|
8587
8590
|
sampleStreams: () => sampleStreams,
|
|
@@ -9123,6 +9126,7 @@ function readCache(uid, now) {
|
|
|
9123
9126
|
}
|
|
9124
9127
|
async function getServerBinding(uid, options = {}) {
|
|
9125
9128
|
if (!uid || typeof uid !== "string") return void 0;
|
|
9129
|
+
uid = uid.toUpperCase();
|
|
9126
9130
|
const now = Date.now();
|
|
9127
9131
|
const cached = readCache(uid, now);
|
|
9128
9132
|
if (cached?.kind === "ok") return cached.response;
|
|
@@ -9165,8 +9169,14 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9165
9169
|
headers: { Accept: "application/json" }
|
|
9166
9170
|
});
|
|
9167
9171
|
if (!res.ok) {
|
|
9172
|
+
let bodyPreview;
|
|
9173
|
+
try {
|
|
9174
|
+
const text = await res.text();
|
|
9175
|
+
bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
|
|
9176
|
+
} catch {
|
|
9177
|
+
}
|
|
9168
9178
|
logger?.log?.(
|
|
9169
|
-
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
|
|
9179
|
+
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
|
|
9170
9180
|
);
|
|
9171
9181
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9172
9182
|
return void 0;
|
|
@@ -9282,6 +9292,68 @@ function parseServerBindingResponse(raw) {
|
|
|
9282
9292
|
}
|
|
9283
9293
|
|
|
9284
9294
|
// src/bcudp/BcUdpStream.ts
|
|
9295
|
+
async function probeEgressForHost(destHost, destPort) {
|
|
9296
|
+
return await new Promise((resolve, reject) => {
|
|
9297
|
+
const probe = import_node_dgram.default.createSocket("udp4");
|
|
9298
|
+
let settled = false;
|
|
9299
|
+
const finish = (err, out) => {
|
|
9300
|
+
if (settled) return;
|
|
9301
|
+
settled = true;
|
|
9302
|
+
try {
|
|
9303
|
+
probe.close();
|
|
9304
|
+
} catch {
|
|
9305
|
+
}
|
|
9306
|
+
if (err || !out) reject(err ?? new Error("egress probe failed"));
|
|
9307
|
+
else resolve(out);
|
|
9308
|
+
};
|
|
9309
|
+
probe.on("error", (e) => finish(e));
|
|
9310
|
+
try {
|
|
9311
|
+
probe.connect(destPort, destHost, () => {
|
|
9312
|
+
try {
|
|
9313
|
+
const a = probe.address();
|
|
9314
|
+
if (typeof a === "string") return finish(new Error("probe address is string"));
|
|
9315
|
+
finish(void 0, {
|
|
9316
|
+
localAddress: a.address,
|
|
9317
|
+
localPort: a.port
|
|
9318
|
+
});
|
|
9319
|
+
} catch (e) {
|
|
9320
|
+
finish(e);
|
|
9321
|
+
}
|
|
9322
|
+
});
|
|
9323
|
+
} catch (e) {
|
|
9324
|
+
finish(e);
|
|
9325
|
+
}
|
|
9326
|
+
});
|
|
9327
|
+
}
|
|
9328
|
+
function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
|
|
9329
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
|
|
9330
|
+
const dest = destHost.split(".").map((s) => Number(s));
|
|
9331
|
+
if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
|
|
9332
|
+
return "unknown";
|
|
9333
|
+
const ifaces = (0, import_node_os.networkInterfaces)();
|
|
9334
|
+
let ownerSubnet;
|
|
9335
|
+
for (const name of Object.keys(ifaces)) {
|
|
9336
|
+
const entries = ifaces[name];
|
|
9337
|
+
if (!entries) continue;
|
|
9338
|
+
for (const e of entries) {
|
|
9339
|
+
if (e.family !== "IPv4" || e.internal) continue;
|
|
9340
|
+
if (e.address !== srcInfo.localAddress) continue;
|
|
9341
|
+
const addr = e.address.split(".").map((s) => Number(s));
|
|
9342
|
+
const mask = e.netmask.split(".").map((s) => Number(s));
|
|
9343
|
+
if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
|
|
9344
|
+
continue;
|
|
9345
|
+
ownerSubnet = { addr, mask };
|
|
9346
|
+
break;
|
|
9347
|
+
}
|
|
9348
|
+
if (ownerSubnet) break;
|
|
9349
|
+
}
|
|
9350
|
+
if (!ownerSubnet) return "unknown";
|
|
9351
|
+
for (let i = 0; i < 4; i++) {
|
|
9352
|
+
if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
|
|
9353
|
+
return "mismatch";
|
|
9354
|
+
}
|
|
9355
|
+
return "same";
|
|
9356
|
+
}
|
|
9285
9357
|
var AckLatency = class {
|
|
9286
9358
|
currentValues = [];
|
|
9287
9359
|
lastReceiveTime = null;
|
|
@@ -9360,6 +9432,16 @@ function isUnroutableForP2P(ip) {
|
|
|
9360
9432
|
var P2P_LOOKUP_PORT = 9999;
|
|
9361
9433
|
var P2P_MAX_WAIT_MS = 15e3;
|
|
9362
9434
|
var P2P_RESEND_WAIT_MS = 500;
|
|
9435
|
+
var inflightP2pLookups = /* @__PURE__ */ new Map();
|
|
9436
|
+
var cachedP2pLookups = /* @__PURE__ */ new Map();
|
|
9437
|
+
var negCachedP2pLookups = /* @__PURE__ */ new Map();
|
|
9438
|
+
var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
|
|
9439
|
+
var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
|
|
9440
|
+
function _clearP2pLookupDedupForTests() {
|
|
9441
|
+
inflightP2pLookups.clear();
|
|
9442
|
+
cachedP2pLookups.clear();
|
|
9443
|
+
negCachedP2pLookups.clear();
|
|
9444
|
+
}
|
|
9363
9445
|
var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
9364
9446
|
opts;
|
|
9365
9447
|
/**
|
|
@@ -9448,31 +9530,14 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9448
9530
|
});
|
|
9449
9531
|
sock.on("error", (e) => this.emit("error", e));
|
|
9450
9532
|
sock.on("close", () => this.emit("close"));
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
await new Promise((resolve, reject) => {
|
|
9460
|
-
sock.once("error", reject);
|
|
9461
|
-
sock.bind(port, "0.0.0.0", () => {
|
|
9462
|
-
sock.removeListener("error", reject);
|
|
9463
|
-
resolve();
|
|
9464
|
-
});
|
|
9465
|
-
});
|
|
9466
|
-
bound = true;
|
|
9467
|
-
break;
|
|
9468
|
-
} catch {
|
|
9469
|
-
}
|
|
9470
|
-
}
|
|
9471
|
-
if (!bound) {
|
|
9472
|
-
await new Promise(
|
|
9473
|
-
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
9474
|
-
);
|
|
9475
|
-
}
|
|
9533
|
+
await new Promise((resolve, reject) => {
|
|
9534
|
+
const onErr = (e) => reject(e);
|
|
9535
|
+
sock.once("error", onErr);
|
|
9536
|
+
sock.bind(0, "0.0.0.0", () => {
|
|
9537
|
+
sock.removeListener("error", onErr);
|
|
9538
|
+
resolve();
|
|
9539
|
+
});
|
|
9540
|
+
});
|
|
9476
9541
|
if (this.opts.mode === "direct") {
|
|
9477
9542
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
9478
9543
|
this.clientId = this.opts.clientId;
|
|
@@ -9543,6 +9608,49 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9543
9608
|
this.remote = { host: connected.rhost, port: connected.rport };
|
|
9544
9609
|
}
|
|
9545
9610
|
async p2pUidLookup(sock, uid) {
|
|
9611
|
+
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
9612
|
+
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
9613
|
+
const cached = cachedP2pLookups.get(uid);
|
|
9614
|
+
if (cached && cached.expires > Date.now()) {
|
|
9615
|
+
log(
|
|
9616
|
+
`UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
|
|
9617
|
+
);
|
|
9618
|
+
return cached.result;
|
|
9619
|
+
}
|
|
9620
|
+
const negCached = negCachedP2pLookups.get(uid);
|
|
9621
|
+
if (negCached && negCached.expires > Date.now()) {
|
|
9622
|
+
const remaining = negCached.expires - Date.now();
|
|
9623
|
+
log(
|
|
9624
|
+
`UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
|
|
9625
|
+
);
|
|
9626
|
+
throw negCached.error;
|
|
9627
|
+
}
|
|
9628
|
+
const inflight = inflightP2pLookups.get(uid);
|
|
9629
|
+
if (inflight) {
|
|
9630
|
+
log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
|
|
9631
|
+
return await inflight;
|
|
9632
|
+
}
|
|
9633
|
+
const work = this._doP2pUidLookupWork(sock, uid);
|
|
9634
|
+
inflightP2pLookups.set(uid, work);
|
|
9635
|
+
try {
|
|
9636
|
+
const result = await work;
|
|
9637
|
+
cachedP2pLookups.set(uid, {
|
|
9638
|
+
result,
|
|
9639
|
+
expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
|
|
9640
|
+
});
|
|
9641
|
+
return result;
|
|
9642
|
+
} catch (e) {
|
|
9643
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
9644
|
+
negCachedP2pLookups.set(uid, {
|
|
9645
|
+
error: err,
|
|
9646
|
+
expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
|
|
9647
|
+
});
|
|
9648
|
+
throw err;
|
|
9649
|
+
} finally {
|
|
9650
|
+
inflightP2pLookups.delete(uid);
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9653
|
+
async _doP2pUidLookupWork(sock, uid) {
|
|
9546
9654
|
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
9547
9655
|
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
9548
9656
|
const t0 = Date.now();
|
|
@@ -10010,6 +10118,23 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
10010
10118
|
log(
|
|
10011
10119
|
`local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
|
|
10012
10120
|
);
|
|
10121
|
+
if (directHost && localMode === "local-direct") {
|
|
10122
|
+
try {
|
|
10123
|
+
const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
|
|
10124
|
+
const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
|
|
10125
|
+
if (sameSubnet === "mismatch") {
|
|
10126
|
+
log(
|
|
10127
|
+
`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.`
|
|
10128
|
+
);
|
|
10129
|
+
} else {
|
|
10130
|
+
log(
|
|
10131
|
+
`egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
|
|
10132
|
+
);
|
|
10133
|
+
}
|
|
10134
|
+
} catch (e) {
|
|
10135
|
+
this.emit("debug", "egress_probe_failed", e);
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
10013
10138
|
let bytesSent = 0;
|
|
10014
10139
|
let pktsRecv = 0;
|
|
10015
10140
|
sock.on("message", () => {
|
|
@@ -40853,7 +40978,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
|
|
|
40853
40978
|
return methods.filter((m) => m === "local-direct");
|
|
40854
40979
|
}
|
|
40855
40980
|
function normalizeUid(uid) {
|
|
40856
|
-
const v = uid?.trim();
|
|
40981
|
+
const v = uid?.trim().toUpperCase();
|
|
40857
40982
|
return v ? v : void 0;
|
|
40858
40983
|
}
|
|
40859
40984
|
function maskUid(uid) {
|
|
@@ -43563,6 +43688,7 @@ function buildInitialStatus(config) {
|
|
|
43563
43688
|
ReolinkHttpClient,
|
|
43564
43689
|
Rfc4571Muxer,
|
|
43565
43690
|
RtspBackchannel,
|
|
43691
|
+
_clearP2pLookupDedupForTests,
|
|
43566
43692
|
_resetEmailPushBusForTests,
|
|
43567
43693
|
abilitiesHasAny,
|
|
43568
43694
|
aesDecrypt,
|
|
@@ -43675,6 +43801,7 @@ function buildInitialStatus(config) {
|
|
|
43675
43801
|
isH265Irap,
|
|
43676
43802
|
isH265KeyframeAnnexB,
|
|
43677
43803
|
isNvrHubModel,
|
|
43804
|
+
isSameSubnetAsAnyLocalIface,
|
|
43678
43805
|
isTcpFailureThatShouldFallbackToUdp,
|
|
43679
43806
|
isUnroutableForP2P,
|
|
43680
43807
|
isValidH264AnnexBAccessUnit,
|
|
@@ -43703,6 +43830,7 @@ function buildInitialStatus(config) {
|
|
|
43703
43830
|
patchNestedTag,
|
|
43704
43831
|
patchShelterXml,
|
|
43705
43832
|
printNvrDiagnostics,
|
|
43833
|
+
probeEgressForHost,
|
|
43706
43834
|
runAllDiagnosticsConsecutively,
|
|
43707
43835
|
runMultifocalDiagnosticsConsecutively,
|
|
43708
43836
|
sampleStreams,
|