@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.
package/dist/index.d.cts CHANGED
@@ -4239,6 +4239,13 @@ declare class ReolinkBaichuanApi {
4239
4239
  * - "replay:XXX" - dedicated per replay session
4240
4240
  */
4241
4241
  private readonly socketPool;
4242
+ /**
4243
+ * Consecutive stream-start (cmdId=3) timeout counter per socket tag.
4244
+ * When a streaming socket has N consecutive timeouts, the socket is force-closed
4245
+ * so the next attempt creates a fresh connection. Resets on success.
4246
+ */
4247
+ private readonly consecutiveStreamTimeouts;
4248
+ private static readonly MAX_CONSECUTIVE_STREAM_TIMEOUTS;
4242
4249
  /** BaichuanClientOptions to use when creating new sockets */
4243
4250
  private readonly clientOptions;
4244
4251
  /**
@@ -4383,6 +4390,14 @@ declare class ReolinkBaichuanApi {
4383
4390
  */
4384
4391
  private readonly deviceCapabilitiesCache;
4385
4392
  private static readonly CAPABILITIES_CACHE_TTL_MS;
4393
+ /**
4394
+ * Dedupe key for battery push events (cmd_id 252), per channel.
4395
+ * Cameras emit BatteryInfoList frequently while streaming (every few
4396
+ * seconds). We only forward an event when the meaningful fields change
4397
+ * (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
4398
+ * consumers and the UI event log.
4399
+ */
4400
+ private readonly lastBatteryPushKey;
4386
4401
  /** Keep replay/streaming sockets warm briefly to reduce clip switch latency. */
4387
4402
  private static readonly SOCKET_POOL_KEEPALIVE_MS;
4388
4403
  /**
@@ -5722,6 +5737,22 @@ declare class ReolinkBaichuanApi {
5722
5737
  * 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
5723
5738
  */
5724
5739
  private notifyD2cDisc;
5740
+ /**
5741
+ * Find the socket pool tag for a given BaichuanClient instance.
5742
+ * Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
5743
+ */
5744
+ private findSocketTagForClient;
5745
+ /**
5746
+ * Reset the consecutive stream-start timeout counter for a streaming socket.
5747
+ * Called on successful stream start.
5748
+ */
5749
+ private resetStreamTimeoutCounter;
5750
+ /**
5751
+ * Track a stream-start timeout on a streaming socket.
5752
+ * After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
5753
+ * socket so the next attempt creates a fresh connection.
5754
+ */
5755
+ private trackStreamTimeout;
5725
5756
  /**
5726
5757
  * Best-effort sleeping inference for battery/BCUDP cameras.
5727
5758
  *
@@ -8046,6 +8077,13 @@ interface Go2rtcTcpServerOptions {
8046
8077
  prebufferMs?: number;
8047
8078
  /** Maximum write buffer per client before dropping the connection (default: 100 MB). */
8048
8079
  maxBufferBytes?: number;
8080
+ /**
8081
+ * Stream inactivity timeout in ms. If no frames arrive from the native
8082
+ * stream for this duration, the stream is considered dead and will be
8083
+ * torn down + restarted (similar to go2rtc's per-packet read deadline).
8084
+ * Default: 15 000 (15s). Set to 0 to disable.
8085
+ */
8086
+ streamTimeoutMs?: number;
8049
8087
  /**
8050
8088
  * When true, the native camera stream is started immediately on start()
8051
8089
  * rather than waiting for the first TCP client. This ensures frames are
@@ -8075,6 +8113,7 @@ declare class Go2rtcTcpServer extends EventEmitter<{
8075
8113
  private readonly gracePeriodMs;
8076
8114
  private readonly prebufferMaxMs;
8077
8115
  private readonly maxBufferBytes;
8116
+ private readonly streamTimeoutMs;
8078
8117
  private readonly prestartStream;
8079
8118
  private active;
8080
8119
  private server;
@@ -8086,6 +8125,10 @@ declare class Go2rtcTcpServer extends EventEmitter<{
8086
8125
  private connectedClients;
8087
8126
  private clientSockets;
8088
8127
  private stopGraceTimer;
8128
+ private lastFrameAt;
8129
+ private streamHealthTimer;
8130
+ private totalFramesReceived;
8131
+ private totalVideoFramesWritten;
8089
8132
  private prebuffer;
8090
8133
  constructor(options: Go2rtcTcpServerOptions);
8091
8134
  /** Start listening. Resolves once the TCP server is bound. */
@@ -8112,6 +8155,8 @@ declare class Go2rtcTcpServer extends EventEmitter<{
8112
8155
  private static splitAnnexBNals;
8113
8156
  private startNativeStream;
8114
8157
  private stopNativeStream;
8158
+ private startStreamHealthMonitor;
8159
+ private stopStreamHealthMonitor;
8115
8160
  private removeClient;
8116
8161
  private scheduleStop;
8117
8162
  }
package/dist/index.d.ts CHANGED
@@ -4200,6 +4200,7 @@ export declare class Go2rtcTcpServer extends EventEmitter<{
4200
4200
  private readonly gracePeriodMs;
4201
4201
  private readonly prebufferMaxMs;
4202
4202
  private readonly maxBufferBytes;
4203
+ private readonly streamTimeoutMs;
4203
4204
  private readonly prestartStream;
4204
4205
  private active;
4205
4206
  private server;
@@ -4211,6 +4212,10 @@ export declare class Go2rtcTcpServer extends EventEmitter<{
4211
4212
  private connectedClients;
4212
4213
  private clientSockets;
4213
4214
  private stopGraceTimer;
4215
+ private lastFrameAt;
4216
+ private streamHealthTimer;
4217
+ private totalFramesReceived;
4218
+ private totalVideoFramesWritten;
4214
4219
  private prebuffer;
4215
4220
  constructor(options: Go2rtcTcpServerOptions);
4216
4221
  /** Start listening. Resolves once the TCP server is bound. */
@@ -4237,6 +4242,8 @@ export declare class Go2rtcTcpServer extends EventEmitter<{
4237
4242
  private static splitAnnexBNals;
4238
4243
  private startNativeStream;
4239
4244
  private stopNativeStream;
4245
+ private startStreamHealthMonitor;
4246
+ private stopStreamHealthMonitor;
4240
4247
  private removeClient;
4241
4248
  private scheduleStop;
4242
4249
  }
@@ -4268,6 +4275,13 @@ export declare interface Go2rtcTcpServerOptions {
4268
4275
  prebufferMs?: number;
4269
4276
  /** Maximum write buffer per client before dropping the connection (default: 100 MB). */
4270
4277
  maxBufferBytes?: number;
4278
+ /**
4279
+ * Stream inactivity timeout in ms. If no frames arrive from the native
4280
+ * stream for this duration, the stream is considered dead and will be
4281
+ * torn down + restarted (similar to go2rtc's per-packet read deadline).
4282
+ * Default: 15 000 (15s). Set to 0 to disable.
4283
+ */
4284
+ streamTimeoutMs?: number;
4271
4285
  /**
4272
4286
  * When true, the native camera stream is started immediately on start()
4273
4287
  * rather than waiting for the first TCP client. This ensures frames are
@@ -5082,6 +5096,13 @@ export declare class ReolinkBaichuanApi {
5082
5096
  * - "replay:XXX" - dedicated per replay session
5083
5097
  */
5084
5098
  private readonly socketPool;
5099
+ /**
5100
+ * Consecutive stream-start (cmdId=3) timeout counter per socket tag.
5101
+ * When a streaming socket has N consecutive timeouts, the socket is force-closed
5102
+ * so the next attempt creates a fresh connection. Resets on success.
5103
+ */
5104
+ private readonly consecutiveStreamTimeouts;
5105
+ private static readonly MAX_CONSECUTIVE_STREAM_TIMEOUTS;
5085
5106
  /** BaichuanClientOptions to use when creating new sockets */
5086
5107
  private readonly clientOptions;
5087
5108
  /**
@@ -5226,6 +5247,14 @@ export declare class ReolinkBaichuanApi {
5226
5247
  */
5227
5248
  private readonly deviceCapabilitiesCache;
5228
5249
  private static readonly CAPABILITIES_CACHE_TTL_MS;
5250
+ /**
5251
+ * Dedupe key for battery push events (cmd_id 252), per channel.
5252
+ * Cameras emit BatteryInfoList frequently while streaming (every few
5253
+ * seconds). We only forward an event when the meaningful fields change
5254
+ * (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
5255
+ * consumers and the UI event log.
5256
+ */
5257
+ private readonly lastBatteryPushKey;
5229
5258
  /** Keep replay/streaming sockets warm briefly to reduce clip switch latency. */
5230
5259
  private static readonly SOCKET_POOL_KEEPALIVE_MS;
5231
5260
  /**
@@ -6565,6 +6594,22 @@ export declare class ReolinkBaichuanApi {
6565
6594
  * 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
6566
6595
  */
6567
6596
  private notifyD2cDisc;
6597
+ /**
6598
+ * Find the socket pool tag for a given BaichuanClient instance.
6599
+ * Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
6600
+ */
6601
+ private findSocketTagForClient;
6602
+ /**
6603
+ * Reset the consecutive stream-start timeout counter for a streaming socket.
6604
+ * Called on successful stream start.
6605
+ */
6606
+ private resetStreamTimeoutCounter;
6607
+ /**
6608
+ * Track a stream-start timeout on a streaming socket.
6609
+ * After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
6610
+ * socket so the next attempt creates a fresh connection.
6611
+ */
6612
+ private trackStreamTimeout;
6568
6613
  /**
6569
6614
  * Best-effort sleeping inference for battery/BCUDP cameras.
6570
6615
  *
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  parseSupportXml,
44
44
  setGlobalLogger,
45
45
  xmlIndicatesFloodlight
46
- } from "./chunk-F2Y5U3YP.js";
46
+ } from "./chunk-GKLOJJ34.js";
47
47
  import {
48
48
  AesStreamDecryptor,
49
49
  BC_AES_IV,
@@ -4725,6 +4725,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4725
4725
  gracePeriodMs;
4726
4726
  prebufferMaxMs;
4727
4727
  maxBufferBytes;
4728
+ streamTimeoutMs;
4728
4729
  prestartStream;
4729
4730
  active = false;
4730
4731
  server;
@@ -4738,6 +4739,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4738
4739
  connectedClients = /* @__PURE__ */ new Set();
4739
4740
  clientSockets = /* @__PURE__ */ new Map();
4740
4741
  stopGraceTimer;
4742
+ // Stream health monitoring
4743
+ lastFrameAt = 0;
4744
+ streamHealthTimer;
4745
+ totalFramesReceived = 0;
4746
+ totalVideoFramesWritten = 0;
4741
4747
  // Prebuffer
4742
4748
  prebuffer = [];
4743
4749
  constructor(options) {
@@ -4753,6 +4759,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4753
4759
  this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
4754
4760
  this.prebufferMaxMs = options.prebufferMs ?? 3e3;
4755
4761
  this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
4762
+ this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
4756
4763
  this.prestartStream = options.prestartStream ?? true;
4757
4764
  }
4758
4765
  // -----------------------------------------------------------------------
@@ -4791,6 +4798,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4791
4798
  if (!this.active) return;
4792
4799
  this.active = false;
4793
4800
  clearTimeout(this.stopGraceTimer);
4801
+ this.stopStreamHealthMonitor();
4794
4802
  for (const [id, sock] of this.clientSockets) {
4795
4803
  sock.destroy();
4796
4804
  this.connectedClients.delete(id);
@@ -4844,12 +4852,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4844
4852
  `[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
4845
4853
  );
4846
4854
  });
4847
- const cleanup = () => {
4848
- this.removeClient(clientId);
4855
+ const cleanup = (reason) => {
4856
+ this.removeClient(clientId, reason);
4849
4857
  socket.destroy();
4850
4858
  };
4851
- socket.on("error", cleanup);
4852
- socket.on("close", cleanup);
4859
+ socket.on("error", (err) => cleanup(`error: ${err.message}`));
4860
+ socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
4853
4861
  }
4854
4862
  async feedClient(clientId, socket) {
4855
4863
  const fanoutDeadline = Date.now() + 3e4;
@@ -4910,6 +4918,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4910
4918
  }
4911
4919
  socket.write(annexB);
4912
4920
  liveVideoWritten++;
4921
+ this.totalVideoFramesWritten++;
4913
4922
  if (Date.now() - lastLogAt > 1e4) {
4914
4923
  this.logger.info?.(
4915
4924
  `[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
@@ -5036,6 +5045,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5036
5045
  ...dedicatedClient ? { client: dedicatedClient } : {}
5037
5046
  }),
5038
5047
  onFrame: (frame) => {
5048
+ this.lastFrameAt = Date.now();
5049
+ this.totalFramesReceived++;
5039
5050
  if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
5040
5051
  this.detectedVideoType = frame.videoType;
5041
5052
  }
@@ -5062,6 +5073,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5062
5073
  if (!this.nativeStreamActive) return;
5063
5074
  this.nativeStreamActive = false;
5064
5075
  this.nativeFanout = null;
5076
+ this.stopStreamHealthMonitor();
5077
+ const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
5078
+ const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
5079
+ this.logger.warn?.(
5080
+ `[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
5081
+ );
5065
5082
  if (this.dedicatedSessionRelease) {
5066
5083
  this.dedicatedSessionRelease().catch(() => {
5067
5084
  });
@@ -5069,16 +5086,18 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5069
5086
  }
5070
5087
  if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
5071
5088
  this.logger.info?.(
5072
- `[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
5089
+ `[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
5073
5090
  );
5074
5091
  this.startNativeStream();
5075
5092
  }
5076
5093
  }
5077
5094
  });
5078
5095
  this.nativeFanout.start();
5096
+ this.startStreamHealthMonitor();
5079
5097
  }
5080
5098
  async stopNativeStream() {
5081
5099
  this.nativeStreamActive = false;
5100
+ this.stopStreamHealthMonitor();
5082
5101
  const fanout = this.nativeFanout;
5083
5102
  this.nativeFanout = null;
5084
5103
  if (fanout) {
@@ -5092,14 +5111,50 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5092
5111
  }
5093
5112
  }
5094
5113
  // -----------------------------------------------------------------------
5114
+ // Stream health monitoring
5115
+ // -----------------------------------------------------------------------
5116
+ startStreamHealthMonitor() {
5117
+ this.stopStreamHealthMonitor();
5118
+ if (this.streamTimeoutMs <= 0) return;
5119
+ this.lastFrameAt = Date.now();
5120
+ this.streamHealthTimer = setInterval(() => {
5121
+ if (!this.nativeStreamActive || !this.active) {
5122
+ this.stopStreamHealthMonitor();
5123
+ return;
5124
+ }
5125
+ const silenceMs = Date.now() - this.lastFrameAt;
5126
+ if (silenceMs > this.streamTimeoutMs) {
5127
+ this.logger.warn?.(
5128
+ `[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`
5129
+ );
5130
+ this.stopStreamHealthMonitor();
5131
+ const fanout = this.nativeFanout;
5132
+ if (fanout) {
5133
+ this.nativeStreamActive = false;
5134
+ this.nativeFanout = null;
5135
+ fanout.stop().catch(() => {
5136
+ });
5137
+ }
5138
+ }
5139
+ }, Math.min(this.streamTimeoutMs / 2, 5e3));
5140
+ }
5141
+ stopStreamHealthMonitor() {
5142
+ if (this.streamHealthTimer) {
5143
+ clearInterval(this.streamHealthTimer);
5144
+ this.streamHealthTimer = void 0;
5145
+ }
5146
+ }
5147
+ // -----------------------------------------------------------------------
5095
5148
  // Client lifecycle
5096
5149
  // -----------------------------------------------------------------------
5097
- removeClient(clientId) {
5150
+ removeClient(clientId, reason) {
5098
5151
  if (!this.connectedClients.has(clientId)) return;
5099
5152
  this.connectedClients.delete(clientId);
5100
5153
  this.clientSockets.delete(clientId);
5154
+ const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
5155
+ const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
5101
5156
  this.logger.info?.(
5102
- `[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
5157
+ `[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
5103
5158
  );
5104
5159
  this.emit("clientDisconnected", clientId);
5105
5160
  if (this.connectedClients.size === 0 && !this.prestartStream) {