@apocaliss92/nodelink-js 0.1.18 → 0.1.20

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.
@@ -9,7 +9,7 @@ import {
9
9
  runMultifocalDiagnosticsConsecutively,
10
10
  sampleStreams,
11
11
  testChannelStreams
12
- } from "./chunk-ZE7D7LI4.js";
12
+ } from "./chunk-YPU7RAEY.js";
13
13
  export {
14
14
  collectCgiDiagnostics,
15
15
  collectMultifocalDiagnostics,
@@ -22,4 +22,4 @@ export {
22
22
  sampleStreams,
23
23
  testChannelStreams
24
24
  };
25
- //# sourceMappingURL=DiagnosticsTools-6WEMO4L4.js.map
25
+ //# sourceMappingURL=DiagnosticsTools-NUMCYEKQ.js.map
@@ -136,7 +136,7 @@ import {
136
136
  talkTraceLog,
137
137
  traceLog,
138
138
  xmlEscape
139
- } from "./chunk-ZE7D7LI4.js";
139
+ } from "./chunk-YPU7RAEY.js";
140
140
 
141
141
  // src/protocol/framing.ts
142
142
  function encodeHeader(h) {
@@ -8753,7 +8753,8 @@ var logDebugStreamBlock = (params) => {
8753
8753
  const bitRateText = getXmlText(raw, "bitRate") ?? getXmlText(raw, "BitRate");
8754
8754
  const videoEncTypeText = getXmlText(raw, "videoEncType") ?? getXmlText(raw, "VideoEncType");
8755
8755
  const audioText = getXmlText(raw, "audio") ?? getXmlText(raw, "Audio");
8756
- const enableText = getXmlText(raw, "enable") ?? getXmlText(raw, "Enable");
8756
+ const enableCheckRaw = raw.replace(/<smartH265[\s\S]*?<\/smartH265>/g, "");
8757
+ const enableText = getXmlText(enableCheckRaw, "enable") ?? getXmlText(enableCheckRaw, "Enable");
8757
8758
  const width = Number(widthText ?? "0");
8758
8759
  const height = Number(heightText ?? "0");
8759
8760
  const frameRate = Number(frameText ?? "0");
@@ -8777,7 +8778,8 @@ var buildStream = (params) => {
8777
8778
  const frameRate = Number(getXmlText(streamXml, "frame") ?? "0");
8778
8779
  const bitRate = Number(getXmlText(streamXml, "bitRate") ?? "0");
8779
8780
  const audio = Number(getXmlText(streamXml, "audio") ?? "0");
8780
- const enabled = getXmlText(streamXml, "enable");
8781
+ const enableXml = streamXml.replace(/<smartH265[\s\S]*?<\/smartH265>/g, "");
8782
+ const enabled = getXmlText(enableXml, "enable");
8781
8783
  const isEnabled = isEnabledFromText(enabled);
8782
8784
  if (!isEnabled || !isPlausibleStream({ width, height, frameRate, bitRate }))
8783
8785
  return void 0;
@@ -9454,14 +9456,16 @@ var DUAL_LENS_SINGLE_MOTION_MODELS = /* @__PURE__ */ new Set([
9454
9456
  "Reolink TrackMix",
9455
9457
  "Reolink TrackMix PoE",
9456
9458
  "Reolink TrackMix WiFi",
9457
- "RLC-81MA"
9459
+ "RLC-81MA",
9460
+ "TrackFlex Floodlight WiFi"
9458
9461
  ]);
9459
9462
  var DUAL_LENS_MODELS = /* @__PURE__ */ new Set([
9460
9463
  ...DUAL_LENS_DUAL_MOTION_MODELS,
9461
9464
  ...DUAL_LENS_SINGLE_MOTION_MODELS
9462
9465
  ]);
9463
9466
  var isDualLenseModel = (model) => {
9464
- return DUAL_LENS_MODELS.has(model) || model.toLowerCase().includes("trackmix");
9467
+ const lower = model.toLowerCase();
9468
+ return Array.from(DUAL_LENS_MODELS).some((m) => m.toLowerCase() === lower) || lower.includes("trackmix") || lower.includes("trackflex");
9465
9469
  };
9466
9470
  var NVR_HUB_EXACT_TYPES = ["NVR", "WIFI_NVR", "HOMEHUB"];
9467
9471
  var NVR_HUB_MODEL_PATTERNS = [
@@ -9545,6 +9549,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9545
9549
  * Set via setIsNvr() from the plugin or auto-detected via isNvrDevice().
9546
9550
  */
9547
9551
  _isNvr;
9552
+ /**
9553
+ * Cached multi-focal detection result.
9554
+ * - true = dual-lens camera (e.g., TrackMix, TrackFlex) with multiple channels on a single device
9555
+ * - false = single-lens camera
9556
+ * Multi-focal cameras reject concurrent streaming TCP connections (response_code 430),
9557
+ * so all channels must multiplex on the same streaming socket.
9558
+ */
9559
+ _isMultiFocal;
9548
9560
  /** Maximum dedicated sessions allowed before triggering a reboot (default: 7). */
9549
9561
  maxDedicatedSessionsBeforeReboot;
9550
9562
  sessionGuardRebootInFlight;
@@ -9575,6 +9587,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9575
9587
  simpleEventResubscribeTimer;
9576
9588
  simpleEventResubscribeInFlight;
9577
9589
  simpleEventResubscribeIntervalMs = 5 * 6e4;
9590
+ // Event watchdog: auto-recovery when events stop flowing or subscription fails
9591
+ simpleEventWatchdogTimer;
9592
+ simpleEventLastReceivedAt = 0;
9593
+ simpleEventWatchdogRecoveryAttempts = 0;
9594
+ simpleEventWatchdogLastRecoveryAt = 0;
9595
+ simpleEventWatchdogIntervalMs = 1e4;
9596
+ // check every 10s
9597
+ simpleEventWatchdogSilenceThresholdMs = 5 * 6e4;
9598
+ // 5 min without events
9578
9599
  statePollingInterval;
9579
9600
  udpSleepInferenceInterval;
9580
9601
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
@@ -9959,14 +9980,16 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9959
9980
  * Determine the socket tag for a given sessionKey.
9960
9981
  * This implements the tag-based allocation strategy:
9961
9982
  *
9962
- * - "general" - commands, events
9963
- * - "streaming:ch{N}" - main + sub for channel N (closed when no streams)
9964
- * - "streaming:ch{N}:ext" - ext for channel N (closed when no streams)
9983
+ * - "general" - commands, events, ext on ch0
9984
+ * - "streaming:ch{N}" - main + sub for channel N (NVR/standalone single-lens)
9985
+ * - "streaming:ch{N}:ext" - ext for channel N (NVR/standalone, N>0)
9986
+ * - "streaming:a" - ch0 main + ch1 sub (multi-focal dedicated socket)
9987
+ * - "general" also carries ch1 main + ch0 sub for multi-focal (merged with commands/events)
9965
9988
  * - "replay:deviceId:ch{N}" - dedicated per device+channel for replay
9966
9989
  *
9967
- * Always uses per-channel tagging for streams (works for both standalone and NVR).
9968
- * Replay uses per-device+channel sockets to allow multiple users to watch
9969
- * different clips simultaneously without interfering with each other.
9990
+ * Multi-focal cameras (TrackMix, TrackFlex, Duo) reject two main or two sub
9991
+ * streams on the same TCP connection (response_code 430). Cross-channel pairing
9992
+ * ensures each socket always has a valid M+S combination using only 2 TCP connections.
9970
9993
  *
9971
9994
  * @param sessionKey - The session key (e.g., "live:device:ch0:main", "replay:device:ch1:file")
9972
9995
  * @returns The socket pool tag to use
@@ -9986,9 +10009,16 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9986
10009
  if (channel === 0) {
9987
10010
  return "general";
9988
10011
  }
9989
- return `streaming:ch${channel}:ext`;
10012
+ if (this._isMultiFocal) {
10013
+ return "general";
10014
+ }
10015
+ return this._isNvr ? `streaming:ch${channel}:ext` : `streaming:ch${channel}:ext`;
10016
+ }
10017
+ if (this._isMultiFocal) {
10018
+ const isSocketA = channel === 0 && profile === "main" || channel === 1 && profile === "sub";
10019
+ return isSocketA ? "streaming:a" : "general";
9990
10020
  }
9991
- return `streaming:ch${channel}`;
10021
+ return this._isNvr ? `streaming:ch${channel}` : `streaming:ch${channel}`;
9992
10022
  }
9993
10023
  return "general";
9994
10024
  }
@@ -10313,6 +10343,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10313
10343
  async cleanupDedicatedClients() {
10314
10344
  }
10315
10345
  dispatchSimpleEvent(evt) {
10346
+ this.simpleEventLastReceivedAt = Date.now();
10347
+ if (this.simpleEventWatchdogRecoveryAttempts > 0) {
10348
+ (this.logger.info ?? this.logger.log).call(
10349
+ this.logger,
10350
+ `[ReolinkBaichuanApi] event watchdog: events flowing again after ${this.simpleEventWatchdogRecoveryAttempts} recovery attempt(s)`
10351
+ );
10352
+ this.simpleEventWatchdogRecoveryAttempts = 0;
10353
+ }
10316
10354
  const debugCfg = this.client.getDebugConfig?.();
10317
10355
  if (debugCfg) {
10318
10356
  const sid = this.client.getSocketSessionId?.();
@@ -10861,11 +10899,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10861
10899
  /**
10862
10900
  * Subscribe to minimal high-level events.
10863
10901
  * The API manages Baichuan subscribe/unsubscribe automatically.
10902
+ * Includes built-in watchdog: if no events arrive for 5 minutes while
10903
+ * the connection is alive, the subscription is automatically renewed.
10864
10904
  */
10865
10905
  async onSimpleEvent(callback) {
10866
10906
  this.simpleEventListeners.add(callback);
10867
10907
  await this.ensureSimpleEventSubscribed();
10908
+ this.simpleEventLastReceivedAt = Date.now();
10868
10909
  this.startSimpleEventResubscribeTimer();
10910
+ this.startSimpleEventWatchdog();
10869
10911
  }
10870
10912
  /**
10871
10913
  * Remove one callback, or all callbacks if omitted.
@@ -10879,6 +10921,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10879
10921
  }
10880
10922
  if (this.simpleEventListeners.size === 0) {
10881
10923
  this.stopSimpleEventResubscribeTimer();
10924
+ this.stopSimpleEventWatchdog();
10882
10925
  this.stopUdpSleepInference();
10883
10926
  await this.ensureSimpleEventUnsubscribed();
10884
10927
  } else {
@@ -10902,6 +10945,97 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10902
10945
  clearInterval(this.simpleEventResubscribeTimer);
10903
10946
  this.simpleEventResubscribeTimer = void 0;
10904
10947
  }
10948
+ /**
10949
+ * Event watchdog: monitors whether events are flowing and auto-recovers if they stop.
10950
+ *
10951
+ * Handles two failure modes:
10952
+ * 1. Subscription flag is true but no events arrive for 5+ minutes (device dropped subscription silently)
10953
+ * 2. Subscription flag is false because initial/retry subscribe failed, but connection is now alive
10954
+ *
10955
+ * Uses exponential backoff (30s → 60s → 120s → 240s → max 5min) to avoid hammering the device.
10956
+ */
10957
+ startSimpleEventWatchdog() {
10958
+ if (this.simpleEventWatchdogTimer) return;
10959
+ if (this.simpleEventListeners.size === 0) return;
10960
+ this.simpleEventWatchdogTimer = setInterval(() => {
10961
+ void this.simpleEventWatchdogTick();
10962
+ }, this.simpleEventWatchdogIntervalMs);
10963
+ }
10964
+ stopSimpleEventWatchdog() {
10965
+ if (!this.simpleEventWatchdogTimer) return;
10966
+ clearInterval(this.simpleEventWatchdogTimer);
10967
+ this.simpleEventWatchdogTimer = void 0;
10968
+ this.simpleEventWatchdogRecoveryAttempts = 0;
10969
+ this.simpleEventWatchdogLastRecoveryAt = 0;
10970
+ this.simpleEventLastReceivedAt = 0;
10971
+ }
10972
+ async simpleEventWatchdogTick() {
10973
+ if (this.simpleEventListeners.size === 0) return;
10974
+ if (!this.client.isSocketConnected?.() || !this.client.loggedIn) return;
10975
+ const now = Date.now();
10976
+ if (this.simpleEventSubscribed && this.simpleEventLastReceivedAt > 0) {
10977
+ const silence = now - this.simpleEventLastReceivedAt;
10978
+ if (silence < this.simpleEventWatchdogSilenceThresholdMs) return;
10979
+ (this.logger.warn ?? this.logger.log).call(
10980
+ this.logger,
10981
+ `[ReolinkBaichuanApi] event watchdog: no events for ${Math.round(silence / 6e4)} min, forcing resubscribe`,
10982
+ { host: this.host, silenceMs: silence }
10983
+ );
10984
+ try {
10985
+ this.simpleEventSubscribed = false;
10986
+ this.client.subscribed = false;
10987
+ await this.ensureSimpleEventSubscribed();
10988
+ this.simpleEventLastReceivedAt = Date.now();
10989
+ this.simpleEventWatchdogRecoveryAttempts = 0;
10990
+ (this.logger.info ?? this.logger.log).call(
10991
+ this.logger,
10992
+ `[ReolinkBaichuanApi] event watchdog: resubscribed successfully after silence`
10993
+ );
10994
+ } catch (e) {
10995
+ (this.logger.debug ?? this.logger.log).call(
10996
+ this.logger,
10997
+ `[ReolinkBaichuanApi] event watchdog: resubscribe after silence failed`,
10998
+ formatErrorForLog(e)
10999
+ );
11000
+ }
11001
+ return;
11002
+ }
11003
+ if (!this.simpleEventSubscribed) {
11004
+ const backoffMs = Math.min(
11005
+ 3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
11006
+ this.simpleEventWatchdogSilenceThresholdMs
11007
+ );
11008
+ if (now - this.simpleEventWatchdogLastRecoveryAt < backoffMs) return;
11009
+ this.simpleEventWatchdogRecoveryAttempts++;
11010
+ this.simpleEventWatchdogLastRecoveryAt = now;
11011
+ const nextBackoff = Math.min(
11012
+ 3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
11013
+ this.simpleEventWatchdogSilenceThresholdMs
11014
+ );
11015
+ (this.logger.info ?? this.logger.log).call(
11016
+ this.logger,
11017
+ `[ReolinkBaichuanApi] event watchdog: subscription inactive, attempting auto-recovery (attempt #${this.simpleEventWatchdogRecoveryAttempts}, next backoff ${Math.round(nextBackoff / 1e3)}s)`,
11018
+ { host: this.host }
11019
+ );
11020
+ try {
11021
+ await this.ensureSimpleEventSubscribed();
11022
+ if (this.simpleEventSubscribed) {
11023
+ this.simpleEventLastReceivedAt = Date.now();
11024
+ (this.logger.info ?? this.logger.log).call(
11025
+ this.logger,
11026
+ `[ReolinkBaichuanApi] event watchdog: auto-recovery successful after ${this.simpleEventWatchdogRecoveryAttempts} attempt(s)`
11027
+ );
11028
+ this.simpleEventWatchdogRecoveryAttempts = 0;
11029
+ }
11030
+ } catch (e) {
11031
+ (this.logger.debug ?? this.logger.log).call(
11032
+ this.logger,
11033
+ `[ReolinkBaichuanApi] event watchdog: recovery attempt #${this.simpleEventWatchdogRecoveryAttempts} failed`,
11034
+ formatErrorForLog(e)
11035
+ );
11036
+ }
11037
+ }
11038
+ }
10905
11039
  async renewSimpleEventSubscription() {
10906
11040
  if (this.simpleEventListeners.size === 0) return;
10907
11041
  if (this.simpleEventResubscribeInFlight)
@@ -11040,6 +11174,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11040
11174
  this._isNvr = isNvr;
11041
11175
  this.logger.debug?.(`[ReolinkBaichuanApi] setIsNvr: ${isNvr}`);
11042
11176
  }
11177
+ /**
11178
+ * Set the multi-focal flag explicitly.
11179
+ * Call this early (before streaming) to ensure correct socket pooling.
11180
+ * Multi-focal cameras (TrackMix, TrackFlex, Duo, etc.) reject concurrent
11181
+ * streaming TCP connections, so all channels must share a single streaming socket.
11182
+ * @param isMultiFocal - true if this is a dual-lens/multi-focal camera
11183
+ */
11184
+ setIsMultiFocal(isMultiFocal) {
11185
+ this._isMultiFocal = isMultiFocal;
11186
+ this.logger.debug?.(`[ReolinkBaichuanApi] setIsMultiFocal: ${isMultiFocal}`);
11187
+ }
11043
11188
  /**
11044
11189
  * Enable or disable idle disconnect dynamically.
11045
11190
  *
@@ -11090,6 +11235,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11090
11235
  }
11091
11236
  this.stopStatePolling();
11092
11237
  this.stopUdpSleepInference();
11238
+ this.stopSimpleEventWatchdog();
11239
+ this.stopSimpleEventResubscribeTimer();
11093
11240
  await this.cleanup();
11094
11241
  await this.stopAllActiveStreams();
11095
11242
  await this.cleanupSocketPool();
@@ -16541,7 +16688,7 @@ ${xml}`
16541
16688
  });
16542
16689
  model = typeof info.type === "string" ? info.type.toLowerCase() : "";
16543
16690
  isMultiFocal = isDualLenseModel(model);
16544
- isTrackMix = model.includes("trackmix");
16691
+ isTrackMix = model.includes("trackmix") || model.includes("trackflex");
16545
16692
  } catch (e) {
16546
16693
  logDebug(
16547
16694
  "[ReolinkBaichuanApi] buildVideoStreamOptions: getInfo(type) failed",
@@ -16946,7 +17093,7 @@ ${xml}`
16946
17093
  * @returns Test results for all stream types and profiles
16947
17094
  */
16948
17095
  async testChannelStreams(channel, logger) {
16949
- const { testChannelStreams } = await import("./DiagnosticsTools-6WEMO4L4.js");
17096
+ const { testChannelStreams } = await import("./DiagnosticsTools-NUMCYEKQ.js");
16950
17097
  return await testChannelStreams({
16951
17098
  api: this,
16952
17099
  channel: this.normalizeChannel(channel),
@@ -16962,7 +17109,7 @@ ${xml}`
16962
17109
  * @returns Complete diagnostics for all channels and streams
16963
17110
  */
16964
17111
  async collectMultifocalDiagnostics(logger) {
16965
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-6WEMO4L4.js");
17112
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-NUMCYEKQ.js");
16966
17113
  return await collectMultifocalDiagnostics({
16967
17114
  api: this,
16968
17115
  logger
@@ -20065,4 +20212,4 @@ export {
20065
20212
  isTcpFailureThatShouldFallbackToUdp,
20066
20213
  autoDetectDeviceType
20067
20214
  };
20068
- //# sourceMappingURL=chunk-ULSFEQSE.js.map
20215
+ //# sourceMappingURL=chunk-EHWVA3SG.js.map