@apocaliss92/nodelink-js 0.5.1-beta.9 → 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) {
@@ -292,6 +292,7 @@ var BCUDP_DEFAULT_MTU = 1350;
292
292
  var BCUDP_DATA_HEADER_SIZE = 20;
293
293
  var BCUDP_DISCOVERY_PORT_LOCAL_ANY = 2015;
294
294
  var BCUDP_DISCOVERY_PORT_LOCAL_UID = 2018;
295
+ var BCUDP_DISCOVERY_PORT_P2P_SCAN = 9999;
295
296
 
296
297
  // src/bcudp/crc.ts
297
298
  var table;
@@ -618,6 +619,143 @@ function parseD2cHb(xml) {
618
619
  return { cid: Number(cid), did: Number(did) };
619
620
  }
620
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
+
621
759
  // src/bcudp/BcUdpStream.ts
622
760
  var AckLatency = class {
623
761
  currentValues = [];
@@ -656,6 +794,10 @@ function sleep(ms) {
656
794
  return new Promise((r) => setTimeout(r, ms));
657
795
  }
658
796
  var P2P_RELAY_HOSTNAMES = [
797
+ // Master anycast routers — try first.
798
+ "p2pm-abr.reolink.com",
799
+ "p2pm-ali.reolink.com",
800
+ // Numbered regional relays.
659
801
  "p2p.reolink.com",
660
802
  "p2p1.reolink.com",
661
803
  "p2p2.reolink.com",
@@ -667,13 +809,44 @@ var P2P_RELAY_HOSTNAMES = [
667
809
  "p2p8.reolink.com",
668
810
  "p2p9.reolink.com",
669
811
  "p2p10.reolink.com",
670
- "p2p11.reolink.com"
812
+ "p2p11.reolink.com",
813
+ // China-region fallbacks (intentionally last).
814
+ "p2p.reolink.com.cn",
815
+ "p2p1.reolink.com.cn",
816
+ "p2p2.reolink.com.cn",
817
+ "p2p3.reolink.com.cn",
818
+ "p2p4.reolink.com.cn",
819
+ "p2p5.reolink.com.cn",
820
+ "p2p6.reolink.com.cn",
821
+ "p2p7.reolink.com.cn",
822
+ "p2p8.reolink.com.cn",
823
+ "p2p9.reolink.com.cn"
671
824
  ];
825
+ function isUnroutableForP2P(ip) {
826
+ if (!ip) return true;
827
+ if (ip === "0.0.0.0") return true;
828
+ if (ip.startsWith("127.")) return true;
829
+ if (ip.startsWith("10.")) return true;
830
+ if (ip.startsWith("192.168.")) return true;
831
+ if (/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip)) return true;
832
+ if (/^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\./.test(ip)) return true;
833
+ return false;
834
+ }
672
835
  var P2P_LOOKUP_PORT = 9999;
673
836
  var P2P_MAX_WAIT_MS = 15e3;
674
837
  var P2P_RESEND_WAIT_MS = 500;
675
838
  var BcUdpStream = class extends EventEmitter {
676
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;
677
850
  sock;
678
851
  remote;
679
852
  mtu;
@@ -717,6 +890,17 @@ var BcUdpStream = class extends EventEmitter {
717
890
  this.mtu = BCUDP_DEFAULT_MTU;
718
891
  }
719
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
+ }
720
904
  isConnected() {
721
905
  return !!this.sock && !!this.remote && this.cameraId != null;
722
906
  }
@@ -834,27 +1018,89 @@ var BcUdpStream = class extends EventEmitter {
834
1018
  this.remote = { host: connected.rhost, port: connected.rport };
835
1019
  }
836
1020
  async p2pUidLookup(sock, uid) {
837
- const resolved = [];
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
+ }
838
1039
  for (const host of P2P_RELAY_HOSTNAMES) {
1040
+ if (!hostnamesToTry.includes(host)) hostnamesToTry.push(host);
1041
+ }
1042
+ const resolved = [];
1043
+ const sinkholed = [];
1044
+ for (const host of hostnamesToTry) {
839
1045
  try {
840
1046
  const answers = await dns.lookup(host, { family: 4, all: true });
1047
+ let publicCount = 0;
1048
+ let sinkCount = 0;
841
1049
  for (const a of answers) {
842
- if (a.address && !resolved.includes(a.address))
1050
+ if (!a.address) continue;
1051
+ if (isUnroutableForP2P(a.address)) {
1052
+ sinkholed.push({ host, ip: a.address });
1053
+ sinkCount++;
1054
+ continue;
1055
+ }
1056
+ if (!resolved.includes(a.address)) {
843
1057
  resolved.push(a.address);
1058
+ publicCount++;
1059
+ }
844
1060
  }
845
- } 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;
846
1075
  }
847
1076
  }
848
1077
  if (resolved.length === 0) {
1078
+ if (sinkholed.length > 0) {
1079
+ const samples = sinkholed.slice(0, 3).map((s) => `${s.host} \u2192 ${s.ip}`).join(", ");
1080
+ throw new Error(
1081
+ `P2P UID lookup failed: DNS resolves Reolink P2P hostnames to non-routable IPs (${samples}). This is almost certainly an /etc/hosts rewrite or a DNS filter (Pi-hole, AdGuard, NextDNS) blocking *.reolink.com. Battery cameras cannot connect without P2P \u2014 whitelist *.reolink.com (at least p2p*.reolink.com on UDP/9999) or remove the override. Verify with \`getent hosts p2p.reolink.com\` inside your container \u2014 if it differs from \`nslookup p2p.reolink.com\`, the offender is /etc/hosts.`
1082
+ );
1083
+ }
849
1084
  throw new Error(
850
- "P2P UID lookup failed: no p2p.reolink.com addresses resolved"
1085
+ "P2P UID lookup failed: no p2p.reolink.com addresses resolved (DNS failure)"
851
1086
  );
852
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
+ );
853
1091
  const start = Date.now();
854
1092
  let lastErr;
1093
+ let attemptsMade = 0;
855
1094
  for (const ip of resolved) {
856
1095
  const remaining = P2P_MAX_WAIT_MS - (Date.now() - start);
857
- 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();
858
1104
  try {
859
1105
  const res = await this.p2pUidLookupOne(
860
1106
  sock,
@@ -862,11 +1108,20 @@ var BcUdpStream = class extends EventEmitter {
862
1108
  { host: ip, port: P2P_LOOKUP_PORT },
863
1109
  Math.min(remaining, 3e3)
864
1110
  );
1111
+ log(
1112
+ `${ip}:${P2P_LOOKUP_PORT} replied in ${Date.now() - probeStart}ms \u2713 (total ${Date.now() - t0}ms)`
1113
+ );
865
1114
  return res;
866
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})`);
867
1119
  lastErr = e instanceof Error ? e : new Error(String(e));
868
1120
  }
869
1121
  }
1122
+ log(
1123
+ `Exhausted all ${attemptsMade} relay candidate(s) after ${Date.now() - t0}ms \u2014 UID lookup failed`
1124
+ );
870
1125
  throw lastErr ?? new Error("P2P UID lookup failed");
871
1126
  }
872
1127
  async p2pUidLookupOne(sock, uid, dest, timeoutMs) {
@@ -1181,7 +1436,8 @@ var BcUdpStream = class extends EventEmitter {
1181
1436
  throw new Error("Internal: discoveryUidLocal called for non-uid mode");
1182
1437
  const ports = [
1183
1438
  BCUDP_DISCOVERY_PORT_LOCAL_ANY,
1184
- BCUDP_DISCOVERY_PORT_LOCAL_UID
1439
+ BCUDP_DISCOVERY_PORT_LOCAL_UID,
1440
+ BCUDP_DISCOVERY_PORT_P2P_SCAN
1185
1441
  ];
1186
1442
  const broadcastHosts = ["255.255.255.255"];
1187
1443
  const ifaces = networkInterfaces();
@@ -1204,13 +1460,23 @@ var BcUdpStream = class extends EventEmitter {
1204
1460
  const directHost = (this.opts.directHost ?? "").trim();
1205
1461
  const localMode = opts?.localMode ?? "local-broadcast";
1206
1462
  const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
1207
- const discoveryTimeout = 3e4;
1463
+ const discoveryTimeout = typeof this.opts.localDiscoveryTimeoutMs === "number" && this.opts.localDiscoveryTimeoutMs > 0 ? this.opts.localDiscoveryTimeoutMs : 15e3;
1208
1464
  const retryInterval = 500;
1209
1465
  const startMs = Date.now();
1210
1466
  sock.setBroadcast(true);
1211
1467
  const addr = sock.address();
1212
1468
  const localPort = typeof addr === "string" ? 0 : addr.port;
1213
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
+ });
1214
1480
  const xml = buildC2dC({
1215
1481
  uid: this.opts.uid,
1216
1482
  clientPort: localPort,
@@ -1221,6 +1487,9 @@ var BcUdpStream = class extends EventEmitter {
1221
1487
  const timeout = setTimeout(() => {
1222
1488
  if (retryTimer) clearInterval(retryTimer);
1223
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
+ );
1224
1493
  reject(
1225
1494
  new Error(
1226
1495
  `BCUDP discovery timeout after ${discoveryTimeout}ms (camera may be sleeping or unreachable)`
@@ -1414,6 +1683,7 @@ var BcUdpStream = class extends EventEmitter {
1414
1683
  for (const port of ports) {
1415
1684
  try {
1416
1685
  sock.send(packet, port, host);
1686
+ bytesSent += packet.length;
1417
1687
  retryCount++;
1418
1688
  this.emit("debug", "discovery_send", { retryCount, host, port });
1419
1689
  } catch {
@@ -2950,6 +3220,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2950
3220
  sock.on("debug", (event, data) => {
2951
3221
  this.logDebug(`udp_${event}`, data);
2952
3222
  });
3223
+ sock.setLogger(this.logger);
2953
3224
  await sock.connect();
2954
3225
  const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : "";
2955
3226
  const udpDiscoveryMethod = this.opts.udpDiscoveryMethod ?? "local-direct";
@@ -13466,7 +13737,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
13466
13737
  return;
13467
13738
  }
13468
13739
  entry.startInFlight = (async () => {
13469
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-NTIGPHYJ.js");
13740
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-OCLOM452.js");
13470
13741
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
13471
13742
  const dedicated = await this.createDedicatedSession(sessionKey);
13472
13743
  const stream = new BaichuanVideoStream2({
@@ -20196,7 +20467,7 @@ ${xml}`
20196
20467
  * @returns Test results for all stream types and profiles
20197
20468
  */
20198
20469
  async testChannelStreams(channel, logger) {
20199
- const { testChannelStreams } = await import("./DiagnosticsTools-7BIWJDZS.js");
20470
+ const { testChannelStreams } = await import("./DiagnosticsTools-K4MF2VXZ.js");
20200
20471
  return await testChannelStreams({
20201
20472
  api: this,
20202
20473
  channel: this.normalizeChannel(channel),
@@ -20212,7 +20483,7 @@ ${xml}`
20212
20483
  * @returns Complete diagnostics for all channels and streams
20213
20484
  */
20214
20485
  async collectMultifocalDiagnostics(logger) {
20215
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-7BIWJDZS.js");
20486
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-K4MF2VXZ.js");
20216
20487
  return await collectMultifocalDiagnostics({
20217
20488
  api: this,
20218
20489
  logger
@@ -23478,7 +23749,11 @@ async function discoverViaUdpDirect(host, options) {
23478
23749
  });
23479
23750
  socket.bind(() => {
23480
23751
  const localPort = socket.address().port;
23481
- const discoveryPorts = [BCUDP_DISCOVERY_PORT_LOCAL_ANY, BCUDP_DISCOVERY_PORT_LOCAL_UID];
23752
+ const discoveryPorts = [
23753
+ BCUDP_DISCOVERY_PORT_LOCAL_ANY,
23754
+ BCUDP_DISCOVERY_PORT_LOCAL_UID,
23755
+ BCUDP_DISCOVERY_PORT_P2P_SCAN
23756
+ ];
23482
23757
  for (const port of discoveryPorts) {
23483
23758
  try {
23484
23759
  const tid = Math.floor(Math.random() * 255) || 1;
@@ -23767,7 +24042,11 @@ async function discoverViaUdpBroadcast(options) {
23767
24042
  socket.bind(() => {
23768
24043
  socket.setBroadcast(true);
23769
24044
  const localPort = socket.address().port;
23770
- const discoveryPorts = [BCUDP_DISCOVERY_PORT_LOCAL_ANY, BCUDP_DISCOVERY_PORT_LOCAL_UID];
24045
+ const discoveryPorts = [
24046
+ BCUDP_DISCOVERY_PORT_LOCAL_ANY,
24047
+ BCUDP_DISCOVERY_PORT_LOCAL_UID,
24048
+ BCUDP_DISCOVERY_PORT_P2P_SCAN
24049
+ ];
23771
24050
  for (const port of discoveryPorts) {
23772
24051
  try {
23773
24052
  const tid = Math.floor(Math.random() * 255) || 1;
@@ -24259,22 +24538,102 @@ async function discoverUidForHost(host, logger) {
24259
24538
  function isTcpFailureThatShouldFallbackToUdp(e) {
24260
24539
  const message = e?.message || e?.toString?.() || "";
24261
24540
  if (typeof message !== "string") return false;
24262
- 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");
24263
24546
  }
24264
24547
  async function pingHost(host, timeoutMs = 3e3) {
24265
- const { exec } = await import("child_process");
24548
+ if (!host || typeof host !== "string") return false;
24266
24549
  const platform2 = process.platform;
24267
- const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
24268
- // macOS: -W is in milliseconds (Linux: seconds)
24269
- `ping -c 1 -W ${timeoutMs} ${host}`
24270
- ) : (
24271
- // Linux/BSD-ish: -W is in seconds on most distros
24272
- `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
24273
- );
24550
+ const pingCandidates = platform2 === "win32" ? ["ping"] : platform2 === "darwin" ? ["/sbin/ping", "/usr/sbin/ping", "ping"] : ["/bin/ping", "/usr/bin/ping", "ping"];
24551
+ const pingArgs = (bin) => {
24552
+ void bin;
24553
+ if (platform2 === "win32") {
24554
+ return ["-n", "1", "-w", String(timeoutMs), host];
24555
+ }
24556
+ if (platform2 === "darwin") {
24557
+ return ["-c", "1", "-W", String(timeoutMs), host];
24558
+ }
24559
+ return ["-c", "1", "-W", String(Math.max(1, Math.floor(timeoutMs / 1e3))), host];
24560
+ };
24561
+ const { spawn: spawn3 } = await import("child_process");
24562
+ for (const bin of pingCandidates) {
24563
+ const ranOk = await new Promise((resolve) => {
24564
+ let settled = false;
24565
+ let child;
24566
+ try {
24567
+ child = spawn3(bin, pingArgs(bin), { stdio: "ignore" });
24568
+ } catch {
24569
+ resolve("spawn-failed");
24570
+ return;
24571
+ }
24572
+ const timer = setTimeout(() => {
24573
+ if (settled) return;
24574
+ settled = true;
24575
+ try {
24576
+ child?.kill("SIGKILL");
24577
+ } catch {
24578
+ }
24579
+ resolve(false);
24580
+ }, timeoutMs + 500);
24581
+ child.on("error", () => {
24582
+ if (settled) return;
24583
+ settled = true;
24584
+ clearTimeout(timer);
24585
+ resolve("spawn-failed");
24586
+ });
24587
+ child.on("exit", (code) => {
24588
+ if (settled) return;
24589
+ settled = true;
24590
+ clearTimeout(timer);
24591
+ resolve(code === 0);
24592
+ });
24593
+ });
24594
+ if (ranOk === true) return true;
24595
+ if (ranOk === "spawn-failed") continue;
24596
+ break;
24597
+ }
24598
+ for (const port of [9e3, 443, 80]) {
24599
+ if (await tcpReachabilityProbe(host, port, 800)) return true;
24600
+ }
24601
+ return false;
24602
+ }
24603
+ async function tcpReachabilityProbe(host, port, timeoutMs) {
24604
+ const net4 = await import("net");
24274
24605
  return new Promise((resolve) => {
24275
- exec(pingCmd, (error) => {
24276
- resolve(!error);
24606
+ let settled = false;
24607
+ const socket = new net4.Socket();
24608
+ const timer = setTimeout(() => {
24609
+ if (settled) return;
24610
+ settled = true;
24611
+ try {
24612
+ socket.destroy();
24613
+ } catch {
24614
+ }
24615
+ resolve(false);
24616
+ }, timeoutMs);
24617
+ const finish = (reachable) => {
24618
+ if (settled) return;
24619
+ settled = true;
24620
+ clearTimeout(timer);
24621
+ try {
24622
+ socket.destroy();
24623
+ } catch {
24624
+ }
24625
+ resolve(reachable);
24626
+ };
24627
+ socket.once("connect", () => finish(true));
24628
+ socket.once("error", (err) => {
24629
+ if (err?.code === "ECONNREFUSED") finish(true);
24630
+ else finish(false);
24277
24631
  });
24632
+ try {
24633
+ socket.connect(port, host);
24634
+ } catch {
24635
+ finish(false);
24636
+ }
24278
24637
  });
24279
24638
  }
24280
24639
  function createBaichuanApi(inputs, transport) {
@@ -24331,6 +24690,7 @@ function attachErrorHandler(api, transport, inputs) {
24331
24690
  }
24332
24691
  async function autoDetectDeviceType(inputs) {
24333
24692
  const { host, uid, logger } = inputs;
24693
+ const autodetectStartedAt = Date.now();
24334
24694
  const mode = inputs.mode ?? "auto";
24335
24695
  const maxRetriesRaw = inputs.maxRetries;
24336
24696
  const maxRetries = Math.max(
@@ -24347,8 +24707,31 @@ async function autoDetectDeviceType(inputs) {
24347
24707
  const sleepMs2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24348
24708
  const shouldRetryTcp = (e) => {
24349
24709
  const msg = fmtErr(e);
24710
+ if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTDOWN") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH") || msg.includes("ENETDOWN")) {
24711
+ return false;
24712
+ }
24350
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");
24351
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
+ };
24352
24735
  const shouldRetryUdp = (e) => {
24353
24736
  const msg = fmtErr(e);
24354
24737
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
@@ -24401,6 +24784,11 @@ async function autoDetectDeviceType(inputs) {
24401
24784
  }
24402
24785
  };
24403
24786
  const effectiveUid = normalizeUid(uid);
24787
+ const speculativeUidPromise = effectiveUid !== void 0 ? Promise.resolve(effectiveUid) : mode === "tcp" ? Promise.resolve(void 0) : discoverUidForHost(host, logger).then((d) => normalizeUid(d) ?? void 0).catch(() => void 0);
24788
+ speculativeUidPromise.then(
24789
+ () => void 0,
24790
+ () => void 0
24791
+ );
24404
24792
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
24405
24793
  const isReachable = await pingHost(host);
24406
24794
  if (!isReachable) {
@@ -24414,19 +24802,11 @@ async function autoDetectDeviceType(inputs) {
24414
24802
  logger?.log?.(
24415
24803
  `[AutoDetect] Forced mode=udp, skipping TCP and starting UDP discovery/login...`
24416
24804
  );
24417
- let normalizedUid = effectiveUid;
24805
+ let normalizedUid = await speculativeUidPromise;
24418
24806
  if (!normalizedUid) {
24419
- logger?.log?.(
24420
- `[AutoDetect] UID not provided; attempting UDP discovery for UID...`
24807
+ throw new Error(
24808
+ `Forced UDP autodetect requires UID (or successful UDP UID discovery), but none was provided/discovered (ip=${host}).`
24421
24809
  );
24422
- const discovered = await discoverUidForHost(host, logger);
24423
- const normalizedDiscovered = normalizeUid(discovered);
24424
- if (!normalizedDiscovered) {
24425
- throw new Error(
24426
- `Forced UDP autodetect requires UID (or successful UDP UID discovery), but none was provided/discovered (ip=${host}).`
24427
- );
24428
- }
24429
- normalizedUid = normalizedDiscovered;
24430
24810
  }
24431
24811
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
24432
24812
  return await runUdpMethodsParallel(
@@ -24504,6 +24884,127 @@ async function autoDetectDeviceType(inputs) {
24504
24884
  "Forced UDP autodetect failed for all methods."
24505
24885
  );
24506
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
+ };
24507
25008
  let tcpApi;
24508
25009
  try {
24509
25010
  logger?.log?.(`[AutoDetect] Trying TCP connection to ${host}...`);
@@ -24513,7 +25014,7 @@ async function autoDetectDeviceType(inputs) {
24513
25014
  async (attempt) => {
24514
25015
  const api2 = createBaichuanApi(inputs, "tcp");
24515
25016
  try {
24516
- await api2.login();
25017
+ await withTcpDeadline(api2.login());
24517
25018
  return api2;
24518
25019
  } catch (e) {
24519
25020
  try {
@@ -24625,7 +25126,7 @@ async function autoDetectDeviceType(inputs) {
24625
25126
  logger?.log?.(
24626
25127
  `[AutoDetect] Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum})`
24627
25128
  );
24628
- return {
25129
+ return _tcpWin({
24629
25130
  type: "multifocal",
24630
25131
  transport: "tcp",
24631
25132
  uid: effectiveUid || uid || "",
@@ -24633,13 +25134,13 @@ async function autoDetectDeviceType(inputs) {
24633
25134
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
24634
25135
  channelNum: effectiveChannelNum,
24635
25136
  api
24636
- };
25137
+ });
24637
25138
  }
24638
25139
  if (effectiveChannelNum > 1) {
24639
25140
  logger?.log?.(
24640
25141
  `[AutoDetect] Detected NVR (${effectiveChannelNum} channels)`
24641
25142
  );
24642
- return {
25143
+ return _tcpWin({
24643
25144
  type: "nvr",
24644
25145
  transport: "tcp",
24645
25146
  uid: effectiveUid || uid || "",
@@ -24647,10 +25148,10 @@ async function autoDetectDeviceType(inputs) {
24647
25148
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
24648
25149
  channelNum: effectiveChannelNum,
24649
25150
  api
24650
- };
25151
+ });
24651
25152
  }
24652
25153
  logger?.log?.(`[AutoDetect] Detected regular camera (single channel)`);
24653
- return {
25154
+ return _tcpWin({
24654
25155
  type: "camera",
24655
25156
  transport: "tcp",
24656
25157
  uid: effectiveUid || uid || "",
@@ -24658,7 +25159,7 @@ async function autoDetectDeviceType(inputs) {
24658
25159
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
24659
25160
  channelNum: 1,
24660
25161
  api
24661
- };
25162
+ });
24662
25163
  } catch (tcpError) {
24663
25164
  if (mode === "tcp") {
24664
25165
  throw tcpError;
@@ -24673,111 +25174,20 @@ async function autoDetectDeviceType(inputs) {
24673
25174
  throw tcpError;
24674
25175
  }
24675
25176
  logger?.log?.(`[AutoDetect] TCP failed, trying UDP...`);
24676
- let normalizedUid = effectiveUid;
24677
- if (!normalizedUid) {
24678
- logger?.log?.(
24679
- `[AutoDetect] UID not provided; attempting UDP broadcast discovery for UID...`
25177
+ if (!speculativeUdpRace) {
25178
+ throw new Error(
25179
+ `AutoDetect internal: speculative UDP race missing in mode=${mode}`
24680
25180
  );
24681
- const discovered = await discoverUidForHost(host, logger);
24682
- if (discovered) {
24683
- const normalizedDiscovered = normalizeUid(discovered);
24684
- if (normalizedDiscovered) {
24685
- normalizedUid = normalizedDiscovered;
24686
- logger?.log?.(
24687
- `[AutoDetect] UID discovered via broadcast: ${normalizedUid}`
24688
- );
24689
- }
24690
- }
24691
- if (!normalizedUid) {
24692
- logger?.log?.(
24693
- `[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.`
24694
- );
24695
- }
24696
25181
  }
24697
25182
  try {
24698
- const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
24699
- const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
24700
- udpApi.getInfo(),
24701
- udpApi.getDeviceCapabilities(),
24702
- udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
24703
- ]);
24704
- const channelNum = capabilities?.support?.channelNum ?? 1;
24705
- const model = deviceInfo.type?.trim();
24706
- const normalizedModel = model ? model.trim() : void 0;
24707
- const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
24708
- const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
24709
- const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
24710
- const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
24711
- const hasBattery = capabilities?.capabilities?.hasBattery === true;
24712
- udpApi.setIdleDisconnect(hasBattery);
24713
- if (isMultifocal) {
24714
- const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
24715
- logger?.log?.(
24716
- `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
24717
- );
24718
- return {
24719
- type: "multifocal",
24720
- transport: "udp",
24721
- uid: normalizedUid ?? "",
24722
- udpDiscoveryMethod,
24723
- deviceInfo,
24724
- ...hostNetworkInfo ? { hostNetworkInfo } : {},
24725
- channelNum,
24726
- hasBattery,
24727
- api: udpApi
24728
- };
24729
- }
24730
- const deviceType = hasBattery ? "battery-cam" : "udp-camera";
24731
- logger?.log?.(
24732
- `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
24733
- );
24734
- return {
24735
- type: deviceType,
24736
- transport: "udp",
24737
- uid: normalizedUid ?? "",
24738
- udpDiscoveryMethod,
24739
- deviceInfo,
24740
- ...hostNetworkInfo ? { hostNetworkInfo } : {},
24741
- channelNum: 1,
24742
- hasBattery,
24743
- api: udpApi
24744
- };
24745
- };
24746
- const viableMethods = selectViableUdpMethods(Boolean(normalizedUid));
24747
- return await runUdpMethodsParallel(
24748
- viableMethods,
24749
- async (m, isAborted) => {
24750
- logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
24751
- const udpApi = await withRetries(
24752
- `UDP(${m})`,
24753
- maxRetries,
24754
- async (attempt) => {
24755
- const apiInputs = { ...inputs, udpDiscoveryMethod: m };
24756
- if (normalizedUid) apiInputs.uid = normalizedUid;
24757
- const api = createBaichuanApi(apiInputs, "udp");
24758
- try {
24759
- await api.login();
24760
- return api;
24761
- } catch (e) {
24762
- try {
24763
- await api.close({
24764
- reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
24765
- });
24766
- } catch {
24767
- }
24768
- throw e;
24769
- }
24770
- },
24771
- shouldRetryUdp,
24772
- isAborted
24773
- );
24774
- return detectOverUdpApi(udpApi, m);
24775
- },
24776
- "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}`
24777
25186
  );
25187
+ return udpResult;
24778
25188
  } catch (udpError) {
24779
25189
  logger?.log?.(
24780
- `[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}`
24781
25191
  );
24782
25192
  throw new Error(
24783
25193
  `Failed to connect via both TCP and UDP. TCP: ${tcpError?.message || tcpError}, UDP: ${udpError?.message || udpError}`
@@ -24790,6 +25200,7 @@ export {
24790
25200
  encodeHeader,
24791
25201
  decodeHeader,
24792
25202
  BaichuanFrameParser,
25203
+ isUnroutableForP2P,
24793
25204
  BcUdpStream,
24794
25205
  asLogger,
24795
25206
  createNullLogger,
@@ -24854,6 +25265,7 @@ export {
24854
25265
  normalizeUid,
24855
25266
  maskUid,
24856
25267
  isTcpFailureThatShouldFallbackToUdp,
25268
+ tcpReachabilityProbe,
24857
25269
  autoDetectDeviceType
24858
25270
  };
24859
- //# sourceMappingURL=chunk-27IU7NXS.js.map
25271
+ //# sourceMappingURL=chunk-7HSTETZR.js.map