@apocaliss92/nodelink-js 0.6.2 → 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/{chunk-D4TKRGUP.js → chunk-Q4AXRV2G.js} +151 -27
- package/dist/chunk-Q4AXRV2G.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +142 -26
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +153 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +37 -0
- 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,
|
|
@@ -9165,8 +9168,14 @@ async function getServerBinding(uid, options = {}) {
|
|
|
9165
9168
|
headers: { Accept: "application/json" }
|
|
9166
9169
|
});
|
|
9167
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
|
+
}
|
|
9168
9177
|
logger?.log?.(
|
|
9169
|
-
`[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}` : "")
|
|
9170
9179
|
);
|
|
9171
9180
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
9172
9181
|
return void 0;
|
|
@@ -9282,6 +9291,68 @@ function parseServerBindingResponse(raw) {
|
|
|
9282
9291
|
}
|
|
9283
9292
|
|
|
9284
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
|
+
}
|
|
9285
9356
|
var AckLatency = class {
|
|
9286
9357
|
currentValues = [];
|
|
9287
9358
|
lastReceiveTime = null;
|
|
@@ -9360,6 +9431,16 @@ function isUnroutableForP2P(ip) {
|
|
|
9360
9431
|
var P2P_LOOKUP_PORT = 9999;
|
|
9361
9432
|
var P2P_MAX_WAIT_MS = 15e3;
|
|
9362
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
|
+
}
|
|
9363
9444
|
var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
9364
9445
|
opts;
|
|
9365
9446
|
/**
|
|
@@ -9448,31 +9529,14 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9448
9529
|
});
|
|
9449
9530
|
sock.on("error", (e) => this.emit("error", e));
|
|
9450
9531
|
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
|
-
}
|
|
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
|
+
});
|
|
9476
9540
|
if (this.opts.mode === "direct") {
|
|
9477
9541
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
9478
9542
|
this.clientId = this.opts.clientId;
|
|
@@ -9543,6 +9607,49 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9543
9607
|
this.remote = { host: connected.rhost, port: connected.rport };
|
|
9544
9608
|
}
|
|
9545
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) {
|
|
9546
9653
|
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
9547
9654
|
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
9548
9655
|
const t0 = Date.now();
|
|
@@ -10010,6 +10117,23 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
10010
10117
|
log(
|
|
10011
10118
|
`local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
|
|
10012
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
|
+
}
|
|
10013
10137
|
let bytesSent = 0;
|
|
10014
10138
|
let pktsRecv = 0;
|
|
10015
10139
|
sock.on("message", () => {
|
|
@@ -43563,6 +43687,7 @@ function buildInitialStatus(config) {
|
|
|
43563
43687
|
ReolinkHttpClient,
|
|
43564
43688
|
Rfc4571Muxer,
|
|
43565
43689
|
RtspBackchannel,
|
|
43690
|
+
_clearP2pLookupDedupForTests,
|
|
43566
43691
|
_resetEmailPushBusForTests,
|
|
43567
43692
|
abilitiesHasAny,
|
|
43568
43693
|
aesDecrypt,
|
|
@@ -43675,6 +43800,7 @@ function buildInitialStatus(config) {
|
|
|
43675
43800
|
isH265Irap,
|
|
43676
43801
|
isH265KeyframeAnnexB,
|
|
43677
43802
|
isNvrHubModel,
|
|
43803
|
+
isSameSubnetAsAnyLocalIface,
|
|
43678
43804
|
isTcpFailureThatShouldFallbackToUdp,
|
|
43679
43805
|
isUnroutableForP2P,
|
|
43680
43806
|
isValidH264AnnexBAccessUnit,
|
|
@@ -43703,6 +43829,7 @@ function buildInitialStatus(config) {
|
|
|
43703
43829
|
patchNestedTag,
|
|
43704
43830
|
patchShelterXml,
|
|
43705
43831
|
printNvrDiagnostics,
|
|
43832
|
+
probeEgressForHost,
|
|
43706
43833
|
runAllDiagnosticsConsecutively,
|
|
43707
43834
|
runMultifocalDiagnosticsConsecutively,
|
|
43708
43835
|
sampleStreams,
|