@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.
@@ -30,7 +30,7 @@ import {
30
30
  runAllDiagnosticsConsecutively,
31
31
  runMultifocalDiagnosticsConsecutively,
32
32
  xmlEscape
33
- } from "./chunk-VOPEOB4H.js";
33
+ } from "./chunk-XDVBNZGR.js";
34
34
  import {
35
35
  BC_CLASS_FILE_DOWNLOAD,
36
36
  BC_CLASS_LEGACY,
@@ -178,7 +178,7 @@ import {
178
178
  splitAnnexBToNalPayloads2,
179
179
  talkTraceLog,
180
180
  traceLog
181
- } from "./chunk-GVWJGQPT.js";
181
+ } from "./chunk-MZUSWKF3.js";
182
182
 
183
183
  // src/protocol/framing.ts
184
184
  function encodeHeader(h) {
@@ -619,6 +619,143 @@ function parseD2cHb(xml) {
619
619
  return { cid: Number(cid), did: Number(did) };
620
620
  }
621
621
 
622
+ // src/cloud/server-binding.ts
623
+ var REOLINK_API_V2_BASE = "https://apis.reolink.com/v2";
624
+ var POSITIVE_TTL_MS = 24 * 60 * 60 * 1e3;
625
+ var NEGATIVE_TTL_MS = 30 * 1e3;
626
+ var cache = /* @__PURE__ */ new Map();
627
+ function readCache(uid, now) {
628
+ const e = cache.get(uid);
629
+ if (!e) return void 0;
630
+ if (now >= e.expires) {
631
+ cache.delete(uid);
632
+ return void 0;
633
+ }
634
+ return e;
635
+ }
636
+ async function getServerBinding(uid, options = {}) {
637
+ if (!uid || typeof uid !== "string") return void 0;
638
+ const now = Date.now();
639
+ const cached = readCache(uid, now);
640
+ if (cached?.kind === "ok") return cached.response;
641
+ if (cached?.kind === "err") return void 0;
642
+ const language = options.language ?? "en";
643
+ const baseUrl = options.baseUrl ?? REOLINK_API_V2_BASE;
644
+ const timeoutMs = options.timeoutMs ?? 4e3;
645
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
646
+ const logger = options.logger;
647
+ if (typeof fetchImpl !== "function") {
648
+ logger?.debug?.(
649
+ `[server-binding] global fetch unavailable; skipping cloud lookup`
650
+ );
651
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
652
+ return void 0;
653
+ }
654
+ const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
655
+ const controller = new AbortController();
656
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
657
+ try {
658
+ const res = await fetchImpl(url, {
659
+ method: "GET",
660
+ signal: controller.signal,
661
+ headers: { Accept: "application/json" }
662
+ });
663
+ if (!res.ok) {
664
+ logger?.debug?.(
665
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
666
+ );
667
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
668
+ return void 0;
669
+ }
670
+ const json = await res.json();
671
+ const parsed = parseServerBindingResponse(json);
672
+ if (!parsed) {
673
+ logger?.debug?.(
674
+ `[server-binding] ${uid}: response shape did not match expectations`
675
+ );
676
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
677
+ return void 0;
678
+ }
679
+ cache.set(uid, {
680
+ kind: "ok",
681
+ response: parsed,
682
+ expires: now + POSITIVE_TTL_MS
683
+ });
684
+ const pick = parsed.availableZones.find(
685
+ (z) => z.status === "active" && z.services.p2p?.server
686
+ );
687
+ const hint = pick?.services.p2p?.server ?? parsed.availableZones[0]?.services.p2p?.server;
688
+ logger?.log?.(
689
+ `[server-binding] ${uid}: ${parsed.availableZones.length} zone(s)${hint ? `, p2p hint=${hint}` : ""}`
690
+ );
691
+ return parsed;
692
+ } catch (e) {
693
+ logger?.debug?.(
694
+ `[server-binding] ${uid}: ${e?.message ?? String(e)}`
695
+ );
696
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
697
+ return void 0;
698
+ } finally {
699
+ clearTimeout(timer);
700
+ }
701
+ }
702
+ function pickP2pHostFromBinding(response) {
703
+ if (!response) return void 0;
704
+ const zones = response.availableZones;
705
+ if (!zones || zones.length === 0) return void 0;
706
+ const active = zones.find(
707
+ (z) => z.status === "active" && z.services.p2p?.server
708
+ );
709
+ if (active?.services.p2p?.server) return active.services.p2p.server;
710
+ const def = zones.find(
711
+ (z) => z.status === "default" && z.services.p2p?.server
712
+ );
713
+ if (def?.services.p2p?.server) return def.services.p2p.server;
714
+ const any = zones.find((z) => z.services.p2p?.server);
715
+ return any?.services.p2p?.server;
716
+ }
717
+ function isString(v) {
718
+ return typeof v === "string";
719
+ }
720
+ function parseServerBindingResponse(raw) {
721
+ if (!raw || typeof raw !== "object") return void 0;
722
+ const rawZones = raw.availableZones;
723
+ if (!Array.isArray(rawZones)) return void 0;
724
+ const zones = [];
725
+ for (const r of rawZones) {
726
+ if (!r || typeof r !== "object") continue;
727
+ const rec = r;
728
+ const id = rec.id;
729
+ const name = rec.name;
730
+ const status = rec.status;
731
+ if (!isString(id) || !isString(name) || !isString(status)) continue;
732
+ const servicesRaw = rec.services;
733
+ const services = {};
734
+ if (servicesRaw && typeof servicesRaw === "object") {
735
+ const s = servicesRaw;
736
+ for (const key of ["p2p", "cloud", "roms_ota", "alarm_push"]) {
737
+ const v = s[key];
738
+ if (v && typeof v === "object") {
739
+ const server = v.server;
740
+ if (isString(server) && server.length > 0) {
741
+ services[key] = { server };
742
+ }
743
+ }
744
+ }
745
+ }
746
+ const locationsRaw = rec.locations;
747
+ const locations = Array.isArray(locationsRaw) && locationsRaw.every(isString) ? locationsRaw : void 0;
748
+ zones.push({
749
+ id,
750
+ name,
751
+ status,
752
+ services,
753
+ ...locations ? { locations } : {}
754
+ });
755
+ }
756
+ return { availableZones: zones };
757
+ }
758
+
622
759
  // src/bcudp/BcUdpStream.ts
623
760
  var AckLatency = class {
624
761
  currentValues = [];
@@ -700,6 +837,16 @@ var P2P_MAX_WAIT_MS = 15e3;
700
837
  var P2P_RESEND_WAIT_MS = 500;
701
838
  var BcUdpStream = class extends EventEmitter {
702
839
  opts;
840
+ /**
841
+ * Optional info-level logger for diagnostic milestones — set via
842
+ * {@link BcUdpStream.setLogger} by `BaichuanClient` so the lib's
843
+ * standard logger sink sees BCUDP / P2P progress (DNS resolutions,
844
+ * outgoing UDP probes, timeouts with elapsed times) without the user
845
+ * having to opt into the per-packet `debug` event firehose. Kept
846
+ * separate from `emit('debug', ...)` because that channel is intended
847
+ * for the per-packet debug trace and is gated by debugOptions.
848
+ */
849
+ discoveryLogger;
703
850
  sock;
704
851
  remote;
705
852
  mtu;
@@ -743,6 +890,17 @@ var BcUdpStream = class extends EventEmitter {
743
890
  this.mtu = BCUDP_DEFAULT_MTU;
744
891
  }
745
892
  /** True if the underlying UDP socket is open and the remote peer is known. */
893
+ /**
894
+ * Attach an info-level logger for high-signal diagnostic milestones
895
+ * (DNS resolution, outgoing UDP probe sends, P2P UID lookup wins/losses,
896
+ * BCUDP local discovery timeouts). The lib's `BaichuanClient` calls
897
+ * this immediately after constructing the stream so consumers get
898
+ * actionable progress logs without enabling the per-packet debug trace.
899
+ * Safe to call repeatedly; only the most recent logger is used.
900
+ */
901
+ setLogger(logger) {
902
+ this.discoveryLogger = logger;
903
+ }
746
904
  isConnected() {
747
905
  return !!this.sock && !!this.remote && this.cameraId != null;
748
906
  }
@@ -860,20 +1018,60 @@ var BcUdpStream = class extends EventEmitter {
860
1018
  this.remote = { host: connected.rhost, port: connected.rport };
861
1019
  }
862
1020
  async p2pUidLookup(sock, uid) {
1021
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
1022
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
1023
+ const t0 = Date.now();
1024
+ const hostnamesToTry = [];
1025
+ const binding = await getServerBinding(uid, {
1026
+ ...this.discoveryLogger ? { logger: this.discoveryLogger } : {}
1027
+ }).catch(() => void 0);
1028
+ const hintedHost = pickP2pHostFromBinding(binding);
1029
+ if (hintedHost) {
1030
+ hostnamesToTry.push(hintedHost);
1031
+ log(
1032
+ `UID=${shortUid} cloud server-binding \u2192 hint=${hintedHost} (will try first)`
1033
+ );
1034
+ } else {
1035
+ log(
1036
+ `UID=${shortUid} cloud server-binding \u2192 no hint (apis.reolink.com unreachable / no zone match) \u2192 sweeping ${P2P_RELAY_HOSTNAMES.length} fallback hostnames`
1037
+ );
1038
+ }
1039
+ for (const host of P2P_RELAY_HOSTNAMES) {
1040
+ if (!hostnamesToTry.includes(host)) hostnamesToTry.push(host);
1041
+ }
863
1042
  const resolved = [];
864
1043
  const sinkholed = [];
865
- for (const host of P2P_RELAY_HOSTNAMES) {
1044
+ for (const host of hostnamesToTry) {
866
1045
  try {
867
1046
  const answers = await dns.lookup(host, { family: 4, all: true });
1047
+ let publicCount = 0;
1048
+ let sinkCount = 0;
868
1049
  for (const a of answers) {
869
1050
  if (!a.address) continue;
870
1051
  if (isUnroutableForP2P(a.address)) {
871
1052
  sinkholed.push({ host, ip: a.address });
1053
+ sinkCount++;
872
1054
  continue;
873
1055
  }
874
- if (!resolved.includes(a.address)) resolved.push(a.address);
1056
+ if (!resolved.includes(a.address)) {
1057
+ resolved.push(a.address);
1058
+ publicCount++;
1059
+ }
875
1060
  }
876
- } catch {
1061
+ if (sinkCount > 0 && publicCount === 0) {
1062
+ log(
1063
+ `DNS ${host} \u2192 sinkhole (${sinkholed[sinkholed.length - 1]?.ip}) \u2014 DNS filter / /etc/hosts override`
1064
+ );
1065
+ } else if (publicCount > 0) {
1066
+ if (host === hintedHost) {
1067
+ log(`DNS ${host} \u2192 ${answers.find((a) => !isUnroutableForP2P(a.address))?.address} \u2713`);
1068
+ }
1069
+ }
1070
+ } catch (e) {
1071
+ log(`DNS ${host} \u2192 ENOTFOUND/timeout (${e?.code ?? "?"})`);
1072
+ }
1073
+ if (hintedHost && host === hintedHost && resolved.length > 0 && sinkholed.length === 0) {
1074
+ break;
877
1075
  }
878
1076
  }
879
1077
  if (resolved.length === 0) {
@@ -887,11 +1085,22 @@ var BcUdpStream = class extends EventEmitter {
887
1085
  "P2P UID lookup failed: no p2p.reolink.com addresses resolved (DNS failure)"
888
1086
  );
889
1087
  }
1088
+ log(
1089
+ `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)`
1090
+ );
890
1091
  const start = Date.now();
891
1092
  let lastErr;
1093
+ let attemptsMade = 0;
892
1094
  for (const ip of resolved) {
893
1095
  const remaining = P2P_MAX_WAIT_MS - (Date.now() - start);
894
- if (remaining <= 0) break;
1096
+ if (remaining <= 0) {
1097
+ log(
1098
+ `Aborting after ${attemptsMade} attempt(s) \u2014 total budget ${P2P_MAX_WAIT_MS}ms exhausted`
1099
+ );
1100
+ break;
1101
+ }
1102
+ attemptsMade++;
1103
+ const probeStart = Date.now();
895
1104
  try {
896
1105
  const res = await this.p2pUidLookupOne(
897
1106
  sock,
@@ -899,11 +1108,20 @@ var BcUdpStream = class extends EventEmitter {
899
1108
  { host: ip, port: P2P_LOOKUP_PORT },
900
1109
  Math.min(remaining, 3e3)
901
1110
  );
1111
+ log(
1112
+ `${ip}:${P2P_LOOKUP_PORT} replied in ${Date.now() - probeStart}ms \u2713 (total ${Date.now() - t0}ms)`
1113
+ );
902
1114
  return res;
903
1115
  } catch (e) {
1116
+ const ms = Date.now() - probeStart;
1117
+ const msg = e?.message ?? String(e);
1118
+ log(`${ip}:${P2P_LOOKUP_PORT} no reply after ${ms}ms (${msg})`);
904
1119
  lastErr = e instanceof Error ? e : new Error(String(e));
905
1120
  }
906
1121
  }
1122
+ log(
1123
+ `Exhausted all ${attemptsMade} relay candidate(s) after ${Date.now() - t0}ms \u2014 UID lookup failed`
1124
+ );
907
1125
  throw lastErr ?? new Error("P2P UID lookup failed");
908
1126
  }
909
1127
  async p2pUidLookupOne(sock, uid, dest, timeoutMs) {
@@ -1242,13 +1460,23 @@ var BcUdpStream = class extends EventEmitter {
1242
1460
  const directHost = (this.opts.directHost ?? "").trim();
1243
1461
  const localMode = opts?.localMode ?? "local-broadcast";
1244
1462
  const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
1245
- const discoveryTimeout = 3e4;
1463
+ const discoveryTimeout = typeof this.opts.localDiscoveryTimeoutMs === "number" && this.opts.localDiscoveryTimeoutMs > 0 ? this.opts.localDiscoveryTimeoutMs : 15e3;
1246
1464
  const retryInterval = 500;
1247
1465
  const startMs = Date.now();
1248
1466
  sock.setBroadcast(true);
1249
1467
  const addr = sock.address();
1250
1468
  const localPort = typeof addr === "string" ? 0 : addr.port;
1251
1469
  const cid = Math.floor(Math.random() * 2147483647) | 0 || 82e3;
1470
+ const log = (msg) => this.discoveryLogger?.log?.(`[BCUDP] ${msg}`);
1471
+ const shortUid = this.opts.uid.length > 7 ? `${this.opts.uid.slice(0, 5)}\u2026${this.opts.uid.slice(-2)}` : this.opts.uid;
1472
+ log(
1473
+ `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
1474
+ );
1475
+ let bytesSent = 0;
1476
+ let pktsRecv = 0;
1477
+ sock.on("message", () => {
1478
+ pktsRecv++;
1479
+ });
1252
1480
  const xml = buildC2dC({
1253
1481
  uid: this.opts.uid,
1254
1482
  clientPort: localPort,
@@ -1259,6 +1487,9 @@ var BcUdpStream = class extends EventEmitter {
1259
1487
  const timeout = setTimeout(() => {
1260
1488
  if (retryTimer) clearInterval(retryTimer);
1261
1489
  sock.off("message", onMsg);
1490
+ log(
1491
+ `local discovery timeout after ${discoveryTimeout}ms \u2014 sent=${bytesSent}B replies=${pktsRecv} (camera likely sleeping / off-LAN / firewall dropping replies)`
1492
+ );
1262
1493
  reject(
1263
1494
  new Error(
1264
1495
  `BCUDP discovery timeout after ${discoveryTimeout}ms (camera may be sleeping or unreachable)`
@@ -1452,6 +1683,7 @@ var BcUdpStream = class extends EventEmitter {
1452
1683
  for (const port of ports) {
1453
1684
  try {
1454
1685
  sock.send(packet, port, host);
1686
+ bytesSent += packet.length;
1455
1687
  retryCount++;
1456
1688
  this.emit("debug", "discovery_send", { retryCount, host, port });
1457
1689
  } catch {
@@ -2988,6 +3220,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2988
3220
  sock.on("debug", (event, data) => {
2989
3221
  this.logDebug(`udp_${event}`, data);
2990
3222
  });
3223
+ sock.setLogger(this.logger);
2991
3224
  await sock.connect();
2992
3225
  const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : "";
2993
3226
  const udpDiscoveryMethod = this.opts.udpDiscoveryMethod ?? "local-direct";
@@ -13504,7 +13737,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
13504
13737
  return;
13505
13738
  }
13506
13739
  entry.startInFlight = (async () => {
13507
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-NTIGPHYJ.js");
13740
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-OCLOM452.js");
13508
13741
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
13509
13742
  const dedicated = await this.createDedicatedSession(sessionKey);
13510
13743
  const stream = new BaichuanVideoStream2({
@@ -20234,7 +20467,7 @@ ${xml}`
20234
20467
  * @returns Test results for all stream types and profiles
20235
20468
  */
20236
20469
  async testChannelStreams(channel, logger) {
20237
- const { testChannelStreams } = await import("./DiagnosticsTools-7BIWJDZS.js");
20470
+ const { testChannelStreams } = await import("./DiagnosticsTools-K4MF2VXZ.js");
20238
20471
  return await testChannelStreams({
20239
20472
  api: this,
20240
20473
  channel: this.normalizeChannel(channel),
@@ -20250,7 +20483,7 @@ ${xml}`
20250
20483
  * @returns Complete diagnostics for all channels and streams
20251
20484
  */
20252
20485
  async collectMultifocalDiagnostics(logger) {
20253
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-7BIWJDZS.js");
20486
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-K4MF2VXZ.js");
20254
20487
  return await collectMultifocalDiagnostics({
20255
20488
  api: this,
20256
20489
  logger
@@ -24305,7 +24538,11 @@ async function discoverUidForHost(host, logger) {
24305
24538
  function isTcpFailureThatShouldFallbackToUdp(e) {
24306
24539
  const message = e?.message || e?.toString?.() || "";
24307
24540
  if (typeof message !== "string") return false;
24308
- return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("socket hang up") || message.includes("TCP connection timeout") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
24541
+ 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
24542
+ // `withTcpDeadline` in `autoDetectDeviceType`. Without this entry the
24543
+ // catch block would rethrow the deadline error instead of awaiting
24544
+ // the speculative UDP race.
24545
+ 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");
24309
24546
  }
24310
24547
  async function pingHost(host, timeoutMs = 3e3) {
24311
24548
  if (!host || typeof host !== "string") return false;
@@ -24453,6 +24690,7 @@ function attachErrorHandler(api, transport, inputs) {
24453
24690
  }
24454
24691
  async function autoDetectDeviceType(inputs) {
24455
24692
  const { host, uid, logger } = inputs;
24693
+ const autodetectStartedAt = Date.now();
24456
24694
  const mode = inputs.mode ?? "auto";
24457
24695
  const maxRetriesRaw = inputs.maxRetries;
24458
24696
  const maxRetries = Math.max(
@@ -24469,9 +24707,31 @@ async function autoDetectDeviceType(inputs) {
24469
24707
  const sleepMs2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24470
24708
  const shouldRetryTcp = (e) => {
24471
24709
  const msg = fmtErr(e);
24472
- if (msg.includes("ECONNREFUSED")) return false;
24710
+ if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTDOWN") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH") || msg.includes("ENETDOWN")) {
24711
+ return false;
24712
+ }
24473
24713
  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");
24474
24714
  };
24715
+ const tcpDeadlineMs = typeof inputs.tcpConnectTimeoutMs === "number" && Number.isFinite(inputs.tcpConnectTimeoutMs) && inputs.tcpConnectTimeoutMs > 0 ? inputs.tcpConnectTimeoutMs : 8e3;
24716
+ const withTcpDeadline = async (op) => {
24717
+ let timer;
24718
+ const deadline = new Promise((_, reject) => {
24719
+ timer = setTimeout(
24720
+ () => reject(
24721
+ new Error(
24722
+ `TCP login deadline exceeded (${tcpDeadlineMs}ms) \u2014 host unreachable`
24723
+ )
24724
+ ),
24725
+ tcpDeadlineMs
24726
+ );
24727
+ timer.unref?.();
24728
+ });
24729
+ try {
24730
+ return await Promise.race([op, deadline]);
24731
+ } finally {
24732
+ if (timer) clearTimeout(timer);
24733
+ }
24734
+ };
24475
24735
  const shouldRetryUdp = (e) => {
24476
24736
  const msg = fmtErr(e);
24477
24737
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
@@ -24624,6 +24884,127 @@ async function autoDetectDeviceType(inputs) {
24624
24884
  "Forced UDP autodetect failed for all methods."
24625
24885
  );
24626
24886
  }
24887
+ const detectOverUdpApi = async (udpApi, udpDiscoveryMethod, resolvedUid) => {
24888
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
24889
+ udpApi.getInfo(),
24890
+ udpApi.getDeviceCapabilities(),
24891
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
24892
+ ]);
24893
+ const channelNum = capabilities?.support?.channelNum ?? 1;
24894
+ const model = deviceInfo.type?.trim();
24895
+ const normalizedModel = model ? model.trim() : void 0;
24896
+ const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
24897
+ const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
24898
+ const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
24899
+ const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
24900
+ const hasBattery = capabilities?.capabilities?.hasBattery === true;
24901
+ udpApi.setIdleDisconnect(hasBattery);
24902
+ if (isMultifocal) {
24903
+ const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
24904
+ logger?.log?.(
24905
+ `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
24906
+ );
24907
+ return {
24908
+ type: "multifocal",
24909
+ transport: "udp",
24910
+ uid: resolvedUid,
24911
+ udpDiscoveryMethod,
24912
+ deviceInfo,
24913
+ ...hostNetworkInfo ? { hostNetworkInfo } : {},
24914
+ channelNum,
24915
+ hasBattery,
24916
+ api: udpApi
24917
+ };
24918
+ }
24919
+ const deviceType = hasBattery ? "battery-cam" : "udp-camera";
24920
+ logger?.log?.(
24921
+ `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
24922
+ );
24923
+ return {
24924
+ type: deviceType,
24925
+ transport: "udp",
24926
+ uid: resolvedUid,
24927
+ udpDiscoveryMethod,
24928
+ deviceInfo,
24929
+ ...hostNetworkInfo ? { hostNetworkInfo } : {},
24930
+ channelNum: 1,
24931
+ hasBattery,
24932
+ api: udpApi
24933
+ };
24934
+ };
24935
+ const udpRaceAbort = new AbortController();
24936
+ const speculativeUdpRace = mode === "auto" ? (async () => {
24937
+ const resolvedUid = await speculativeUidPromise;
24938
+ const viableMethods = selectViableUdpMethods(Boolean(resolvedUid));
24939
+ return await runUdpMethodsParallel(
24940
+ viableMethods,
24941
+ async (m, isInnerAborted) => {
24942
+ const isAborted = () => udpRaceAbort.signal.aborted || isInnerAborted();
24943
+ if (isAborted()) {
24944
+ throw new Error(
24945
+ `UDP(${m}) speculative race aborted before start`
24946
+ );
24947
+ }
24948
+ logger?.log?.(
24949
+ `[AutoDetect] (race) Trying UDP discovery method: ${m}...`
24950
+ );
24951
+ const udpApi = await withRetries(
24952
+ `UDP(${m})`,
24953
+ maxRetries,
24954
+ async (attempt) => {
24955
+ const apiInputs = {
24956
+ ...inputs,
24957
+ udpDiscoveryMethod: m
24958
+ };
24959
+ if (resolvedUid) apiInputs.uid = resolvedUid;
24960
+ const api = createBaichuanApi(apiInputs, "udp");
24961
+ try {
24962
+ await api.login();
24963
+ return api;
24964
+ } catch (e) {
24965
+ try {
24966
+ await api.close({
24967
+ reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
24968
+ });
24969
+ } catch {
24970
+ }
24971
+ throw e;
24972
+ }
24973
+ },
24974
+ shouldRetryUdp,
24975
+ isAborted
24976
+ );
24977
+ if (isAborted()) {
24978
+ try {
24979
+ await udpApi.close({
24980
+ reason: "autodetect:udp_aborted_after_tcp_won"
24981
+ });
24982
+ } catch {
24983
+ }
24984
+ throw new Error(
24985
+ `UDP(${m}) speculative race aborted after login`
24986
+ );
24987
+ }
24988
+ return detectOverUdpApi(udpApi, m, resolvedUid ?? "");
24989
+ },
24990
+ "Speculative UDP race failed for all methods."
24991
+ );
24992
+ })() : void 0;
24993
+ speculativeUdpRace?.then(
24994
+ (udpResult) => {
24995
+ if (udpRaceAbort.signal.aborted && udpResult?.api) {
24996
+ udpResult.api.close({ reason: "autodetect:tcp_won_race" }).catch(() => void 0);
24997
+ }
24998
+ },
24999
+ () => void 0
25000
+ );
25001
+ const _tcpWin = (result) => {
25002
+ udpRaceAbort.abort();
25003
+ logger?.log?.(
25004
+ `[AutoDetect] DONE in ${Date.now() - autodetectStartedAt}ms via TCP \u2014 type=${result.type} model=${result.deviceInfo?.type ?? "?"} channels=${result.channelNum}`
25005
+ );
25006
+ return result;
25007
+ };
24627
25008
  let tcpApi;
24628
25009
  try {
24629
25010
  logger?.log?.(`[AutoDetect] Trying TCP connection to ${host}...`);
@@ -24633,7 +25014,7 @@ async function autoDetectDeviceType(inputs) {
24633
25014
  async (attempt) => {
24634
25015
  const api2 = createBaichuanApi(inputs, "tcp");
24635
25016
  try {
24636
- await api2.login();
25017
+ await withTcpDeadline(api2.login());
24637
25018
  return api2;
24638
25019
  } catch (e) {
24639
25020
  try {
@@ -24745,7 +25126,7 @@ async function autoDetectDeviceType(inputs) {
24745
25126
  logger?.log?.(
24746
25127
  `[AutoDetect] Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum})`
24747
25128
  );
24748
- return {
25129
+ return _tcpWin({
24749
25130
  type: "multifocal",
24750
25131
  transport: "tcp",
24751
25132
  uid: effectiveUid || uid || "",
@@ -24753,13 +25134,13 @@ async function autoDetectDeviceType(inputs) {
24753
25134
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
24754
25135
  channelNum: effectiveChannelNum,
24755
25136
  api
24756
- };
25137
+ });
24757
25138
  }
24758
25139
  if (effectiveChannelNum > 1) {
24759
25140
  logger?.log?.(
24760
25141
  `[AutoDetect] Detected NVR (${effectiveChannelNum} channels)`
24761
25142
  );
24762
- return {
25143
+ return _tcpWin({
24763
25144
  type: "nvr",
24764
25145
  transport: "tcp",
24765
25146
  uid: effectiveUid || uid || "",
@@ -24767,10 +25148,10 @@ async function autoDetectDeviceType(inputs) {
24767
25148
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
24768
25149
  channelNum: effectiveChannelNum,
24769
25150
  api
24770
- };
25151
+ });
24771
25152
  }
24772
25153
  logger?.log?.(`[AutoDetect] Detected regular camera (single channel)`);
24773
- return {
25154
+ return _tcpWin({
24774
25155
  type: "camera",
24775
25156
  transport: "tcp",
24776
25157
  uid: effectiveUid || uid || "",
@@ -24778,7 +25159,7 @@ async function autoDetectDeviceType(inputs) {
24778
25159
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
24779
25160
  channelNum: 1,
24780
25161
  api
24781
- };
25162
+ });
24782
25163
  } catch (tcpError) {
24783
25164
  if (mode === "tcp") {
24784
25165
  throw tcpError;
@@ -24793,100 +25174,20 @@ async function autoDetectDeviceType(inputs) {
24793
25174
  throw tcpError;
24794
25175
  }
24795
25176
  logger?.log?.(`[AutoDetect] TCP failed, trying UDP...`);
24796
- let normalizedUid = await speculativeUidPromise;
24797
- if (!normalizedUid) {
24798
- logger?.log?.(
24799
- `[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.`
24800
- );
24801
- } else if (effectiveUid === void 0) {
24802
- logger?.log?.(
24803
- `[AutoDetect] UID resolved via concurrent broadcast discovery: ${normalizedUid}`
25177
+ if (!speculativeUdpRace) {
25178
+ throw new Error(
25179
+ `AutoDetect internal: speculative UDP race missing in mode=${mode}`
24804
25180
  );
24805
25181
  }
24806
25182
  try {
24807
- const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
24808
- const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
24809
- udpApi.getInfo(),
24810
- udpApi.getDeviceCapabilities(),
24811
- udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
24812
- ]);
24813
- const channelNum = capabilities?.support?.channelNum ?? 1;
24814
- const model = deviceInfo.type?.trim();
24815
- const normalizedModel = model ? model.trim() : void 0;
24816
- const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
24817
- const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
24818
- const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
24819
- const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
24820
- const hasBattery = capabilities?.capabilities?.hasBattery === true;
24821
- udpApi.setIdleDisconnect(hasBattery);
24822
- if (isMultifocal) {
24823
- const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
24824
- logger?.log?.(
24825
- `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
24826
- );
24827
- return {
24828
- type: "multifocal",
24829
- transport: "udp",
24830
- uid: normalizedUid ?? "",
24831
- udpDiscoveryMethod,
24832
- deviceInfo,
24833
- ...hostNetworkInfo ? { hostNetworkInfo } : {},
24834
- channelNum,
24835
- hasBattery,
24836
- api: udpApi
24837
- };
24838
- }
24839
- const deviceType = hasBattery ? "battery-cam" : "udp-camera";
24840
- logger?.log?.(
24841
- `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
24842
- );
24843
- return {
24844
- type: deviceType,
24845
- transport: "udp",
24846
- uid: normalizedUid ?? "",
24847
- udpDiscoveryMethod,
24848
- deviceInfo,
24849
- ...hostNetworkInfo ? { hostNetworkInfo } : {},
24850
- channelNum: 1,
24851
- hasBattery,
24852
- api: udpApi
24853
- };
24854
- };
24855
- const viableMethods = selectViableUdpMethods(Boolean(normalizedUid));
24856
- return await runUdpMethodsParallel(
24857
- viableMethods,
24858
- async (m, isAborted) => {
24859
- logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
24860
- const udpApi = await withRetries(
24861
- `UDP(${m})`,
24862
- maxRetries,
24863
- async (attempt) => {
24864
- const apiInputs = { ...inputs, udpDiscoveryMethod: m };
24865
- if (normalizedUid) apiInputs.uid = normalizedUid;
24866
- const api = createBaichuanApi(apiInputs, "udp");
24867
- try {
24868
- await api.login();
24869
- return api;
24870
- } catch (e) {
24871
- try {
24872
- await api.close({
24873
- reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
24874
- });
24875
- } catch {
24876
- }
24877
- throw e;
24878
- }
24879
- },
24880
- shouldRetryUdp,
24881
- isAborted
24882
- );
24883
- return detectOverUdpApi(udpApi, m);
24884
- },
24885
- "UDP discovery failed for all methods."
25183
+ const udpResult = await speculativeUdpRace;
25184
+ logger?.log?.(
25185
+ `[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}`
24886
25186
  );
25187
+ return udpResult;
24887
25188
  } catch (udpError) {
24888
25189
  logger?.log?.(
24889
- `[AutoDetect] Both TCP and UDP failed. TCP error: ${tcpError}, UDP error: ${udpError}`
25190
+ `[AutoDetect] FAILED after ${Date.now() - autodetectStartedAt}ms \u2014 neither TCP nor UDP could reach the camera. TCP: ${tcpError?.message ?? tcpError}. UDP: ${udpError?.message ?? udpError}`
24890
25191
  );
24891
25192
  throw new Error(
24892
25193
  `Failed to connect via both TCP and UDP. TCP: ${tcpError?.message || tcpError}, UDP: ${udpError?.message || udpError}`
@@ -24967,4 +25268,4 @@ export {
24967
25268
  tcpReachabilityProbe,
24968
25269
  autoDetectDeviceType
24969
25270
  };
24970
- //# sourceMappingURL=chunk-OJQLZETO.js.map
25271
+ //# sourceMappingURL=chunk-7HSTETZR.js.map