@apocaliss92/nodelink-js 0.4.6 → 0.4.7

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.
@@ -3,7 +3,7 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-F2Y5U3YP.js";
6
+ } from "../chunk-GKLOJJ34.js";
7
7
  import {
8
8
  __require
9
9
  } from "../chunk-TR3V5FTO.js";
package/dist/index.cjs CHANGED
@@ -18072,6 +18072,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18072
18072
  * - "replay:XXX" - dedicated per replay session
18073
18073
  */
18074
18074
  socketPool = /* @__PURE__ */ new Map();
18075
+ /**
18076
+ * Consecutive stream-start (cmdId=3) timeout counter per socket tag.
18077
+ * When a streaming socket has N consecutive timeouts, the socket is force-closed
18078
+ * so the next attempt creates a fresh connection. Resets on success.
18079
+ */
18080
+ consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
18081
+ static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
18075
18082
  /** BaichuanClientOptions to use when creating new sockets */
18076
18083
  clientOptions;
18077
18084
  /**
@@ -18226,14 +18233,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18226
18233
  if (!xml) return;
18227
18234
  const channel = frame.header.channelId;
18228
18235
  const battery = this.parseBatteryInfoXml(xml, channel);
18229
- if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
18230
- this.dispatchSimpleEvent({
18231
- type: "battery",
18232
- channel,
18233
- timestamp: Date.now(),
18234
- battery
18235
- });
18236
+ if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
18237
+ return;
18238
+ }
18239
+ const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
18240
+ if (this.lastBatteryPushKey.get(channel) === key) {
18241
+ return;
18236
18242
  }
18243
+ this.lastBatteryPushKey.set(channel, key);
18244
+ this.dispatchSimpleEvent({
18245
+ type: "battery",
18246
+ channel,
18247
+ timestamp: Date.now(),
18248
+ battery
18249
+ });
18237
18250
  } catch (e) {
18238
18251
  this.logger.debug?.(
18239
18252
  "[ReolinkBaichuanApi] Error parsing battery push",
@@ -18399,6 +18412,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18399
18412
  deviceCapabilitiesCache = /* @__PURE__ */ new Map();
18400
18413
  static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
18401
18414
  // 5 minutes
18415
+ /**
18416
+ * Dedupe key for battery push events (cmd_id 252), per channel.
18417
+ * Cameras emit BatteryInfoList frequently while streaming (every few
18418
+ * seconds). We only forward an event when the meaningful fields change
18419
+ * (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
18420
+ * consumers and the UI event log.
18421
+ */
18422
+ lastBatteryPushKey = /* @__PURE__ */ new Map();
18402
18423
  // ─────────────────────────────────────────────────────────────────────────────
18403
18424
  // SOCKET POOL CONSTANTS
18404
18425
  // ─────────────────────────────────────────────────────────────────────────────
@@ -23871,6 +23892,7 @@ ${stderr}`)
23871
23892
  `${ch}:${profile}:${variant}`,
23872
23893
  frame.header.msgNum
23873
23894
  );
23895
+ this.resetStreamTimeoutCounter(targetClient);
23874
23896
  return;
23875
23897
  } catch (error) {
23876
23898
  lastError = error;
@@ -23885,6 +23907,10 @@ ${stderr}`)
23885
23907
  }
23886
23908
  }
23887
23909
  }
23910
+ const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
23911
+ if (isTimeout) {
23912
+ this.trackStreamTimeout(targetClient);
23913
+ }
23888
23914
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
23889
23915
  }
23890
23916
  /**
@@ -24344,6 +24370,18 @@ ${stderr}`)
24344
24370
  notifyD2cDisc() {
24345
24371
  const now = Date.now();
24346
24372
  this.lastD2cDiscAtMs = now;
24373
+ const streamingTags = Array.from(this.socketPool.keys()).filter(
24374
+ (tag) => tag.startsWith("streaming:")
24375
+ );
24376
+ if (streamingTags.length > 0) {
24377
+ this.logger?.log?.(
24378
+ `[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
24379
+ );
24380
+ for (const tag of streamingTags) {
24381
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
24382
+ });
24383
+ }
24384
+ }
24347
24385
  const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
24348
24386
  const existing = this.socketPoolCooldowns.get(this.host);
24349
24387
  if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
@@ -24376,6 +24414,43 @@ ${stderr}`)
24376
24414
  }
24377
24415
  }
24378
24416
  }
24417
+ /**
24418
+ * Find the socket pool tag for a given BaichuanClient instance.
24419
+ * Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
24420
+ */
24421
+ findSocketTagForClient(client) {
24422
+ for (const [tag, entry] of this.socketPool) {
24423
+ if (entry.client === client) return tag;
24424
+ }
24425
+ return void 0;
24426
+ }
24427
+ /**
24428
+ * Reset the consecutive stream-start timeout counter for a streaming socket.
24429
+ * Called on successful stream start.
24430
+ */
24431
+ resetStreamTimeoutCounter(client) {
24432
+ const tag = this.findSocketTagForClient(client);
24433
+ if (tag) this.consecutiveStreamTimeouts.delete(tag);
24434
+ }
24435
+ /**
24436
+ * Track a stream-start timeout on a streaming socket.
24437
+ * After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
24438
+ * socket so the next attempt creates a fresh connection.
24439
+ */
24440
+ trackStreamTimeout(client) {
24441
+ const tag = this.findSocketTagForClient(client);
24442
+ if (!tag || !tag.startsWith("streaming:")) return;
24443
+ const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
24444
+ this.consecutiveStreamTimeouts.set(tag, count);
24445
+ if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
24446
+ this.logger?.warn?.(
24447
+ `[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
24448
+ );
24449
+ this.consecutiveStreamTimeouts.delete(tag);
24450
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
24451
+ });
24452
+ }
24453
+ }
24379
24454
  /**
24380
24455
  * Best-effort sleeping inference for battery/BCUDP cameras.
24381
24456
  *
@@ -33640,6 +33715,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33640
33715
  gracePeriodMs;
33641
33716
  prebufferMaxMs;
33642
33717
  maxBufferBytes;
33718
+ streamTimeoutMs;
33643
33719
  prestartStream;
33644
33720
  active = false;
33645
33721
  server;
@@ -33653,6 +33729,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33653
33729
  connectedClients = /* @__PURE__ */ new Set();
33654
33730
  clientSockets = /* @__PURE__ */ new Map();
33655
33731
  stopGraceTimer;
33732
+ // Stream health monitoring
33733
+ lastFrameAt = 0;
33734
+ streamHealthTimer;
33735
+ totalFramesReceived = 0;
33736
+ totalVideoFramesWritten = 0;
33656
33737
  // Prebuffer
33657
33738
  prebuffer = [];
33658
33739
  constructor(options) {
@@ -33668,6 +33749,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33668
33749
  this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
33669
33750
  this.prebufferMaxMs = options.prebufferMs ?? 3e3;
33670
33751
  this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
33752
+ this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
33671
33753
  this.prestartStream = options.prestartStream ?? true;
33672
33754
  }
33673
33755
  // -----------------------------------------------------------------------
@@ -33706,6 +33788,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33706
33788
  if (!this.active) return;
33707
33789
  this.active = false;
33708
33790
  clearTimeout(this.stopGraceTimer);
33791
+ this.stopStreamHealthMonitor();
33709
33792
  for (const [id, sock] of this.clientSockets) {
33710
33793
  sock.destroy();
33711
33794
  this.connectedClients.delete(id);
@@ -33759,12 +33842,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33759
33842
  `[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
33760
33843
  );
33761
33844
  });
33762
- const cleanup = () => {
33763
- this.removeClient(clientId);
33845
+ const cleanup = (reason) => {
33846
+ this.removeClient(clientId, reason);
33764
33847
  socket.destroy();
33765
33848
  };
33766
- socket.on("error", cleanup);
33767
- socket.on("close", cleanup);
33849
+ socket.on("error", (err) => cleanup(`error: ${err.message}`));
33850
+ socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
33768
33851
  }
33769
33852
  async feedClient(clientId, socket) {
33770
33853
  const fanoutDeadline = Date.now() + 3e4;
@@ -33825,6 +33908,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33825
33908
  }
33826
33909
  socket.write(annexB);
33827
33910
  liveVideoWritten++;
33911
+ this.totalVideoFramesWritten++;
33828
33912
  if (Date.now() - lastLogAt > 1e4) {
33829
33913
  this.logger.info?.(
33830
33914
  `[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
@@ -33951,6 +34035,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33951
34035
  ...dedicatedClient ? { client: dedicatedClient } : {}
33952
34036
  }),
33953
34037
  onFrame: (frame) => {
34038
+ this.lastFrameAt = Date.now();
34039
+ this.totalFramesReceived++;
33954
34040
  if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
33955
34041
  this.detectedVideoType = frame.videoType;
33956
34042
  }
@@ -33977,6 +34063,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33977
34063
  if (!this.nativeStreamActive) return;
33978
34064
  this.nativeStreamActive = false;
33979
34065
  this.nativeFanout = null;
34066
+ this.stopStreamHealthMonitor();
34067
+ const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
34068
+ const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
34069
+ this.logger.warn?.(
34070
+ `[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
34071
+ );
33980
34072
  if (this.dedicatedSessionRelease) {
33981
34073
  this.dedicatedSessionRelease().catch(() => {
33982
34074
  });
@@ -33984,16 +34076,18 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33984
34076
  }
33985
34077
  if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
33986
34078
  this.logger.info?.(
33987
- `[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
34079
+ `[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
33988
34080
  );
33989
34081
  this.startNativeStream();
33990
34082
  }
33991
34083
  }
33992
34084
  });
33993
34085
  this.nativeFanout.start();
34086
+ this.startStreamHealthMonitor();
33994
34087
  }
33995
34088
  async stopNativeStream() {
33996
34089
  this.nativeStreamActive = false;
34090
+ this.stopStreamHealthMonitor();
33997
34091
  const fanout = this.nativeFanout;
33998
34092
  this.nativeFanout = null;
33999
34093
  if (fanout) {
@@ -34007,14 +34101,50 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34007
34101
  }
34008
34102
  }
34009
34103
  // -----------------------------------------------------------------------
34104
+ // Stream health monitoring
34105
+ // -----------------------------------------------------------------------
34106
+ startStreamHealthMonitor() {
34107
+ this.stopStreamHealthMonitor();
34108
+ if (this.streamTimeoutMs <= 0) return;
34109
+ this.lastFrameAt = Date.now();
34110
+ this.streamHealthTimer = setInterval(() => {
34111
+ if (!this.nativeStreamActive || !this.active) {
34112
+ this.stopStreamHealthMonitor();
34113
+ return;
34114
+ }
34115
+ const silenceMs = Date.now() - this.lastFrameAt;
34116
+ if (silenceMs > this.streamTimeoutMs) {
34117
+ this.logger.warn?.(
34118
+ `[Go2rtcTcpServer] stream inactivity timeout: no frames for ${(silenceMs / 1e3).toFixed(1)}s (threshold=${this.streamTimeoutMs}ms), totalReceived=${this.totalFramesReceived} clients=${this.connectedClients.size} \u2014 forcing stream restart`
34119
+ );
34120
+ this.stopStreamHealthMonitor();
34121
+ const fanout = this.nativeFanout;
34122
+ if (fanout) {
34123
+ this.nativeStreamActive = false;
34124
+ this.nativeFanout = null;
34125
+ fanout.stop().catch(() => {
34126
+ });
34127
+ }
34128
+ }
34129
+ }, Math.min(this.streamTimeoutMs / 2, 5e3));
34130
+ }
34131
+ stopStreamHealthMonitor() {
34132
+ if (this.streamHealthTimer) {
34133
+ clearInterval(this.streamHealthTimer);
34134
+ this.streamHealthTimer = void 0;
34135
+ }
34136
+ }
34137
+ // -----------------------------------------------------------------------
34010
34138
  // Client lifecycle
34011
34139
  // -----------------------------------------------------------------------
34012
- removeClient(clientId) {
34140
+ removeClient(clientId, reason) {
34013
34141
  if (!this.connectedClients.has(clientId)) return;
34014
34142
  this.connectedClients.delete(clientId);
34015
34143
  this.clientSockets.delete(clientId);
34144
+ const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
34145
+ const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
34016
34146
  this.logger.info?.(
34017
- `[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
34147
+ `[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
34018
34148
  );
34019
34149
  this.emit("clientDisconnected", clientId);
34020
34150
  if (this.connectedClients.size === 0 && !this.prestartStream) {
@@ -36349,16 +36479,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
36349
36479
  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");
36350
36480
  }
36351
36481
  async function pingHost(host, timeoutMs = 3e3) {
36482
+ const { exec } = await import("child_process");
36483
+ const platform2 = process.platform;
36484
+ const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
36485
+ // macOS: -W is in milliseconds (Linux: seconds)
36486
+ `ping -c 1 -W ${timeoutMs} ${host}`
36487
+ ) : (
36488
+ // Linux/BSD-ish: -W is in seconds on most distros
36489
+ `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
36490
+ );
36352
36491
  return new Promise((resolve) => {
36353
- const { exec } = require("child_process");
36354
- const platform2 = process.platform;
36355
- const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
36356
- // macOS: -W is in milliseconds (Linux: seconds)
36357
- `ping -c 1 -W ${timeoutMs} ${host}`
36358
- ) : (
36359
- // Linux/BSD-ish: -W is in seconds on most distros
36360
- `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
36361
- );
36362
36492
  exec(pingCmd, (error) => {
36363
36493
  resolve(!error);
36364
36494
  });