@apocaliss92/nodelink-js 0.6.7 → 0.6.8-beta.0

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-IQVVVSXO.js";
33
+ } from "./chunk-27ZKJZFH.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-MZUSWKF3.js";
181
+ } from "./chunk-4LZABN6I.js";
182
182
 
183
183
  // src/protocol/framing.ts
184
184
  function encodeHeader(h) {
@@ -2473,6 +2473,16 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2473
2473
  // Public to allow ReolinkBaichuanApi to check subscription status
2474
2474
  keepAliveTimer;
2475
2475
  keepAlivePingInFlight = false;
2476
+ // Consecutive TCP keepalive (PING) failures with no response. A half-open
2477
+ // socket after a camera reboot is NOT reported as `destroyed`, so
2478
+ // `isSocketConnected()` stays stale-true and recovery never triggers. We
2479
+ // treat repeated unanswered pings as a hard liveness failure and tear the
2480
+ // socket down so the 'close' handler fires (resetting loggedIn/subscribed
2481
+ // and emitting "close"), which lets ensureConnected() rebuild it.
2482
+ consecutiveKeepalivePingFailures = 0;
2483
+ // Number of unanswered keepalive pings tolerated before the TCP socket is
2484
+ // declared dead. 2 misses ≈ up to ~2 keepalive intervals of silence.
2485
+ static KEEPALIVE_MAX_PING_FAILURES = 2;
2476
2486
  // Battery/BCUDP cameras may actively terminate sessions (D2C_DISC) when overwhelmed
2477
2487
  // or sleeping. Without a cooldown, callers can end up in tight reconnect loops.
2478
2488
  udpReconnectCooldownUntilMs = 0;
@@ -3101,8 +3111,21 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
3101
3111
  // Keepalive has empty body
3102
3112
  internal: true
3103
3113
  });
3114
+ this.consecutiveKeepalivePingFailures = 0;
3104
3115
  } catch (e) {
3105
- this.logDebug("keepalive_ping_failed", e);
3116
+ this.consecutiveKeepalivePingFailures++;
3117
+ this.logDebug("keepalive_ping_failed", {
3118
+ error: e,
3119
+ consecutiveFailures: this.consecutiveKeepalivePingFailures
3120
+ });
3121
+ if (this.transport === "tcp" && this.consecutiveKeepalivePingFailures >= _BaichuanClient.KEEPALIVE_MAX_PING_FAILURES && this.tcpSocket && !this.tcpSocket.destroyed) {
3122
+ this.logFixed(
3123
+ "keepalive_dead",
3124
+ `transport=tcp host=${this.opts.host} consecutiveFailures=${this.consecutiveKeepalivePingFailures} \u2014 destroying half-open socket`
3125
+ );
3126
+ this.consecutiveKeepalivePingFailures = 0;
3127
+ this.tcpSocket.destroy();
3128
+ }
3106
3129
  }
3107
3130
  }
3108
3131
  async connect() {
@@ -3195,6 +3218,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
3195
3218
  this.tcpSocket = sock;
3196
3219
  this.transport = "tcp";
3197
3220
  this.socketClosed = false;
3221
+ this.consecutiveKeepalivePingFailures = 0;
3198
3222
  this.socketSessionId = this.newSocketSessionId("tcp");
3199
3223
  try {
3200
3224
  sock.setKeepAlive(true, 3e4);
@@ -6676,6 +6700,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter4 {
6676
6700
  // from the last IDR in the prebuffer immediately.
6677
6701
  PREBUFFER_MAX_MS = 3e3;
6678
6702
  prebuffer = [];
6703
+ /**
6704
+ * Hard cap on the userspace TCP send buffer for a single RTSP client. A
6705
+ * healthy consumer drains near-instantly; if this much data backs up the
6706
+ * client is dead or far too slow to keep up with the live stream.
6707
+ */
6708
+ static MAX_CLIENT_BUFFERED_BYTES = 8 * 1024 * 1024;
6709
+ /**
6710
+ * Pure backpressure decision: should the client socket be disconnected
6711
+ * because its send buffer has grown past the hard cap? Extracted so the
6712
+ * decision can be unit-tested without a live socket.
6713
+ */
6714
+ static shouldDisconnectForBackpressure(bufferedBytes) {
6715
+ return bufferedBytes > _BaichuanRtspServer.MAX_CLIENT_BUFFERED_BYTES;
6716
+ }
6679
6717
  static isAdtsAacFrame(b) {
6680
6718
  return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
6681
6719
  }
@@ -7652,14 +7690,36 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter4 {
7652
7690
  const rtspDebugLog = (message) => this.rtspDebugLog(message);
7653
7691
  const sendInterleaved = (channel, msg) => {
7654
7692
  if (!rtspSocket || rtspSocket.destroyed || !rtspSocket.writable)
7655
- return false;
7693
+ return true;
7694
+ if (_BaichuanRtspServer.shouldDisconnectForBackpressure(
7695
+ rtspSocket.writableLength
7696
+ )) {
7697
+ this.logger.warn(
7698
+ `[rebroadcast] backpressure: ${Math.round(rtspSocket.writableLength / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
7699
+ );
7700
+ rtspSocket.destroy();
7701
+ return true;
7702
+ }
7656
7703
  try {
7657
7704
  return rtspSocket.write(frameRtpOverTcp(channel, msg));
7658
7705
  } catch (error) {
7659
7706
  if (error && typeof error === "object" && "code" in error && error.code === "EPIPE")
7660
- return false;
7707
+ return true;
7661
7708
  }
7662
- return false;
7709
+ return true;
7710
+ };
7711
+ const awaitClientDrain = async () => {
7712
+ if (!rtspSocket || rtspSocket.destroyed) return;
7713
+ if (!rtspSocket.writableNeedDrain) return;
7714
+ await new Promise((resolve) => {
7715
+ const done = () => {
7716
+ rtspSocket.removeListener("drain", done);
7717
+ rtspSocket.removeListener("close", done);
7718
+ resolve();
7719
+ };
7720
+ rtspSocket.once("drain", done);
7721
+ rtspSocket.once("close", done);
7722
+ });
7663
7723
  };
7664
7724
  const getVideoChannel = () => resources?.track0RtpChannel ?? 0;
7665
7725
  const getAudioChannel = () => resources?.track1RtpChannel ?? 2;
@@ -8238,6 +8298,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter4 {
8238
8298
  resources.framesSent = (resources.framesSent ?? 0) + 1;
8239
8299
  }
8240
8300
  sendVideoAccessUnit(videoType, normalizedVideoData, true);
8301
+ await awaitClientDrain();
8241
8302
  } else {
8242
8303
  try {
8243
8304
  if (stdin && !stdin.destroyed && !stdin.writableEnded && !stdin.writableFinished) {
@@ -14528,7 +14589,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
14528
14589
  return;
14529
14590
  }
14530
14591
  entry.startInFlight = (async () => {
14531
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-OCLOM452.js");
14592
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-6Y2JSBLN.js");
14532
14593
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
14533
14594
  const dedicated = await this.createDedicatedSession(sessionKey);
14534
14595
  const stream = new BaichuanVideoStream2({
@@ -14679,9 +14740,45 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
14679
14740
  const generalEntry = this.socketPool.get("general");
14680
14741
  if (!generalEntry) return;
14681
14742
  if (!generalEntry.client.isSocketConnected?.() || !generalEntry.client.loggedIn) {
14682
- this.logger.debug?.(
14683
- `[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
14743
+ if (!this.eventWatchdogSilenceResubscribeEnabled) {
14744
+ this.logger.debug?.(
14745
+ `[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive, battery/UDP \u2014 not forcing reconnect: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
14746
+ );
14747
+ return;
14748
+ }
14749
+ const now2 = Date.now();
14750
+ const backoffMs = Math.min(
14751
+ 3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
14752
+ this.simpleEventWatchdogSilenceThresholdMs
14753
+ );
14754
+ if (now2 - this.simpleEventWatchdogLastRecoveryAt < backoffMs) {
14755
+ this.logger.debug?.(
14756
+ `[ReolinkBaichuanApi] event watchdog tick: connection down, reconnect backoff active (connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
14757
+ );
14758
+ return;
14759
+ }
14760
+ this.simpleEventWatchdogRecoveryAttempts++;
14761
+ this.simpleEventWatchdogLastRecoveryAt = now2;
14762
+ (this.logger.info ?? this.logger.log).call(
14763
+ this.logger,
14764
+ `[ReolinkBaichuanApi] event watchdog: control socket down, forcing full reconnect (attempt #${this.simpleEventWatchdogRecoveryAttempts})`,
14765
+ { host: this.host }
14684
14766
  );
14767
+ try {
14768
+ await this.ensureConnected();
14769
+ this.simpleEventLastReceivedAt = Date.now();
14770
+ this.simpleEventWatchdogRecoveryAttempts = 0;
14771
+ (this.logger.info ?? this.logger.log).call(
14772
+ this.logger,
14773
+ `[ReolinkBaichuanApi] event watchdog: full reconnect successful`
14774
+ );
14775
+ } catch (e) {
14776
+ (this.logger.debug ?? this.logger.log).call(
14777
+ this.logger,
14778
+ `[ReolinkBaichuanApi] event watchdog: full reconnect attempt #${this.simpleEventWatchdogRecoveryAttempts} failed`,
14779
+ formatErrorForLog(e)
14780
+ );
14781
+ }
14685
14782
  return;
14686
14783
  }
14687
14784
  const now = Date.now();
@@ -21282,7 +21379,7 @@ ${xml}`
21282
21379
  * @returns Test results for all stream types and profiles
21283
21380
  */
21284
21381
  async testChannelStreams(channel, logger) {
21285
- const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA.js");
21382
+ const { testChannelStreams } = await import("./DiagnosticsTools-G3S6L3FX.js");
21286
21383
  return await testChannelStreams({
21287
21384
  api: this,
21288
21385
  channel: this.normalizeChannel(channel),
@@ -21298,7 +21395,7 @@ ${xml}`
21298
21395
  * @returns Complete diagnostics for all channels and streams
21299
21396
  */
21300
21397
  async collectMultifocalDiagnostics(logger) {
21301
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA.js");
21398
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-G3S6L3FX.js");
21302
21399
  return await collectMultifocalDiagnostics({
21303
21400
  api: this,
21304
21401
  logger
@@ -26090,4 +26187,4 @@ export {
26090
26187
  tcpReachabilityProbe,
26091
26188
  autoDetectDeviceType
26092
26189
  };
26093
- //# sourceMappingURL=chunk-T22QCNBR.js.map
26190
+ //# sourceMappingURL=chunk-OJ4YR45R.js.map