@apocaliss92/nodelink-js 0.5.1 → 0.5.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/BaichuanVideoStream-OCLOM452.js +7 -0
- package/dist/{DiagnosticsTools-7BIWJDZS.js → DiagnosticsTools-K4MF2VXZ.js} +3 -3
- package/dist/{chunk-OJQLZETO.js → chunk-7HSTETZR.js} +409 -108
- package/dist/chunk-7HSTETZR.js.map +1 -0
- package/dist/{chunk-GVWJGQPT.js → chunk-MZUSWKF3.js} +5 -1
- package/dist/chunk-MZUSWKF3.js.map +1 -0
- package/dist/{chunk-VOPEOB4H.js → chunk-XDVBNZGR.js} +2 -2
- package/dist/cli/rtsp-server.cjs +403 -102
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +3 -3
- package/dist/index.cjs +410 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +42 -0
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/BaichuanVideoStream-NTIGPHYJ.js +0 -7
- package/dist/chunk-GVWJGQPT.js.map +0 -1
- package/dist/chunk-OJQLZETO.js.map +0 -1
- /package/dist/{BaichuanVideoStream-NTIGPHYJ.js.map → BaichuanVideoStream-OCLOM452.js.map} +0 -0
- /package/dist/{DiagnosticsTools-7BIWJDZS.js.map → DiagnosticsTools-K4MF2VXZ.js.map} +0 -0
- /package/dist/{chunk-VOPEOB4H.js.map → chunk-XDVBNZGR.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -11292,6 +11292,143 @@ function parseD2cHb(xml) {
|
|
|
11292
11292
|
return { cid: Number(cid), did: Number(did) };
|
|
11293
11293
|
}
|
|
11294
11294
|
|
|
11295
|
+
// src/cloud/server-binding.ts
|
|
11296
|
+
var REOLINK_API_V2_BASE = "https://apis.reolink.com/v2";
|
|
11297
|
+
var POSITIVE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11298
|
+
var NEGATIVE_TTL_MS = 30 * 1e3;
|
|
11299
|
+
var cache = /* @__PURE__ */ new Map();
|
|
11300
|
+
function readCache(uid, now) {
|
|
11301
|
+
const e = cache.get(uid);
|
|
11302
|
+
if (!e) return void 0;
|
|
11303
|
+
if (now >= e.expires) {
|
|
11304
|
+
cache.delete(uid);
|
|
11305
|
+
return void 0;
|
|
11306
|
+
}
|
|
11307
|
+
return e;
|
|
11308
|
+
}
|
|
11309
|
+
async function getServerBinding(uid, options = {}) {
|
|
11310
|
+
if (!uid || typeof uid !== "string") return void 0;
|
|
11311
|
+
const now = Date.now();
|
|
11312
|
+
const cached = readCache(uid, now);
|
|
11313
|
+
if (cached?.kind === "ok") return cached.response;
|
|
11314
|
+
if (cached?.kind === "err") return void 0;
|
|
11315
|
+
const language = options.language ?? "en";
|
|
11316
|
+
const baseUrl = options.baseUrl ?? REOLINK_API_V2_BASE;
|
|
11317
|
+
const timeoutMs = options.timeoutMs ?? 4e3;
|
|
11318
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
11319
|
+
const logger = options.logger;
|
|
11320
|
+
if (typeof fetchImpl !== "function") {
|
|
11321
|
+
logger?.debug?.(
|
|
11322
|
+
`[server-binding] global fetch unavailable; skipping cloud lookup`
|
|
11323
|
+
);
|
|
11324
|
+
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
11325
|
+
return void 0;
|
|
11326
|
+
}
|
|
11327
|
+
const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
|
|
11328
|
+
const controller = new AbortController();
|
|
11329
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11330
|
+
try {
|
|
11331
|
+
const res = await fetchImpl(url, {
|
|
11332
|
+
method: "GET",
|
|
11333
|
+
signal: controller.signal,
|
|
11334
|
+
headers: { Accept: "application/json" }
|
|
11335
|
+
});
|
|
11336
|
+
if (!res.ok) {
|
|
11337
|
+
logger?.debug?.(
|
|
11338
|
+
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
|
|
11339
|
+
);
|
|
11340
|
+
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
11341
|
+
return void 0;
|
|
11342
|
+
}
|
|
11343
|
+
const json = await res.json();
|
|
11344
|
+
const parsed = parseServerBindingResponse(json);
|
|
11345
|
+
if (!parsed) {
|
|
11346
|
+
logger?.debug?.(
|
|
11347
|
+
`[server-binding] ${uid}: response shape did not match expectations`
|
|
11348
|
+
);
|
|
11349
|
+
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
11350
|
+
return void 0;
|
|
11351
|
+
}
|
|
11352
|
+
cache.set(uid, {
|
|
11353
|
+
kind: "ok",
|
|
11354
|
+
response: parsed,
|
|
11355
|
+
expires: now + POSITIVE_TTL_MS
|
|
11356
|
+
});
|
|
11357
|
+
const pick = parsed.availableZones.find(
|
|
11358
|
+
(z) => z.status === "active" && z.services.p2p?.server
|
|
11359
|
+
);
|
|
11360
|
+
const hint = pick?.services.p2p?.server ?? parsed.availableZones[0]?.services.p2p?.server;
|
|
11361
|
+
logger?.log?.(
|
|
11362
|
+
`[server-binding] ${uid}: ${parsed.availableZones.length} zone(s)${hint ? `, p2p hint=${hint}` : ""}`
|
|
11363
|
+
);
|
|
11364
|
+
return parsed;
|
|
11365
|
+
} catch (e) {
|
|
11366
|
+
logger?.debug?.(
|
|
11367
|
+
`[server-binding] ${uid}: ${e?.message ?? String(e)}`
|
|
11368
|
+
);
|
|
11369
|
+
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
11370
|
+
return void 0;
|
|
11371
|
+
} finally {
|
|
11372
|
+
clearTimeout(timer);
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
function pickP2pHostFromBinding(response) {
|
|
11376
|
+
if (!response) return void 0;
|
|
11377
|
+
const zones = response.availableZones;
|
|
11378
|
+
if (!zones || zones.length === 0) return void 0;
|
|
11379
|
+
const active = zones.find(
|
|
11380
|
+
(z) => z.status === "active" && z.services.p2p?.server
|
|
11381
|
+
);
|
|
11382
|
+
if (active?.services.p2p?.server) return active.services.p2p.server;
|
|
11383
|
+
const def = zones.find(
|
|
11384
|
+
(z) => z.status === "default" && z.services.p2p?.server
|
|
11385
|
+
);
|
|
11386
|
+
if (def?.services.p2p?.server) return def.services.p2p.server;
|
|
11387
|
+
const any = zones.find((z) => z.services.p2p?.server);
|
|
11388
|
+
return any?.services.p2p?.server;
|
|
11389
|
+
}
|
|
11390
|
+
function isString(v) {
|
|
11391
|
+
return typeof v === "string";
|
|
11392
|
+
}
|
|
11393
|
+
function parseServerBindingResponse(raw) {
|
|
11394
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
11395
|
+
const rawZones = raw.availableZones;
|
|
11396
|
+
if (!Array.isArray(rawZones)) return void 0;
|
|
11397
|
+
const zones = [];
|
|
11398
|
+
for (const r of rawZones) {
|
|
11399
|
+
if (!r || typeof r !== "object") continue;
|
|
11400
|
+
const rec = r;
|
|
11401
|
+
const id = rec.id;
|
|
11402
|
+
const name = rec.name;
|
|
11403
|
+
const status = rec.status;
|
|
11404
|
+
if (!isString(id) || !isString(name) || !isString(status)) continue;
|
|
11405
|
+
const servicesRaw = rec.services;
|
|
11406
|
+
const services = {};
|
|
11407
|
+
if (servicesRaw && typeof servicesRaw === "object") {
|
|
11408
|
+
const s = servicesRaw;
|
|
11409
|
+
for (const key of ["p2p", "cloud", "roms_ota", "alarm_push"]) {
|
|
11410
|
+
const v = s[key];
|
|
11411
|
+
if (v && typeof v === "object") {
|
|
11412
|
+
const server = v.server;
|
|
11413
|
+
if (isString(server) && server.length > 0) {
|
|
11414
|
+
services[key] = { server };
|
|
11415
|
+
}
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
}
|
|
11419
|
+
const locationsRaw = rec.locations;
|
|
11420
|
+
const locations = Array.isArray(locationsRaw) && locationsRaw.every(isString) ? locationsRaw : void 0;
|
|
11421
|
+
zones.push({
|
|
11422
|
+
id,
|
|
11423
|
+
name,
|
|
11424
|
+
status,
|
|
11425
|
+
services,
|
|
11426
|
+
...locations ? { locations } : {}
|
|
11427
|
+
});
|
|
11428
|
+
}
|
|
11429
|
+
return { availableZones: zones };
|
|
11430
|
+
}
|
|
11431
|
+
|
|
11295
11432
|
// src/bcudp/BcUdpStream.ts
|
|
11296
11433
|
var AckLatency = class {
|
|
11297
11434
|
currentValues = [];
|
|
@@ -11373,6 +11510,16 @@ var P2P_MAX_WAIT_MS = 15e3;
|
|
|
11373
11510
|
var P2P_RESEND_WAIT_MS = 500;
|
|
11374
11511
|
var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
11375
11512
|
opts;
|
|
11513
|
+
/**
|
|
11514
|
+
* Optional info-level logger for diagnostic milestones — set via
|
|
11515
|
+
* {@link BcUdpStream.setLogger} by `BaichuanClient` so the lib's
|
|
11516
|
+
* standard logger sink sees BCUDP / P2P progress (DNS resolutions,
|
|
11517
|
+
* outgoing UDP probes, timeouts with elapsed times) without the user
|
|
11518
|
+
* having to opt into the per-packet `debug` event firehose. Kept
|
|
11519
|
+
* separate from `emit('debug', ...)` because that channel is intended
|
|
11520
|
+
* for the per-packet debug trace and is gated by debugOptions.
|
|
11521
|
+
*/
|
|
11522
|
+
discoveryLogger;
|
|
11376
11523
|
sock;
|
|
11377
11524
|
remote;
|
|
11378
11525
|
mtu;
|
|
@@ -11416,6 +11563,17 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11416
11563
|
this.mtu = BCUDP_DEFAULT_MTU;
|
|
11417
11564
|
}
|
|
11418
11565
|
/** True if the underlying UDP socket is open and the remote peer is known. */
|
|
11566
|
+
/**
|
|
11567
|
+
* Attach an info-level logger for high-signal diagnostic milestones
|
|
11568
|
+
* (DNS resolution, outgoing UDP probe sends, P2P UID lookup wins/losses,
|
|
11569
|
+
* BCUDP local discovery timeouts). The lib's `BaichuanClient` calls
|
|
11570
|
+
* this immediately after constructing the stream so consumers get
|
|
11571
|
+
* actionable progress logs without enabling the per-packet debug trace.
|
|
11572
|
+
* Safe to call repeatedly; only the most recent logger is used.
|
|
11573
|
+
*/
|
|
11574
|
+
setLogger(logger) {
|
|
11575
|
+
this.discoveryLogger = logger;
|
|
11576
|
+
}
|
|
11419
11577
|
isConnected() {
|
|
11420
11578
|
return !!this.sock && !!this.remote && this.cameraId != null;
|
|
11421
11579
|
}
|
|
@@ -11533,20 +11691,60 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11533
11691
|
this.remote = { host: connected.rhost, port: connected.rport };
|
|
11534
11692
|
}
|
|
11535
11693
|
async p2pUidLookup(sock, uid) {
|
|
11694
|
+
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
11695
|
+
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
11696
|
+
const t0 = Date.now();
|
|
11697
|
+
const hostnamesToTry = [];
|
|
11698
|
+
const binding = await getServerBinding(uid, {
|
|
11699
|
+
...this.discoveryLogger ? { logger: this.discoveryLogger } : {}
|
|
11700
|
+
}).catch(() => void 0);
|
|
11701
|
+
const hintedHost = pickP2pHostFromBinding(binding);
|
|
11702
|
+
if (hintedHost) {
|
|
11703
|
+
hostnamesToTry.push(hintedHost);
|
|
11704
|
+
log(
|
|
11705
|
+
`UID=${shortUid} cloud server-binding \u2192 hint=${hintedHost} (will try first)`
|
|
11706
|
+
);
|
|
11707
|
+
} else {
|
|
11708
|
+
log(
|
|
11709
|
+
`UID=${shortUid} cloud server-binding \u2192 no hint (apis.reolink.com unreachable / no zone match) \u2192 sweeping ${P2P_RELAY_HOSTNAMES.length} fallback hostnames`
|
|
11710
|
+
);
|
|
11711
|
+
}
|
|
11712
|
+
for (const host of P2P_RELAY_HOSTNAMES) {
|
|
11713
|
+
if (!hostnamesToTry.includes(host)) hostnamesToTry.push(host);
|
|
11714
|
+
}
|
|
11536
11715
|
const resolved = [];
|
|
11537
11716
|
const sinkholed = [];
|
|
11538
|
-
for (const host of
|
|
11717
|
+
for (const host of hostnamesToTry) {
|
|
11539
11718
|
try {
|
|
11540
11719
|
const answers = await import_promises.default.lookup(host, { family: 4, all: true });
|
|
11720
|
+
let publicCount = 0;
|
|
11721
|
+
let sinkCount = 0;
|
|
11541
11722
|
for (const a of answers) {
|
|
11542
11723
|
if (!a.address) continue;
|
|
11543
11724
|
if (isUnroutableForP2P(a.address)) {
|
|
11544
11725
|
sinkholed.push({ host, ip: a.address });
|
|
11726
|
+
sinkCount++;
|
|
11545
11727
|
continue;
|
|
11546
11728
|
}
|
|
11547
|
-
if (!resolved.includes(a.address))
|
|
11729
|
+
if (!resolved.includes(a.address)) {
|
|
11730
|
+
resolved.push(a.address);
|
|
11731
|
+
publicCount++;
|
|
11732
|
+
}
|
|
11548
11733
|
}
|
|
11549
|
-
|
|
11734
|
+
if (sinkCount > 0 && publicCount === 0) {
|
|
11735
|
+
log(
|
|
11736
|
+
`DNS ${host} \u2192 sinkhole (${sinkholed[sinkholed.length - 1]?.ip}) \u2014 DNS filter / /etc/hosts override`
|
|
11737
|
+
);
|
|
11738
|
+
} else if (publicCount > 0) {
|
|
11739
|
+
if (host === hintedHost) {
|
|
11740
|
+
log(`DNS ${host} \u2192 ${answers.find((a) => !isUnroutableForP2P(a.address))?.address} \u2713`);
|
|
11741
|
+
}
|
|
11742
|
+
}
|
|
11743
|
+
} catch (e) {
|
|
11744
|
+
log(`DNS ${host} \u2192 ENOTFOUND/timeout (${e?.code ?? "?"})`);
|
|
11745
|
+
}
|
|
11746
|
+
if (hintedHost && host === hintedHost && resolved.length > 0 && sinkholed.length === 0) {
|
|
11747
|
+
break;
|
|
11550
11748
|
}
|
|
11551
11749
|
}
|
|
11552
11750
|
if (resolved.length === 0) {
|
|
@@ -11560,11 +11758,22 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11560
11758
|
"P2P UID lookup failed: no p2p.reolink.com addresses resolved (DNS failure)"
|
|
11561
11759
|
);
|
|
11562
11760
|
}
|
|
11761
|
+
log(
|
|
11762
|
+
`Resolved ${resolved.length} P2P relay IP(s) (${resolved.slice(0, 3).join(", ")}${resolved.length > 3 ? "\u2026" : ""}). Sending C2M_Q probes (3s budget each, ${P2P_MAX_WAIT_MS}ms total)`
|
|
11763
|
+
);
|
|
11563
11764
|
const start = Date.now();
|
|
11564
11765
|
let lastErr;
|
|
11766
|
+
let attemptsMade = 0;
|
|
11565
11767
|
for (const ip of resolved) {
|
|
11566
11768
|
const remaining = P2P_MAX_WAIT_MS - (Date.now() - start);
|
|
11567
|
-
if (remaining <= 0)
|
|
11769
|
+
if (remaining <= 0) {
|
|
11770
|
+
log(
|
|
11771
|
+
`Aborting after ${attemptsMade} attempt(s) \u2014 total budget ${P2P_MAX_WAIT_MS}ms exhausted`
|
|
11772
|
+
);
|
|
11773
|
+
break;
|
|
11774
|
+
}
|
|
11775
|
+
attemptsMade++;
|
|
11776
|
+
const probeStart = Date.now();
|
|
11568
11777
|
try {
|
|
11569
11778
|
const res = await this.p2pUidLookupOne(
|
|
11570
11779
|
sock,
|
|
@@ -11572,11 +11781,20 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11572
11781
|
{ host: ip, port: P2P_LOOKUP_PORT },
|
|
11573
11782
|
Math.min(remaining, 3e3)
|
|
11574
11783
|
);
|
|
11784
|
+
log(
|
|
11785
|
+
`${ip}:${P2P_LOOKUP_PORT} replied in ${Date.now() - probeStart}ms \u2713 (total ${Date.now() - t0}ms)`
|
|
11786
|
+
);
|
|
11575
11787
|
return res;
|
|
11576
11788
|
} catch (e) {
|
|
11789
|
+
const ms = Date.now() - probeStart;
|
|
11790
|
+
const msg = e?.message ?? String(e);
|
|
11791
|
+
log(`${ip}:${P2P_LOOKUP_PORT} no reply after ${ms}ms (${msg})`);
|
|
11577
11792
|
lastErr = e instanceof Error ? e : new Error(String(e));
|
|
11578
11793
|
}
|
|
11579
11794
|
}
|
|
11795
|
+
log(
|
|
11796
|
+
`Exhausted all ${attemptsMade} relay candidate(s) after ${Date.now() - t0}ms \u2014 UID lookup failed`
|
|
11797
|
+
);
|
|
11580
11798
|
throw lastErr ?? new Error("P2P UID lookup failed");
|
|
11581
11799
|
}
|
|
11582
11800
|
async p2pUidLookupOne(sock, uid, dest, timeoutMs) {
|
|
@@ -11915,13 +12133,23 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11915
12133
|
const directHost = (this.opts.directHost ?? "").trim();
|
|
11916
12134
|
const localMode = opts?.localMode ?? "local-broadcast";
|
|
11917
12135
|
const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
|
|
11918
|
-
const discoveryTimeout =
|
|
12136
|
+
const discoveryTimeout = typeof this.opts.localDiscoveryTimeoutMs === "number" && this.opts.localDiscoveryTimeoutMs > 0 ? this.opts.localDiscoveryTimeoutMs : 15e3;
|
|
11919
12137
|
const retryInterval = 500;
|
|
11920
12138
|
const startMs = Date.now();
|
|
11921
12139
|
sock.setBroadcast(true);
|
|
11922
12140
|
const addr = sock.address();
|
|
11923
12141
|
const localPort = typeof addr === "string" ? 0 : addr.port;
|
|
11924
12142
|
const cid = Math.floor(Math.random() * 2147483647) | 0 || 82e3;
|
|
12143
|
+
const log = (msg) => this.discoveryLogger?.log?.(`[BCUDP] ${msg}`);
|
|
12144
|
+
const shortUid = this.opts.uid.length > 7 ? `${this.opts.uid.slice(0, 5)}\u2026${this.opts.uid.slice(-2)}` : this.opts.uid;
|
|
12145
|
+
log(
|
|
12146
|
+
`local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
|
|
12147
|
+
);
|
|
12148
|
+
let bytesSent = 0;
|
|
12149
|
+
let pktsRecv = 0;
|
|
12150
|
+
sock.on("message", () => {
|
|
12151
|
+
pktsRecv++;
|
|
12152
|
+
});
|
|
11925
12153
|
const xml = buildC2dC({
|
|
11926
12154
|
uid: this.opts.uid,
|
|
11927
12155
|
clientPort: localPort,
|
|
@@ -11932,6 +12160,9 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11932
12160
|
const timeout = setTimeout(() => {
|
|
11933
12161
|
if (retryTimer) clearInterval(retryTimer);
|
|
11934
12162
|
sock.off("message", onMsg);
|
|
12163
|
+
log(
|
|
12164
|
+
`local discovery timeout after ${discoveryTimeout}ms \u2014 sent=${bytesSent}B replies=${pktsRecv} (camera likely sleeping / off-LAN / firewall dropping replies)`
|
|
12165
|
+
);
|
|
11935
12166
|
reject(
|
|
11936
12167
|
new Error(
|
|
11937
12168
|
`BCUDP discovery timeout after ${discoveryTimeout}ms (camera may be sleeping or unreachable)`
|
|
@@ -12125,6 +12356,7 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
12125
12356
|
for (const port of ports) {
|
|
12126
12357
|
try {
|
|
12127
12358
|
sock.send(packet, port, host);
|
|
12359
|
+
bytesSent += packet.length;
|
|
12128
12360
|
retryCount++;
|
|
12129
12361
|
this.emit("debug", "discovery_send", { retryCount, host, port });
|
|
12130
12362
|
} catch {
|
|
@@ -13703,6 +13935,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
13703
13935
|
sock.on("debug", (event, data) => {
|
|
13704
13936
|
this.logDebug(`udp_${event}`, data);
|
|
13705
13937
|
});
|
|
13938
|
+
sock.setLogger(this.logger);
|
|
13706
13939
|
await sock.connect();
|
|
13707
13940
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : "";
|
|
13708
13941
|
const udpDiscoveryMethod = this.opts.udpDiscoveryMethod ?? "local-direct";
|
|
@@ -31501,7 +31734,11 @@ async function discoverUidForHost(host, logger) {
|
|
|
31501
31734
|
function isTcpFailureThatShouldFallbackToUdp(e) {
|
|
31502
31735
|
const message = e?.message || e?.toString?.() || "";
|
|
31503
31736
|
if (typeof message !== "string") return false;
|
|
31504
|
-
return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("
|
|
31737
|
+
return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTDOWN") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("ENETDOWN") || message.includes("socket hang up") || message.includes("TCP connection timeout") || // Autodetect's own hard deadline on the TCP login attempt — see
|
|
31738
|
+
// `withTcpDeadline` in `autoDetectDeviceType`. Without this entry the
|
|
31739
|
+
// catch block would rethrow the deadline error instead of awaiting
|
|
31740
|
+
// the speculative UDP race.
|
|
31741
|
+
message.includes("TCP login deadline exceeded") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
|
|
31505
31742
|
}
|
|
31506
31743
|
async function pingHost(host, timeoutMs = 3e3) {
|
|
31507
31744
|
if (!host || typeof host !== "string") return false;
|
|
@@ -31649,6 +31886,7 @@ function attachErrorHandler(api, transport, inputs) {
|
|
|
31649
31886
|
}
|
|
31650
31887
|
async function autoDetectDeviceType(inputs) {
|
|
31651
31888
|
const { host, uid, logger } = inputs;
|
|
31889
|
+
const autodetectStartedAt = Date.now();
|
|
31652
31890
|
const mode = inputs.mode ?? "auto";
|
|
31653
31891
|
const maxRetriesRaw = inputs.maxRetries;
|
|
31654
31892
|
const maxRetries = Math.max(
|
|
@@ -31665,9 +31903,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31665
31903
|
const sleepMs3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
31666
31904
|
const shouldRetryTcp = (e) => {
|
|
31667
31905
|
const msg = fmtErr(e);
|
|
31668
|
-
if (msg.includes("ECONNREFUSED"))
|
|
31906
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTDOWN") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH") || msg.includes("ENETDOWN")) {
|
|
31907
|
+
return false;
|
|
31908
|
+
}
|
|
31669
31909
|
return isTcpFailureThatShouldFallbackToUdp(e) || msg.includes("timeout waiting for nonce") || msg.includes("expected encryption info") || msg.includes("Baichuan socket closed") || msg.includes("ECONNRESET") || msg.includes("EPIPE");
|
|
31670
31910
|
};
|
|
31911
|
+
const tcpDeadlineMs = typeof inputs.tcpConnectTimeoutMs === "number" && Number.isFinite(inputs.tcpConnectTimeoutMs) && inputs.tcpConnectTimeoutMs > 0 ? inputs.tcpConnectTimeoutMs : 8e3;
|
|
31912
|
+
const withTcpDeadline = async (op) => {
|
|
31913
|
+
let timer;
|
|
31914
|
+
const deadline = new Promise((_, reject) => {
|
|
31915
|
+
timer = setTimeout(
|
|
31916
|
+
() => reject(
|
|
31917
|
+
new Error(
|
|
31918
|
+
`TCP login deadline exceeded (${tcpDeadlineMs}ms) \u2014 host unreachable`
|
|
31919
|
+
)
|
|
31920
|
+
),
|
|
31921
|
+
tcpDeadlineMs
|
|
31922
|
+
);
|
|
31923
|
+
timer.unref?.();
|
|
31924
|
+
});
|
|
31925
|
+
try {
|
|
31926
|
+
return await Promise.race([op, deadline]);
|
|
31927
|
+
} finally {
|
|
31928
|
+
if (timer) clearTimeout(timer);
|
|
31929
|
+
}
|
|
31930
|
+
};
|
|
31671
31931
|
const shouldRetryUdp = (e) => {
|
|
31672
31932
|
const msg = fmtErr(e);
|
|
31673
31933
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
@@ -31820,6 +32080,127 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31820
32080
|
"Forced UDP autodetect failed for all methods."
|
|
31821
32081
|
);
|
|
31822
32082
|
}
|
|
32083
|
+
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod, resolvedUid) => {
|
|
32084
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
32085
|
+
udpApi.getInfo(),
|
|
32086
|
+
udpApi.getDeviceCapabilities(),
|
|
32087
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
32088
|
+
]);
|
|
32089
|
+
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
32090
|
+
const model = deviceInfo.type?.trim();
|
|
32091
|
+
const normalizedModel = model ? model.trim() : void 0;
|
|
32092
|
+
const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
32093
|
+
const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
|
|
32094
|
+
const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
|
|
32095
|
+
const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
|
|
32096
|
+
const hasBattery = capabilities?.capabilities?.hasBattery === true;
|
|
32097
|
+
udpApi.setIdleDisconnect(hasBattery);
|
|
32098
|
+
if (isMultifocal) {
|
|
32099
|
+
const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
|
|
32100
|
+
logger?.log?.(
|
|
32101
|
+
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
|
|
32102
|
+
);
|
|
32103
|
+
return {
|
|
32104
|
+
type: "multifocal",
|
|
32105
|
+
transport: "udp",
|
|
32106
|
+
uid: resolvedUid,
|
|
32107
|
+
udpDiscoveryMethod,
|
|
32108
|
+
deviceInfo,
|
|
32109
|
+
...hostNetworkInfo ? { hostNetworkInfo } : {},
|
|
32110
|
+
channelNum,
|
|
32111
|
+
hasBattery,
|
|
32112
|
+
api: udpApi
|
|
32113
|
+
};
|
|
32114
|
+
}
|
|
32115
|
+
const deviceType = hasBattery ? "battery-cam" : "udp-camera";
|
|
32116
|
+
logger?.log?.(
|
|
32117
|
+
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
|
|
32118
|
+
);
|
|
32119
|
+
return {
|
|
32120
|
+
type: deviceType,
|
|
32121
|
+
transport: "udp",
|
|
32122
|
+
uid: resolvedUid,
|
|
32123
|
+
udpDiscoveryMethod,
|
|
32124
|
+
deviceInfo,
|
|
32125
|
+
...hostNetworkInfo ? { hostNetworkInfo } : {},
|
|
32126
|
+
channelNum: 1,
|
|
32127
|
+
hasBattery,
|
|
32128
|
+
api: udpApi
|
|
32129
|
+
};
|
|
32130
|
+
};
|
|
32131
|
+
const udpRaceAbort = new AbortController();
|
|
32132
|
+
const speculativeUdpRace = mode === "auto" ? (async () => {
|
|
32133
|
+
const resolvedUid = await speculativeUidPromise;
|
|
32134
|
+
const viableMethods = selectViableUdpMethods(Boolean(resolvedUid));
|
|
32135
|
+
return await runUdpMethodsParallel(
|
|
32136
|
+
viableMethods,
|
|
32137
|
+
async (m, isInnerAborted) => {
|
|
32138
|
+
const isAborted = () => udpRaceAbort.signal.aborted || isInnerAborted();
|
|
32139
|
+
if (isAborted()) {
|
|
32140
|
+
throw new Error(
|
|
32141
|
+
`UDP(${m}) speculative race aborted before start`
|
|
32142
|
+
);
|
|
32143
|
+
}
|
|
32144
|
+
logger?.log?.(
|
|
32145
|
+
`[AutoDetect] (race) Trying UDP discovery method: ${m}...`
|
|
32146
|
+
);
|
|
32147
|
+
const udpApi = await withRetries(
|
|
32148
|
+
`UDP(${m})`,
|
|
32149
|
+
maxRetries,
|
|
32150
|
+
async (attempt) => {
|
|
32151
|
+
const apiInputs = {
|
|
32152
|
+
...inputs,
|
|
32153
|
+
udpDiscoveryMethod: m
|
|
32154
|
+
};
|
|
32155
|
+
if (resolvedUid) apiInputs.uid = resolvedUid;
|
|
32156
|
+
const api = createBaichuanApi(apiInputs, "udp");
|
|
32157
|
+
try {
|
|
32158
|
+
await api.login();
|
|
32159
|
+
return api;
|
|
32160
|
+
} catch (e) {
|
|
32161
|
+
try {
|
|
32162
|
+
await api.close({
|
|
32163
|
+
reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
|
|
32164
|
+
});
|
|
32165
|
+
} catch {
|
|
32166
|
+
}
|
|
32167
|
+
throw e;
|
|
32168
|
+
}
|
|
32169
|
+
},
|
|
32170
|
+
shouldRetryUdp,
|
|
32171
|
+
isAborted
|
|
32172
|
+
);
|
|
32173
|
+
if (isAborted()) {
|
|
32174
|
+
try {
|
|
32175
|
+
await udpApi.close({
|
|
32176
|
+
reason: "autodetect:udp_aborted_after_tcp_won"
|
|
32177
|
+
});
|
|
32178
|
+
} catch {
|
|
32179
|
+
}
|
|
32180
|
+
throw new Error(
|
|
32181
|
+
`UDP(${m}) speculative race aborted after login`
|
|
32182
|
+
);
|
|
32183
|
+
}
|
|
32184
|
+
return detectOverUdpApi(udpApi, m, resolvedUid ?? "");
|
|
32185
|
+
},
|
|
32186
|
+
"Speculative UDP race failed for all methods."
|
|
32187
|
+
);
|
|
32188
|
+
})() : void 0;
|
|
32189
|
+
speculativeUdpRace?.then(
|
|
32190
|
+
(udpResult) => {
|
|
32191
|
+
if (udpRaceAbort.signal.aborted && udpResult?.api) {
|
|
32192
|
+
udpResult.api.close({ reason: "autodetect:tcp_won_race" }).catch(() => void 0);
|
|
32193
|
+
}
|
|
32194
|
+
},
|
|
32195
|
+
() => void 0
|
|
32196
|
+
);
|
|
32197
|
+
const _tcpWin = (result) => {
|
|
32198
|
+
udpRaceAbort.abort();
|
|
32199
|
+
logger?.log?.(
|
|
32200
|
+
`[AutoDetect] DONE in ${Date.now() - autodetectStartedAt}ms via TCP \u2014 type=${result.type} model=${result.deviceInfo?.type ?? "?"} channels=${result.channelNum}`
|
|
32201
|
+
);
|
|
32202
|
+
return result;
|
|
32203
|
+
};
|
|
31823
32204
|
let tcpApi;
|
|
31824
32205
|
try {
|
|
31825
32206
|
logger?.log?.(`[AutoDetect] Trying TCP connection to ${host}...`);
|
|
@@ -31829,7 +32210,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31829
32210
|
async (attempt) => {
|
|
31830
32211
|
const api2 = createBaichuanApi(inputs, "tcp");
|
|
31831
32212
|
try {
|
|
31832
|
-
await api2.login();
|
|
32213
|
+
await withTcpDeadline(api2.login());
|
|
31833
32214
|
return api2;
|
|
31834
32215
|
} catch (e) {
|
|
31835
32216
|
try {
|
|
@@ -31941,7 +32322,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31941
32322
|
logger?.log?.(
|
|
31942
32323
|
`[AutoDetect] Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum})`
|
|
31943
32324
|
);
|
|
31944
|
-
return {
|
|
32325
|
+
return _tcpWin({
|
|
31945
32326
|
type: "multifocal",
|
|
31946
32327
|
transport: "tcp",
|
|
31947
32328
|
uid: effectiveUid || uid || "",
|
|
@@ -31949,13 +32330,13 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31949
32330
|
// ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
|
|
31950
32331
|
channelNum: effectiveChannelNum,
|
|
31951
32332
|
api
|
|
31952
|
-
};
|
|
32333
|
+
});
|
|
31953
32334
|
}
|
|
31954
32335
|
if (effectiveChannelNum > 1) {
|
|
31955
32336
|
logger?.log?.(
|
|
31956
32337
|
`[AutoDetect] Detected NVR (${effectiveChannelNum} channels)`
|
|
31957
32338
|
);
|
|
31958
|
-
return {
|
|
32339
|
+
return _tcpWin({
|
|
31959
32340
|
type: "nvr",
|
|
31960
32341
|
transport: "tcp",
|
|
31961
32342
|
uid: effectiveUid || uid || "",
|
|
@@ -31963,10 +32344,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31963
32344
|
// ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
|
|
31964
32345
|
channelNum: effectiveChannelNum,
|
|
31965
32346
|
api
|
|
31966
|
-
};
|
|
32347
|
+
});
|
|
31967
32348
|
}
|
|
31968
32349
|
logger?.log?.(`[AutoDetect] Detected regular camera (single channel)`);
|
|
31969
|
-
return {
|
|
32350
|
+
return _tcpWin({
|
|
31970
32351
|
type: "camera",
|
|
31971
32352
|
transport: "tcp",
|
|
31972
32353
|
uid: effectiveUid || uid || "",
|
|
@@ -31974,7 +32355,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31974
32355
|
// ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
|
|
31975
32356
|
channelNum: 1,
|
|
31976
32357
|
api
|
|
31977
|
-
};
|
|
32358
|
+
});
|
|
31978
32359
|
} catch (tcpError) {
|
|
31979
32360
|
if (mode === "tcp") {
|
|
31980
32361
|
throw tcpError;
|
|
@@ -31989,100 +32370,20 @@ async function autoDetectDeviceType(inputs) {
|
|
|
31989
32370
|
throw tcpError;
|
|
31990
32371
|
}
|
|
31991
32372
|
logger?.log?.(`[AutoDetect] TCP failed, trying UDP...`);
|
|
31992
|
-
|
|
31993
|
-
|
|
31994
|
-
|
|
31995
|
-
`[AutoDetect] UID discovery failed; only local-direct can run without a UID. If the camera is sleeping or on a different subnet, supply its UID to enable BCUDP P2P fallback (remote/relay/map) which can wake it via Reolink's servers.`
|
|
31996
|
-
);
|
|
31997
|
-
} else if (effectiveUid === void 0) {
|
|
31998
|
-
logger?.log?.(
|
|
31999
|
-
`[AutoDetect] UID resolved via concurrent broadcast discovery: ${normalizedUid}`
|
|
32373
|
+
if (!speculativeUdpRace) {
|
|
32374
|
+
throw new Error(
|
|
32375
|
+
`AutoDetect internal: speculative UDP race missing in mode=${mode}`
|
|
32000
32376
|
);
|
|
32001
32377
|
}
|
|
32002
32378
|
try {
|
|
32003
|
-
const
|
|
32004
|
-
|
|
32005
|
-
|
|
32006
|
-
udpApi.getDeviceCapabilities(),
|
|
32007
|
-
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
32008
|
-
]);
|
|
32009
|
-
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
32010
|
-
const model = deviceInfo.type?.trim();
|
|
32011
|
-
const normalizedModel = model ? model.trim() : void 0;
|
|
32012
|
-
const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
32013
|
-
const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
|
|
32014
|
-
const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
|
|
32015
|
-
const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
|
|
32016
|
-
const hasBattery = capabilities?.capabilities?.hasBattery === true;
|
|
32017
|
-
udpApi.setIdleDisconnect(hasBattery);
|
|
32018
|
-
if (isMultifocal) {
|
|
32019
|
-
const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
|
|
32020
|
-
logger?.log?.(
|
|
32021
|
-
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
|
|
32022
|
-
);
|
|
32023
|
-
return {
|
|
32024
|
-
type: "multifocal",
|
|
32025
|
-
transport: "udp",
|
|
32026
|
-
uid: normalizedUid ?? "",
|
|
32027
|
-
udpDiscoveryMethod,
|
|
32028
|
-
deviceInfo,
|
|
32029
|
-
...hostNetworkInfo ? { hostNetworkInfo } : {},
|
|
32030
|
-
channelNum,
|
|
32031
|
-
hasBattery,
|
|
32032
|
-
api: udpApi
|
|
32033
|
-
};
|
|
32034
|
-
}
|
|
32035
|
-
const deviceType = hasBattery ? "battery-cam" : "udp-camera";
|
|
32036
|
-
logger?.log?.(
|
|
32037
|
-
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
|
|
32038
|
-
);
|
|
32039
|
-
return {
|
|
32040
|
-
type: deviceType,
|
|
32041
|
-
transport: "udp",
|
|
32042
|
-
uid: normalizedUid ?? "",
|
|
32043
|
-
udpDiscoveryMethod,
|
|
32044
|
-
deviceInfo,
|
|
32045
|
-
...hostNetworkInfo ? { hostNetworkInfo } : {},
|
|
32046
|
-
channelNum: 1,
|
|
32047
|
-
hasBattery,
|
|
32048
|
-
api: udpApi
|
|
32049
|
-
};
|
|
32050
|
-
};
|
|
32051
|
-
const viableMethods = selectViableUdpMethods(Boolean(normalizedUid));
|
|
32052
|
-
return await runUdpMethodsParallel(
|
|
32053
|
-
viableMethods,
|
|
32054
|
-
async (m, isAborted) => {
|
|
32055
|
-
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
32056
|
-
const udpApi = await withRetries(
|
|
32057
|
-
`UDP(${m})`,
|
|
32058
|
-
maxRetries,
|
|
32059
|
-
async (attempt) => {
|
|
32060
|
-
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
32061
|
-
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
32062
|
-
const api = createBaichuanApi(apiInputs, "udp");
|
|
32063
|
-
try {
|
|
32064
|
-
await api.login();
|
|
32065
|
-
return api;
|
|
32066
|
-
} catch (e) {
|
|
32067
|
-
try {
|
|
32068
|
-
await api.close({
|
|
32069
|
-
reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
|
|
32070
|
-
});
|
|
32071
|
-
} catch {
|
|
32072
|
-
}
|
|
32073
|
-
throw e;
|
|
32074
|
-
}
|
|
32075
|
-
},
|
|
32076
|
-
shouldRetryUdp,
|
|
32077
|
-
isAborted
|
|
32078
|
-
);
|
|
32079
|
-
return detectOverUdpApi(udpApi, m);
|
|
32080
|
-
},
|
|
32081
|
-
"UDP discovery failed for all methods."
|
|
32379
|
+
const udpResult = await speculativeUdpRace;
|
|
32380
|
+
logger?.log?.(
|
|
32381
|
+
`[AutoDetect] DONE in ${Date.now() - autodetectStartedAt}ms via UDP \u2014 type=${udpResult.type} method=${udpResult.udpDiscoveryMethod ?? "n/a"} model=${udpResult.deviceInfo?.type ?? "?"} channels=${udpResult.channelNum}`
|
|
32082
32382
|
);
|
|
32383
|
+
return udpResult;
|
|
32083
32384
|
} catch (udpError) {
|
|
32084
32385
|
logger?.log?.(
|
|
32085
|
-
`[AutoDetect]
|
|
32386
|
+
`[AutoDetect] FAILED after ${Date.now() - autodetectStartedAt}ms \u2014 neither TCP nor UDP could reach the camera. TCP: ${tcpError?.message ?? tcpError}. UDP: ${udpError?.message ?? udpError}`
|
|
32086
32387
|
);
|
|
32087
32388
|
throw new Error(
|
|
32088
32389
|
`Failed to connect via both TCP and UDP. TCP: ${tcpError?.message || tcpError}, UDP: ${udpError?.message || udpError}`
|