@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/index.cjs CHANGED
@@ -34,7 +34,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
34
34
  function bcHeaderHasPayloadOffset(messageClass) {
35
35
  return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
36
36
  }
37
- var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGIN, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_ALARM_EVENT_LIST, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_GET_SNAPSHOT, BC_CMD_ID_GET_UID, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_SET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_VERSION_INFO, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_GET_EMAIL, BC_CMD_ID_SET_EMAIL, BC_CMD_ID_TEST_EMAIL, BC_CMD_ID_GET_NTP, BC_CMD_ID_SET_NTP, BC_CMD_ID_SET_SYSTEM_GENERAL, BC_CMD_ID_GET_DST, BC_CMD_ID_SET_DST, BC_CMD_ID_GET_AUTO_REBOOT, BC_CMD_ID_SET_AUTO_REBOOT, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
37
+ var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGIN, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_ALARM_EVENT_LIST, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_DEVICE_DETECT_CANDIDATES, BC_CMD_ID_DEVICE_DETECT, BC_CMD_ID_GET_SNAPSHOT, BC_CMD_ID_GET_UID, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_SET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_VERSION_INFO, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_GET_EMAIL, BC_CMD_ID_SET_EMAIL, BC_CMD_ID_TEST_EMAIL, BC_CMD_ID_GET_NTP, BC_CMD_ID_SET_NTP, BC_CMD_ID_SET_SYSTEM_GENERAL, BC_CMD_ID_GET_DST, BC_CMD_ID_SET_DST, BC_CMD_ID_GET_AUTO_REBOOT, BC_CMD_ID_SET_AUTO_REBOOT, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
38
38
  var init_constants = __esm({
39
39
  "src/protocol/constants.ts"() {
40
40
  "use strict";
@@ -107,6 +107,8 @@ var init_constants = __esm({
107
107
  BC_CMD_ID_ABILITY_INFO = 151;
108
108
  BC_CMD_ID_SUPPORT = 199;
109
109
  BC_CMD_ID_PING = 93;
110
+ BC_CMD_ID_DEVICE_DETECT_CANDIDATES = [31, 132, 150];
111
+ BC_CMD_ID_DEVICE_DETECT = null;
110
112
  BC_CMD_ID_GET_SNAPSHOT = 109;
111
113
  BC_CMD_ID_GET_UID = 114;
112
114
  BC_CMD_ID_CHANNEL_INFO_ALL = 145;
@@ -8256,6 +8258,8 @@ __export(index_exports, {
8256
8258
  BC_CMD_ID_COVER_STANDALONE_460: () => BC_CMD_ID_COVER_STANDALONE_460,
8257
8259
  BC_CMD_ID_COVER_STANDALONE_461: () => BC_CMD_ID_COVER_STANDALONE_461,
8258
8260
  BC_CMD_ID_COVER_STANDALONE_462: () => BC_CMD_ID_COVER_STANDALONE_462,
8261
+ BC_CMD_ID_DEVICE_DETECT: () => BC_CMD_ID_DEVICE_DETECT,
8262
+ BC_CMD_ID_DEVICE_DETECT_CANDIDATES: () => BC_CMD_ID_DEVICE_DETECT_CANDIDATES,
8259
8263
  BC_CMD_ID_DING_DONG_CTRL: () => BC_CMD_ID_DING_DONG_CTRL,
8260
8264
  BC_CMD_ID_DING_DONG_OPT: () => BC_CMD_ID_DING_DONG_OPT,
8261
8265
  BC_CMD_ID_FILE_INFO_LIST_CLOSE: () => BC_CMD_ID_FILE_INFO_LIST_CLOSE,
@@ -9017,6 +9021,143 @@ function parseD2cHb(xml) {
9017
9021
  return { cid: Number(cid), did: Number(did) };
9018
9022
  }
9019
9023
 
9024
+ // src/cloud/server-binding.ts
9025
+ var REOLINK_API_V2_BASE = "https://apis.reolink.com/v2";
9026
+ var POSITIVE_TTL_MS = 24 * 60 * 60 * 1e3;
9027
+ var NEGATIVE_TTL_MS = 30 * 1e3;
9028
+ var cache = /* @__PURE__ */ new Map();
9029
+ function readCache(uid, now) {
9030
+ const e = cache.get(uid);
9031
+ if (!e) return void 0;
9032
+ if (now >= e.expires) {
9033
+ cache.delete(uid);
9034
+ return void 0;
9035
+ }
9036
+ return e;
9037
+ }
9038
+ async function getServerBinding(uid, options = {}) {
9039
+ if (!uid || typeof uid !== "string") return void 0;
9040
+ const now = Date.now();
9041
+ const cached = readCache(uid, now);
9042
+ if (cached?.kind === "ok") return cached.response;
9043
+ if (cached?.kind === "err") return void 0;
9044
+ const language = options.language ?? "en";
9045
+ const baseUrl = options.baseUrl ?? REOLINK_API_V2_BASE;
9046
+ const timeoutMs = options.timeoutMs ?? 4e3;
9047
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
9048
+ const logger = options.logger;
9049
+ if (typeof fetchImpl !== "function") {
9050
+ logger?.debug?.(
9051
+ `[server-binding] global fetch unavailable; skipping cloud lookup`
9052
+ );
9053
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9054
+ return void 0;
9055
+ }
9056
+ const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
9057
+ const controller = new AbortController();
9058
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
9059
+ try {
9060
+ const res = await fetchImpl(url, {
9061
+ method: "GET",
9062
+ signal: controller.signal,
9063
+ headers: { Accept: "application/json" }
9064
+ });
9065
+ if (!res.ok) {
9066
+ logger?.debug?.(
9067
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
9068
+ );
9069
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9070
+ return void 0;
9071
+ }
9072
+ const json = await res.json();
9073
+ const parsed = parseServerBindingResponse(json);
9074
+ if (!parsed) {
9075
+ logger?.debug?.(
9076
+ `[server-binding] ${uid}: response shape did not match expectations`
9077
+ );
9078
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9079
+ return void 0;
9080
+ }
9081
+ cache.set(uid, {
9082
+ kind: "ok",
9083
+ response: parsed,
9084
+ expires: now + POSITIVE_TTL_MS
9085
+ });
9086
+ const pick = parsed.availableZones.find(
9087
+ (z) => z.status === "active" && z.services.p2p?.server
9088
+ );
9089
+ const hint = pick?.services.p2p?.server ?? parsed.availableZones[0]?.services.p2p?.server;
9090
+ logger?.log?.(
9091
+ `[server-binding] ${uid}: ${parsed.availableZones.length} zone(s)${hint ? `, p2p hint=${hint}` : ""}`
9092
+ );
9093
+ return parsed;
9094
+ } catch (e) {
9095
+ logger?.debug?.(
9096
+ `[server-binding] ${uid}: ${e?.message ?? String(e)}`
9097
+ );
9098
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9099
+ return void 0;
9100
+ } finally {
9101
+ clearTimeout(timer);
9102
+ }
9103
+ }
9104
+ function pickP2pHostFromBinding(response) {
9105
+ if (!response) return void 0;
9106
+ const zones = response.availableZones;
9107
+ if (!zones || zones.length === 0) return void 0;
9108
+ const active = zones.find(
9109
+ (z) => z.status === "active" && z.services.p2p?.server
9110
+ );
9111
+ if (active?.services.p2p?.server) return active.services.p2p.server;
9112
+ const def = zones.find(
9113
+ (z) => z.status === "default" && z.services.p2p?.server
9114
+ );
9115
+ if (def?.services.p2p?.server) return def.services.p2p.server;
9116
+ const any = zones.find((z) => z.services.p2p?.server);
9117
+ return any?.services.p2p?.server;
9118
+ }
9119
+ function isString(v) {
9120
+ return typeof v === "string";
9121
+ }
9122
+ function parseServerBindingResponse(raw) {
9123
+ if (!raw || typeof raw !== "object") return void 0;
9124
+ const rawZones = raw.availableZones;
9125
+ if (!Array.isArray(rawZones)) return void 0;
9126
+ const zones = [];
9127
+ for (const r of rawZones) {
9128
+ if (!r || typeof r !== "object") continue;
9129
+ const rec = r;
9130
+ const id = rec.id;
9131
+ const name = rec.name;
9132
+ const status = rec.status;
9133
+ if (!isString(id) || !isString(name) || !isString(status)) continue;
9134
+ const servicesRaw = rec.services;
9135
+ const services = {};
9136
+ if (servicesRaw && typeof servicesRaw === "object") {
9137
+ const s = servicesRaw;
9138
+ for (const key of ["p2p", "cloud", "roms_ota", "alarm_push"]) {
9139
+ const v = s[key];
9140
+ if (v && typeof v === "object") {
9141
+ const server = v.server;
9142
+ if (isString(server) && server.length > 0) {
9143
+ services[key] = { server };
9144
+ }
9145
+ }
9146
+ }
9147
+ }
9148
+ const locationsRaw = rec.locations;
9149
+ const locations = Array.isArray(locationsRaw) && locationsRaw.every(isString) ? locationsRaw : void 0;
9150
+ zones.push({
9151
+ id,
9152
+ name,
9153
+ status,
9154
+ services,
9155
+ ...locations ? { locations } : {}
9156
+ });
9157
+ }
9158
+ return { availableZones: zones };
9159
+ }
9160
+
9020
9161
  // src/bcudp/BcUdpStream.ts
9021
9162
  var AckLatency = class {
9022
9163
  currentValues = [];
@@ -9098,6 +9239,16 @@ var P2P_MAX_WAIT_MS = 15e3;
9098
9239
  var P2P_RESEND_WAIT_MS = 500;
9099
9240
  var BcUdpStream = class extends import_node_events.EventEmitter {
9100
9241
  opts;
9242
+ /**
9243
+ * Optional info-level logger for diagnostic milestones — set via
9244
+ * {@link BcUdpStream.setLogger} by `BaichuanClient` so the lib's
9245
+ * standard logger sink sees BCUDP / P2P progress (DNS resolutions,
9246
+ * outgoing UDP probes, timeouts with elapsed times) without the user
9247
+ * having to opt into the per-packet `debug` event firehose. Kept
9248
+ * separate from `emit('debug', ...)` because that channel is intended
9249
+ * for the per-packet debug trace and is gated by debugOptions.
9250
+ */
9251
+ discoveryLogger;
9101
9252
  sock;
9102
9253
  remote;
9103
9254
  mtu;
@@ -9141,6 +9292,17 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9141
9292
  this.mtu = BCUDP_DEFAULT_MTU;
9142
9293
  }
9143
9294
  /** True if the underlying UDP socket is open and the remote peer is known. */
9295
+ /**
9296
+ * Attach an info-level logger for high-signal diagnostic milestones
9297
+ * (DNS resolution, outgoing UDP probe sends, P2P UID lookup wins/losses,
9298
+ * BCUDP local discovery timeouts). The lib's `BaichuanClient` calls
9299
+ * this immediately after constructing the stream so consumers get
9300
+ * actionable progress logs without enabling the per-packet debug trace.
9301
+ * Safe to call repeatedly; only the most recent logger is used.
9302
+ */
9303
+ setLogger(logger) {
9304
+ this.discoveryLogger = logger;
9305
+ }
9144
9306
  isConnected() {
9145
9307
  return !!this.sock && !!this.remote && this.cameraId != null;
9146
9308
  }
@@ -9258,20 +9420,60 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9258
9420
  this.remote = { host: connected.rhost, port: connected.rport };
9259
9421
  }
9260
9422
  async p2pUidLookup(sock, uid) {
9423
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
9424
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
9425
+ const t0 = Date.now();
9426
+ const hostnamesToTry = [];
9427
+ const binding = await getServerBinding(uid, {
9428
+ ...this.discoveryLogger ? { logger: this.discoveryLogger } : {}
9429
+ }).catch(() => void 0);
9430
+ const hintedHost = pickP2pHostFromBinding(binding);
9431
+ if (hintedHost) {
9432
+ hostnamesToTry.push(hintedHost);
9433
+ log(
9434
+ `UID=${shortUid} cloud server-binding \u2192 hint=${hintedHost} (will try first)`
9435
+ );
9436
+ } else {
9437
+ log(
9438
+ `UID=${shortUid} cloud server-binding \u2192 no hint (apis.reolink.com unreachable / no zone match) \u2192 sweeping ${P2P_RELAY_HOSTNAMES.length} fallback hostnames`
9439
+ );
9440
+ }
9441
+ for (const host of P2P_RELAY_HOSTNAMES) {
9442
+ if (!hostnamesToTry.includes(host)) hostnamesToTry.push(host);
9443
+ }
9261
9444
  const resolved = [];
9262
9445
  const sinkholed = [];
9263
- for (const host of P2P_RELAY_HOSTNAMES) {
9446
+ for (const host of hostnamesToTry) {
9264
9447
  try {
9265
9448
  const answers = await import_promises.default.lookup(host, { family: 4, all: true });
9449
+ let publicCount = 0;
9450
+ let sinkCount = 0;
9266
9451
  for (const a of answers) {
9267
9452
  if (!a.address) continue;
9268
9453
  if (isUnroutableForP2P(a.address)) {
9269
9454
  sinkholed.push({ host, ip: a.address });
9455
+ sinkCount++;
9270
9456
  continue;
9271
9457
  }
9272
- if (!resolved.includes(a.address)) resolved.push(a.address);
9458
+ if (!resolved.includes(a.address)) {
9459
+ resolved.push(a.address);
9460
+ publicCount++;
9461
+ }
9273
9462
  }
9274
- } catch {
9463
+ if (sinkCount > 0 && publicCount === 0) {
9464
+ log(
9465
+ `DNS ${host} \u2192 sinkhole (${sinkholed[sinkholed.length - 1]?.ip}) \u2014 DNS filter / /etc/hosts override`
9466
+ );
9467
+ } else if (publicCount > 0) {
9468
+ if (host === hintedHost) {
9469
+ log(`DNS ${host} \u2192 ${answers.find((a) => !isUnroutableForP2P(a.address))?.address} \u2713`);
9470
+ }
9471
+ }
9472
+ } catch (e) {
9473
+ log(`DNS ${host} \u2192 ENOTFOUND/timeout (${e?.code ?? "?"})`);
9474
+ }
9475
+ if (hintedHost && host === hintedHost && resolved.length > 0 && sinkholed.length === 0) {
9476
+ break;
9275
9477
  }
9276
9478
  }
9277
9479
  if (resolved.length === 0) {
@@ -9285,11 +9487,22 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9285
9487
  "P2P UID lookup failed: no p2p.reolink.com addresses resolved (DNS failure)"
9286
9488
  );
9287
9489
  }
9490
+ log(
9491
+ `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)`
9492
+ );
9288
9493
  const start = Date.now();
9289
9494
  let lastErr;
9495
+ let attemptsMade = 0;
9290
9496
  for (const ip of resolved) {
9291
9497
  const remaining = P2P_MAX_WAIT_MS - (Date.now() - start);
9292
- if (remaining <= 0) break;
9498
+ if (remaining <= 0) {
9499
+ log(
9500
+ `Aborting after ${attemptsMade} attempt(s) \u2014 total budget ${P2P_MAX_WAIT_MS}ms exhausted`
9501
+ );
9502
+ break;
9503
+ }
9504
+ attemptsMade++;
9505
+ const probeStart = Date.now();
9293
9506
  try {
9294
9507
  const res = await this.p2pUidLookupOne(
9295
9508
  sock,
@@ -9297,11 +9510,20 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9297
9510
  { host: ip, port: P2P_LOOKUP_PORT },
9298
9511
  Math.min(remaining, 3e3)
9299
9512
  );
9513
+ log(
9514
+ `${ip}:${P2P_LOOKUP_PORT} replied in ${Date.now() - probeStart}ms \u2713 (total ${Date.now() - t0}ms)`
9515
+ );
9300
9516
  return res;
9301
9517
  } catch (e) {
9518
+ const ms = Date.now() - probeStart;
9519
+ const msg = e?.message ?? String(e);
9520
+ log(`${ip}:${P2P_LOOKUP_PORT} no reply after ${ms}ms (${msg})`);
9302
9521
  lastErr = e instanceof Error ? e : new Error(String(e));
9303
9522
  }
9304
9523
  }
9524
+ log(
9525
+ `Exhausted all ${attemptsMade} relay candidate(s) after ${Date.now() - t0}ms \u2014 UID lookup failed`
9526
+ );
9305
9527
  throw lastErr ?? new Error("P2P UID lookup failed");
9306
9528
  }
9307
9529
  async p2pUidLookupOne(sock, uid, dest, timeoutMs) {
@@ -9640,13 +9862,23 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9640
9862
  const directHost = (this.opts.directHost ?? "").trim();
9641
9863
  const localMode = opts?.localMode ?? "local-broadcast";
9642
9864
  const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
9643
- const discoveryTimeout = 3e4;
9865
+ const discoveryTimeout = typeof this.opts.localDiscoveryTimeoutMs === "number" && this.opts.localDiscoveryTimeoutMs > 0 ? this.opts.localDiscoveryTimeoutMs : 15e3;
9644
9866
  const retryInterval = 500;
9645
9867
  const startMs = Date.now();
9646
9868
  sock.setBroadcast(true);
9647
9869
  const addr = sock.address();
9648
9870
  const localPort = typeof addr === "string" ? 0 : addr.port;
9649
9871
  const cid = Math.floor(Math.random() * 2147483647) | 0 || 82e3;
9872
+ const log = (msg) => this.discoveryLogger?.log?.(`[BCUDP] ${msg}`);
9873
+ const shortUid = this.opts.uid.length > 7 ? `${this.opts.uid.slice(0, 5)}\u2026${this.opts.uid.slice(-2)}` : this.opts.uid;
9874
+ log(
9875
+ `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
9876
+ );
9877
+ let bytesSent = 0;
9878
+ let pktsRecv = 0;
9879
+ sock.on("message", () => {
9880
+ pktsRecv++;
9881
+ });
9650
9882
  const xml = buildC2dC({
9651
9883
  uid: this.opts.uid,
9652
9884
  clientPort: localPort,
@@ -9657,6 +9889,9 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9657
9889
  const timeout = setTimeout(() => {
9658
9890
  if (retryTimer) clearInterval(retryTimer);
9659
9891
  sock.off("message", onMsg);
9892
+ log(
9893
+ `local discovery timeout after ${discoveryTimeout}ms \u2014 sent=${bytesSent}B replies=${pktsRecv} (camera likely sleeping / off-LAN / firewall dropping replies)`
9894
+ );
9660
9895
  reject(
9661
9896
  new Error(
9662
9897
  `BCUDP discovery timeout after ${discoveryTimeout}ms (camera may be sleeping or unreachable)`
@@ -9850,6 +10085,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9850
10085
  for (const port of ports) {
9851
10086
  try {
9852
10087
  sock.send(packet, port, host);
10088
+ bytesSent += packet.length;
9853
10089
  retryCount++;
9854
10090
  this.emit("debug", "discovery_send", { retryCount, host, port });
9855
10091
  } catch {
@@ -11389,6 +11625,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
11389
11625
  sock.on("debug", (event, data) => {
11390
11626
  this.logDebug(`udp_${event}`, data);
11391
11627
  });
11628
+ sock.setLogger(this.logger);
11392
11629
  await sock.connect();
11393
11630
  const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : "";
11394
11631
  const udpDiscoveryMethod = this.opts.udpDiscoveryMethod ?? "local-direct";
@@ -40496,7 +40733,11 @@ async function discoverUidForHost(host, logger) {
40496
40733
  function isTcpFailureThatShouldFallbackToUdp(e) {
40497
40734
  const message = e?.message || e?.toString?.() || "";
40498
40735
  if (typeof message !== "string") return false;
40499
- 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");
40736
+ 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
40737
+ // `withTcpDeadline` in `autoDetectDeviceType`. Without this entry the
40738
+ // catch block would rethrow the deadline error instead of awaiting
40739
+ // the speculative UDP race.
40740
+ 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");
40500
40741
  }
40501
40742
  async function pingHost(host, timeoutMs = 3e3) {
40502
40743
  if (!host || typeof host !== "string") return false;
@@ -40644,6 +40885,7 @@ function attachErrorHandler(api, transport, inputs) {
40644
40885
  }
40645
40886
  async function autoDetectDeviceType(inputs) {
40646
40887
  const { host, uid, logger } = inputs;
40888
+ const autodetectStartedAt = Date.now();
40647
40889
  const mode = inputs.mode ?? "auto";
40648
40890
  const maxRetriesRaw = inputs.maxRetries;
40649
40891
  const maxRetries = Math.max(
@@ -40660,9 +40902,31 @@ async function autoDetectDeviceType(inputs) {
40660
40902
  const sleepMs3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
40661
40903
  const shouldRetryTcp = (e) => {
40662
40904
  const msg = fmtErr(e);
40663
- if (msg.includes("ECONNREFUSED")) return false;
40905
+ if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTDOWN") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH") || msg.includes("ENETDOWN")) {
40906
+ return false;
40907
+ }
40664
40908
  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");
40665
40909
  };
40910
+ const tcpDeadlineMs = typeof inputs.tcpConnectTimeoutMs === "number" && Number.isFinite(inputs.tcpConnectTimeoutMs) && inputs.tcpConnectTimeoutMs > 0 ? inputs.tcpConnectTimeoutMs : 8e3;
40911
+ const withTcpDeadline = async (op) => {
40912
+ let timer;
40913
+ const deadline = new Promise((_, reject) => {
40914
+ timer = setTimeout(
40915
+ () => reject(
40916
+ new Error(
40917
+ `TCP login deadline exceeded (${tcpDeadlineMs}ms) \u2014 host unreachable`
40918
+ )
40919
+ ),
40920
+ tcpDeadlineMs
40921
+ );
40922
+ timer.unref?.();
40923
+ });
40924
+ try {
40925
+ return await Promise.race([op, deadline]);
40926
+ } finally {
40927
+ if (timer) clearTimeout(timer);
40928
+ }
40929
+ };
40666
40930
  const shouldRetryUdp = (e) => {
40667
40931
  const msg = fmtErr(e);
40668
40932
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
@@ -40815,6 +41079,127 @@ async function autoDetectDeviceType(inputs) {
40815
41079
  "Forced UDP autodetect failed for all methods."
40816
41080
  );
40817
41081
  }
41082
+ const detectOverUdpApi = async (udpApi, udpDiscoveryMethod, resolvedUid) => {
41083
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
41084
+ udpApi.getInfo(),
41085
+ udpApi.getDeviceCapabilities(),
41086
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
41087
+ ]);
41088
+ const channelNum = capabilities?.support?.channelNum ?? 1;
41089
+ const model = deviceInfo.type?.trim();
41090
+ const normalizedModel = model ? model.trim() : void 0;
41091
+ const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
41092
+ const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
41093
+ const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
41094
+ const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
41095
+ const hasBattery = capabilities?.capabilities?.hasBattery === true;
41096
+ udpApi.setIdleDisconnect(hasBattery);
41097
+ if (isMultifocal) {
41098
+ const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
41099
+ logger?.log?.(
41100
+ `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
41101
+ );
41102
+ return {
41103
+ type: "multifocal",
41104
+ transport: "udp",
41105
+ uid: resolvedUid,
41106
+ udpDiscoveryMethod,
41107
+ deviceInfo,
41108
+ ...hostNetworkInfo ? { hostNetworkInfo } : {},
41109
+ channelNum,
41110
+ hasBattery,
41111
+ api: udpApi
41112
+ };
41113
+ }
41114
+ const deviceType = hasBattery ? "battery-cam" : "udp-camera";
41115
+ logger?.log?.(
41116
+ `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
41117
+ );
41118
+ return {
41119
+ type: deviceType,
41120
+ transport: "udp",
41121
+ uid: resolvedUid,
41122
+ udpDiscoveryMethod,
41123
+ deviceInfo,
41124
+ ...hostNetworkInfo ? { hostNetworkInfo } : {},
41125
+ channelNum: 1,
41126
+ hasBattery,
41127
+ api: udpApi
41128
+ };
41129
+ };
41130
+ const udpRaceAbort = new AbortController();
41131
+ const speculativeUdpRace = mode === "auto" ? (async () => {
41132
+ const resolvedUid = await speculativeUidPromise;
41133
+ const viableMethods = selectViableUdpMethods(Boolean(resolvedUid));
41134
+ return await runUdpMethodsParallel(
41135
+ viableMethods,
41136
+ async (m, isInnerAborted) => {
41137
+ const isAborted = () => udpRaceAbort.signal.aborted || isInnerAborted();
41138
+ if (isAborted()) {
41139
+ throw new Error(
41140
+ `UDP(${m}) speculative race aborted before start`
41141
+ );
41142
+ }
41143
+ logger?.log?.(
41144
+ `[AutoDetect] (race) Trying UDP discovery method: ${m}...`
41145
+ );
41146
+ const udpApi = await withRetries(
41147
+ `UDP(${m})`,
41148
+ maxRetries,
41149
+ async (attempt) => {
41150
+ const apiInputs = {
41151
+ ...inputs,
41152
+ udpDiscoveryMethod: m
41153
+ };
41154
+ if (resolvedUid) apiInputs.uid = resolvedUid;
41155
+ const api = createBaichuanApi(apiInputs, "udp");
41156
+ try {
41157
+ await api.login();
41158
+ return api;
41159
+ } catch (e) {
41160
+ try {
41161
+ await api.close({
41162
+ reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
41163
+ });
41164
+ } catch {
41165
+ }
41166
+ throw e;
41167
+ }
41168
+ },
41169
+ shouldRetryUdp,
41170
+ isAborted
41171
+ );
41172
+ if (isAborted()) {
41173
+ try {
41174
+ await udpApi.close({
41175
+ reason: "autodetect:udp_aborted_after_tcp_won"
41176
+ });
41177
+ } catch {
41178
+ }
41179
+ throw new Error(
41180
+ `UDP(${m}) speculative race aborted after login`
41181
+ );
41182
+ }
41183
+ return detectOverUdpApi(udpApi, m, resolvedUid ?? "");
41184
+ },
41185
+ "Speculative UDP race failed for all methods."
41186
+ );
41187
+ })() : void 0;
41188
+ speculativeUdpRace?.then(
41189
+ (udpResult) => {
41190
+ if (udpRaceAbort.signal.aborted && udpResult?.api) {
41191
+ udpResult.api.close({ reason: "autodetect:tcp_won_race" }).catch(() => void 0);
41192
+ }
41193
+ },
41194
+ () => void 0
41195
+ );
41196
+ const _tcpWin = (result) => {
41197
+ udpRaceAbort.abort();
41198
+ logger?.log?.(
41199
+ `[AutoDetect] DONE in ${Date.now() - autodetectStartedAt}ms via TCP \u2014 type=${result.type} model=${result.deviceInfo?.type ?? "?"} channels=${result.channelNum}`
41200
+ );
41201
+ return result;
41202
+ };
40818
41203
  let tcpApi;
40819
41204
  try {
40820
41205
  logger?.log?.(`[AutoDetect] Trying TCP connection to ${host}...`);
@@ -40824,7 +41209,7 @@ async function autoDetectDeviceType(inputs) {
40824
41209
  async (attempt) => {
40825
41210
  const api2 = createBaichuanApi(inputs, "tcp");
40826
41211
  try {
40827
- await api2.login();
41212
+ await withTcpDeadline(api2.login());
40828
41213
  return api2;
40829
41214
  } catch (e) {
40830
41215
  try {
@@ -40936,7 +41321,7 @@ async function autoDetectDeviceType(inputs) {
40936
41321
  logger?.log?.(
40937
41322
  `[AutoDetect] Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum})`
40938
41323
  );
40939
- return {
41324
+ return _tcpWin({
40940
41325
  type: "multifocal",
40941
41326
  transport: "tcp",
40942
41327
  uid: effectiveUid || uid || "",
@@ -40944,13 +41329,13 @@ async function autoDetectDeviceType(inputs) {
40944
41329
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
40945
41330
  channelNum: effectiveChannelNum,
40946
41331
  api
40947
- };
41332
+ });
40948
41333
  }
40949
41334
  if (effectiveChannelNum > 1) {
40950
41335
  logger?.log?.(
40951
41336
  `[AutoDetect] Detected NVR (${effectiveChannelNum} channels)`
40952
41337
  );
40953
- return {
41338
+ return _tcpWin({
40954
41339
  type: "nvr",
40955
41340
  transport: "tcp",
40956
41341
  uid: effectiveUid || uid || "",
@@ -40958,10 +41343,10 @@ async function autoDetectDeviceType(inputs) {
40958
41343
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
40959
41344
  channelNum: effectiveChannelNum,
40960
41345
  api
40961
- };
41346
+ });
40962
41347
  }
40963
41348
  logger?.log?.(`[AutoDetect] Detected regular camera (single channel)`);
40964
- return {
41349
+ return _tcpWin({
40965
41350
  type: "camera",
40966
41351
  transport: "tcp",
40967
41352
  uid: effectiveUid || uid || "",
@@ -40969,7 +41354,7 @@ async function autoDetectDeviceType(inputs) {
40969
41354
  // ...(hostNetworkInfo ? { hostNetworkInfo } : {}),
40970
41355
  channelNum: 1,
40971
41356
  api
40972
- };
41357
+ });
40973
41358
  } catch (tcpError) {
40974
41359
  if (mode === "tcp") {
40975
41360
  throw tcpError;
@@ -40984,100 +41369,20 @@ async function autoDetectDeviceType(inputs) {
40984
41369
  throw tcpError;
40985
41370
  }
40986
41371
  logger?.log?.(`[AutoDetect] TCP failed, trying UDP...`);
40987
- let normalizedUid = await speculativeUidPromise;
40988
- if (!normalizedUid) {
40989
- logger?.log?.(
40990
- `[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.`
40991
- );
40992
- } else if (effectiveUid === void 0) {
40993
- logger?.log?.(
40994
- `[AutoDetect] UID resolved via concurrent broadcast discovery: ${normalizedUid}`
41372
+ if (!speculativeUdpRace) {
41373
+ throw new Error(
41374
+ `AutoDetect internal: speculative UDP race missing in mode=${mode}`
40995
41375
  );
40996
41376
  }
40997
41377
  try {
40998
- const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
40999
- const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
41000
- udpApi.getInfo(),
41001
- udpApi.getDeviceCapabilities(),
41002
- udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
41003
- ]);
41004
- const channelNum = capabilities?.support?.channelNum ?? 1;
41005
- const model = deviceInfo.type?.trim();
41006
- const normalizedModel = model ? model.trim() : void 0;
41007
- const isMultifocalByModel = normalizedModel ? isDualLenseModel(normalizedModel) : false;
41008
- const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
41009
- const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
41010
- const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
41011
- const hasBattery = capabilities?.capabilities?.hasBattery === true;
41012
- udpApi.setIdleDisconnect(hasBattery);
41013
- if (isMultifocal) {
41014
- const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
41015
- logger?.log?.(
41016
- `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
41017
- );
41018
- return {
41019
- type: "multifocal",
41020
- transport: "udp",
41021
- uid: normalizedUid ?? "",
41022
- udpDiscoveryMethod,
41023
- deviceInfo,
41024
- ...hostNetworkInfo ? { hostNetworkInfo } : {},
41025
- channelNum,
41026
- hasBattery,
41027
- api: udpApi
41028
- };
41029
- }
41030
- const deviceType = hasBattery ? "battery-cam" : "udp-camera";
41031
- logger?.log?.(
41032
- `[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
41033
- );
41034
- return {
41035
- type: deviceType,
41036
- transport: "udp",
41037
- uid: normalizedUid ?? "",
41038
- udpDiscoveryMethod,
41039
- deviceInfo,
41040
- ...hostNetworkInfo ? { hostNetworkInfo } : {},
41041
- channelNum: 1,
41042
- hasBattery,
41043
- api: udpApi
41044
- };
41045
- };
41046
- const viableMethods = selectViableUdpMethods(Boolean(normalizedUid));
41047
- return await runUdpMethodsParallel(
41048
- viableMethods,
41049
- async (m, isAborted) => {
41050
- logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
41051
- const udpApi = await withRetries(
41052
- `UDP(${m})`,
41053
- maxRetries,
41054
- async (attempt) => {
41055
- const apiInputs = { ...inputs, udpDiscoveryMethod: m };
41056
- if (normalizedUid) apiInputs.uid = normalizedUid;
41057
- const api = createBaichuanApi(apiInputs, "udp");
41058
- try {
41059
- await api.login();
41060
- return api;
41061
- } catch (e) {
41062
- try {
41063
- await api.close({
41064
- reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
41065
- });
41066
- } catch {
41067
- }
41068
- throw e;
41069
- }
41070
- },
41071
- shouldRetryUdp,
41072
- isAborted
41073
- );
41074
- return detectOverUdpApi(udpApi, m);
41075
- },
41076
- "UDP discovery failed for all methods."
41378
+ const udpResult = await speculativeUdpRace;
41379
+ logger?.log?.(
41380
+ `[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}`
41077
41381
  );
41382
+ return udpResult;
41078
41383
  } catch (udpError) {
41079
41384
  logger?.log?.(
41080
- `[AutoDetect] Both TCP and UDP failed. TCP error: ${tcpError}, UDP error: ${udpError}`
41385
+ `[AutoDetect] FAILED after ${Date.now() - autodetectStartedAt}ms \u2014 neither TCP nor UDP could reach the camera. TCP: ${tcpError?.message ?? tcpError}. UDP: ${udpError?.message ?? udpError}`
41081
41386
  );
41082
41387
  throw new Error(
41083
41388
  `Failed to connect via both TCP and UDP. TCP: ${tcpError?.message || tcpError}, UDP: ${udpError?.message || udpError}`
@@ -42927,6 +43232,8 @@ function buildInitialStatus(config) {
42927
43232
  BC_CMD_ID_COVER_STANDALONE_460,
42928
43233
  BC_CMD_ID_COVER_STANDALONE_461,
42929
43234
  BC_CMD_ID_COVER_STANDALONE_462,
43235
+ BC_CMD_ID_DEVICE_DETECT,
43236
+ BC_CMD_ID_DEVICE_DETECT_CANDIDATES,
42930
43237
  BC_CMD_ID_DING_DONG_CTRL,
42931
43238
  BC_CMD_ID_DING_DONG_OPT,
42932
43239
  BC_CMD_ID_FILE_INFO_LIST_CLOSE,