@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
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
runAllDiagnosticsConsecutively,
|
|
31
31
|
runMultifocalDiagnosticsConsecutively,
|
|
32
32
|
xmlEscape
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-IQVVVSXO.js";
|
|
34
34
|
import {
|
|
35
35
|
BC_CLASS_FILE_DOWNLOAD,
|
|
36
36
|
BC_CLASS_LEGACY,
|
|
@@ -232,35 +232,88 @@ function decodeHeader(buf) {
|
|
|
232
232
|
return { header, headerLen, messageKey };
|
|
233
233
|
}
|
|
234
234
|
var BaichuanFrameParser = class {
|
|
235
|
+
/** Retained-but-unconsumed contiguous bytes from previous push() calls. */
|
|
235
236
|
buffer = Buffer.alloc(0);
|
|
237
|
+
/** Chunks received since the last materialization, not yet concatenated. */
|
|
238
|
+
pending = [];
|
|
239
|
+
/** Total bytes held in `pending` (kept in sync to avoid re-summing). */
|
|
240
|
+
pendingLen = 0;
|
|
241
|
+
/**
|
|
242
|
+
* Total contiguous bytes (`buffer` + `pending`) required before the next
|
|
243
|
+
* parse attempt can make progress. While buffered bytes stay below this,
|
|
244
|
+
* incoming chunks are merely stashed in `pending` with no copy. This is
|
|
245
|
+
* the mechanism that turns the worst case (a large frame fragmented over
|
|
246
|
+
* many small TCP chunks) from O(n²) into O(n): we concatenate once, when
|
|
247
|
+
* enough bytes have arrived, instead of on every chunk.
|
|
248
|
+
*
|
|
249
|
+
* Starts at 4 — the minimum needed to inspect the magic header.
|
|
250
|
+
*/
|
|
251
|
+
needed = 4;
|
|
252
|
+
/**
|
|
253
|
+
* Collapse `this.buffer` + all `pending` chunks into a single contiguous
|
|
254
|
+
* buffer. The retained leftover is copied at most once per materialize(),
|
|
255
|
+
* and materialize() only runs when `needed` bytes are available — so a
|
|
256
|
+
* fragmented frame is assembled with a single concat, not one per chunk.
|
|
257
|
+
*/
|
|
258
|
+
materialize() {
|
|
259
|
+
if (this.pendingLen === 0) return;
|
|
260
|
+
if (this.buffer.length === 0 && this.pending.length === 1) {
|
|
261
|
+
this.buffer = this.pending[0];
|
|
262
|
+
} else {
|
|
263
|
+
const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
|
|
264
|
+
this.buffer = Buffer.concat(parts);
|
|
265
|
+
}
|
|
266
|
+
this.pending = [];
|
|
267
|
+
this.pendingLen = 0;
|
|
268
|
+
}
|
|
269
|
+
/** Total buffered bytes, whether materialized or still pending. */
|
|
270
|
+
get available() {
|
|
271
|
+
return this.buffer.length + this.pendingLen;
|
|
272
|
+
}
|
|
236
273
|
push(chunk) {
|
|
237
274
|
if (chunk.length === 0) return [];
|
|
238
|
-
|
|
239
|
-
this.
|
|
275
|
+
this.pending.push(chunk);
|
|
276
|
+
this.pendingLen += chunk.length;
|
|
277
|
+
if (this.available < this.needed) return [];
|
|
278
|
+
this.materialize();
|
|
240
279
|
const out = [];
|
|
241
280
|
while (true) {
|
|
242
|
-
if (this.buffer.length < 4)
|
|
281
|
+
if (this.buffer.length < 4) {
|
|
282
|
+
this.needed = 4;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
243
285
|
if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
|
|
244
286
|
const idx = this.buffer.indexOf(BC_MAGIC);
|
|
245
287
|
const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
|
|
246
288
|
const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
|
|
247
289
|
if (next === -1) {
|
|
248
290
|
this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
|
|
291
|
+
this.needed = 4;
|
|
249
292
|
break;
|
|
250
293
|
}
|
|
251
294
|
this.buffer = this.buffer.subarray(next);
|
|
252
|
-
if (this.buffer.length < 20)
|
|
295
|
+
if (this.buffer.length < 20) {
|
|
296
|
+
this.needed = 20;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (this.buffer.length < 20) {
|
|
301
|
+
this.needed = 20;
|
|
302
|
+
break;
|
|
253
303
|
}
|
|
254
|
-
if (this.buffer.length < 20) break;
|
|
255
304
|
let headerInfo;
|
|
256
305
|
try {
|
|
257
306
|
headerInfo = decodeHeader(this.buffer);
|
|
258
307
|
} catch {
|
|
308
|
+
this.needed = 24;
|
|
259
309
|
break;
|
|
260
310
|
}
|
|
261
311
|
const { header, headerLen, messageKey } = headerInfo;
|
|
262
312
|
const frameLen = headerLen + header.bodyLen;
|
|
263
|
-
if (this.buffer.length < frameLen)
|
|
313
|
+
if (this.buffer.length < frameLen) {
|
|
314
|
+
this.needed = frameLen;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
264
317
|
const raw = this.buffer.subarray(0, frameLen);
|
|
265
318
|
const body = raw.subarray(headerLen);
|
|
266
319
|
let extLen = 0;
|
|
@@ -272,6 +325,7 @@ var BaichuanFrameParser = class {
|
|
|
272
325
|
const payload = body.subarray(extLen);
|
|
273
326
|
out.push({ header, body, extension, payload, messageKey, raw });
|
|
274
327
|
this.buffer = this.buffer.subarray(frameLen);
|
|
328
|
+
this.needed = 4;
|
|
275
329
|
}
|
|
276
330
|
return out;
|
|
277
331
|
}
|
|
@@ -677,8 +731,14 @@ async function getServerBinding(uid, options = {}) {
|
|
|
677
731
|
headers: { Accept: "application/json" }
|
|
678
732
|
});
|
|
679
733
|
if (!res.ok) {
|
|
734
|
+
let bodyPreview;
|
|
735
|
+
try {
|
|
736
|
+
const text = await res.text();
|
|
737
|
+
bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
680
740
|
logger?.log?.(
|
|
681
|
-
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
|
|
741
|
+
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
|
|
682
742
|
);
|
|
683
743
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
684
744
|
return void 0;
|
|
@@ -794,6 +854,68 @@ function parseServerBindingResponse(raw) {
|
|
|
794
854
|
}
|
|
795
855
|
|
|
796
856
|
// src/bcudp/BcUdpStream.ts
|
|
857
|
+
async function probeEgressForHost(destHost, destPort) {
|
|
858
|
+
return await new Promise((resolve, reject) => {
|
|
859
|
+
const probe = dgram.createSocket("udp4");
|
|
860
|
+
let settled = false;
|
|
861
|
+
const finish = (err, out) => {
|
|
862
|
+
if (settled) return;
|
|
863
|
+
settled = true;
|
|
864
|
+
try {
|
|
865
|
+
probe.close();
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
if (err || !out) reject(err ?? new Error("egress probe failed"));
|
|
869
|
+
else resolve(out);
|
|
870
|
+
};
|
|
871
|
+
probe.on("error", (e) => finish(e));
|
|
872
|
+
try {
|
|
873
|
+
probe.connect(destPort, destHost, () => {
|
|
874
|
+
try {
|
|
875
|
+
const a = probe.address();
|
|
876
|
+
if (typeof a === "string") return finish(new Error("probe address is string"));
|
|
877
|
+
finish(void 0, {
|
|
878
|
+
localAddress: a.address,
|
|
879
|
+
localPort: a.port
|
|
880
|
+
});
|
|
881
|
+
} catch (e) {
|
|
882
|
+
finish(e);
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
} catch (e) {
|
|
886
|
+
finish(e);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
|
|
891
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
|
|
892
|
+
const dest = destHost.split(".").map((s) => Number(s));
|
|
893
|
+
if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
|
|
894
|
+
return "unknown";
|
|
895
|
+
const ifaces = networkInterfaces();
|
|
896
|
+
let ownerSubnet;
|
|
897
|
+
for (const name of Object.keys(ifaces)) {
|
|
898
|
+
const entries = ifaces[name];
|
|
899
|
+
if (!entries) continue;
|
|
900
|
+
for (const e of entries) {
|
|
901
|
+
if (e.family !== "IPv4" || e.internal) continue;
|
|
902
|
+
if (e.address !== srcInfo.localAddress) continue;
|
|
903
|
+
const addr = e.address.split(".").map((s) => Number(s));
|
|
904
|
+
const mask = e.netmask.split(".").map((s) => Number(s));
|
|
905
|
+
if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
|
|
906
|
+
continue;
|
|
907
|
+
ownerSubnet = { addr, mask };
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
if (ownerSubnet) break;
|
|
911
|
+
}
|
|
912
|
+
if (!ownerSubnet) return "unknown";
|
|
913
|
+
for (let i = 0; i < 4; i++) {
|
|
914
|
+
if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
|
|
915
|
+
return "mismatch";
|
|
916
|
+
}
|
|
917
|
+
return "same";
|
|
918
|
+
}
|
|
797
919
|
var AckLatency = class {
|
|
798
920
|
currentValues = [];
|
|
799
921
|
lastReceiveTime = null;
|
|
@@ -872,6 +994,16 @@ function isUnroutableForP2P(ip) {
|
|
|
872
994
|
var P2P_LOOKUP_PORT = 9999;
|
|
873
995
|
var P2P_MAX_WAIT_MS = 15e3;
|
|
874
996
|
var P2P_RESEND_WAIT_MS = 500;
|
|
997
|
+
var inflightP2pLookups = /* @__PURE__ */ new Map();
|
|
998
|
+
var cachedP2pLookups = /* @__PURE__ */ new Map();
|
|
999
|
+
var negCachedP2pLookups = /* @__PURE__ */ new Map();
|
|
1000
|
+
var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
|
|
1001
|
+
var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
|
|
1002
|
+
function _clearP2pLookupDedupForTests() {
|
|
1003
|
+
inflightP2pLookups.clear();
|
|
1004
|
+
cachedP2pLookups.clear();
|
|
1005
|
+
negCachedP2pLookups.clear();
|
|
1006
|
+
}
|
|
875
1007
|
var BcUdpStream = class extends EventEmitter {
|
|
876
1008
|
opts;
|
|
877
1009
|
/**
|
|
@@ -960,31 +1092,14 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
960
1092
|
});
|
|
961
1093
|
sock.on("error", (e) => this.emit("error", e));
|
|
962
1094
|
sock.on("close", () => this.emit("close"));
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
await new Promise((resolve, reject) => {
|
|
972
|
-
sock.once("error", reject);
|
|
973
|
-
sock.bind(port, "0.0.0.0", () => {
|
|
974
|
-
sock.removeListener("error", reject);
|
|
975
|
-
resolve();
|
|
976
|
-
});
|
|
977
|
-
});
|
|
978
|
-
bound = true;
|
|
979
|
-
break;
|
|
980
|
-
} catch {
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
if (!bound) {
|
|
984
|
-
await new Promise(
|
|
985
|
-
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
986
|
-
);
|
|
987
|
-
}
|
|
1095
|
+
await new Promise((resolve, reject) => {
|
|
1096
|
+
const onErr = (e) => reject(e);
|
|
1097
|
+
sock.once("error", onErr);
|
|
1098
|
+
sock.bind(0, "0.0.0.0", () => {
|
|
1099
|
+
sock.removeListener("error", onErr);
|
|
1100
|
+
resolve();
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
988
1103
|
if (this.opts.mode === "direct") {
|
|
989
1104
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
990
1105
|
this.clientId = this.opts.clientId;
|
|
@@ -1055,6 +1170,49 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1055
1170
|
this.remote = { host: connected.rhost, port: connected.rport };
|
|
1056
1171
|
}
|
|
1057
1172
|
async p2pUidLookup(sock, uid) {
|
|
1173
|
+
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
1174
|
+
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
1175
|
+
const cached = cachedP2pLookups.get(uid);
|
|
1176
|
+
if (cached && cached.expires > Date.now()) {
|
|
1177
|
+
log(
|
|
1178
|
+
`UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
|
|
1179
|
+
);
|
|
1180
|
+
return cached.result;
|
|
1181
|
+
}
|
|
1182
|
+
const negCached = negCachedP2pLookups.get(uid);
|
|
1183
|
+
if (negCached && negCached.expires > Date.now()) {
|
|
1184
|
+
const remaining = negCached.expires - Date.now();
|
|
1185
|
+
log(
|
|
1186
|
+
`UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
|
|
1187
|
+
);
|
|
1188
|
+
throw negCached.error;
|
|
1189
|
+
}
|
|
1190
|
+
const inflight = inflightP2pLookups.get(uid);
|
|
1191
|
+
if (inflight) {
|
|
1192
|
+
log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
|
|
1193
|
+
return await inflight;
|
|
1194
|
+
}
|
|
1195
|
+
const work = this._doP2pUidLookupWork(sock, uid);
|
|
1196
|
+
inflightP2pLookups.set(uid, work);
|
|
1197
|
+
try {
|
|
1198
|
+
const result = await work;
|
|
1199
|
+
cachedP2pLookups.set(uid, {
|
|
1200
|
+
result,
|
|
1201
|
+
expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
|
|
1202
|
+
});
|
|
1203
|
+
return result;
|
|
1204
|
+
} catch (e) {
|
|
1205
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
1206
|
+
negCachedP2pLookups.set(uid, {
|
|
1207
|
+
error: err,
|
|
1208
|
+
expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
|
|
1209
|
+
});
|
|
1210
|
+
throw err;
|
|
1211
|
+
} finally {
|
|
1212
|
+
inflightP2pLookups.delete(uid);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async _doP2pUidLookupWork(sock, uid) {
|
|
1058
1216
|
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
1059
1217
|
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
1060
1218
|
const t0 = Date.now();
|
|
@@ -1522,6 +1680,23 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1522
1680
|
log(
|
|
1523
1681
|
`local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
|
|
1524
1682
|
);
|
|
1683
|
+
if (directHost && localMode === "local-direct") {
|
|
1684
|
+
try {
|
|
1685
|
+
const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
|
|
1686
|
+
const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
|
|
1687
|
+
if (sameSubnet === "mismatch") {
|
|
1688
|
+
log(
|
|
1689
|
+
`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.`
|
|
1690
|
+
);
|
|
1691
|
+
} else {
|
|
1692
|
+
log(
|
|
1693
|
+
`egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
this.emit("debug", "egress_probe_failed", e);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1525
1700
|
let bytesSent = 0;
|
|
1526
1701
|
let pktsRecv = 0;
|
|
1527
1702
|
sock.on("message", () => {
|
|
@@ -20517,7 +20692,7 @@ ${xml}`
|
|
|
20517
20692
|
* @returns Test results for all stream types and profiles
|
|
20518
20693
|
*/
|
|
20519
20694
|
async testChannelStreams(channel, logger) {
|
|
20520
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
20695
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA.js");
|
|
20521
20696
|
return await testChannelStreams({
|
|
20522
20697
|
api: this,
|
|
20523
20698
|
channel: this.normalizeChannel(channel),
|
|
@@ -20533,7 +20708,7 @@ ${xml}`
|
|
|
20533
20708
|
* @returns Complete diagnostics for all channels and streams
|
|
20534
20709
|
*/
|
|
20535
20710
|
async collectMultifocalDiagnostics(logger) {
|
|
20536
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
20711
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA.js");
|
|
20537
20712
|
return await collectMultifocalDiagnostics({
|
|
20538
20713
|
api: this,
|
|
20539
20714
|
logger
|
|
@@ -25250,7 +25425,10 @@ export {
|
|
|
25250
25425
|
encodeHeader,
|
|
25251
25426
|
decodeHeader,
|
|
25252
25427
|
BaichuanFrameParser,
|
|
25428
|
+
probeEgressForHost,
|
|
25429
|
+
isSameSubnetAsAnyLocalIface,
|
|
25253
25430
|
isUnroutableForP2P,
|
|
25431
|
+
_clearP2pLookupDedupForTests,
|
|
25254
25432
|
BcUdpStream,
|
|
25255
25433
|
asLogger,
|
|
25256
25434
|
createNullLogger,
|
|
@@ -25318,4 +25496,4 @@ export {
|
|
|
25318
25496
|
tcpReachabilityProbe,
|
|
25319
25497
|
autoDetectDeviceType
|
|
25320
25498
|
};
|
|
25321
|
-
//# sourceMappingURL=chunk-
|
|
25499
|
+
//# sourceMappingURL=chunk-Q4AXRV2G.js.map
|