@apocaliss92/nodelink-js 0.4.1 → 0.4.3

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.
@@ -7450,7 +7450,7 @@ async function captureModelFixtures(params) {
7450
7450
  () => api.getDualLensChannelInfo(channel),
7451
7451
  (v) => writeJsonSafe(path4.join(outDir, "dual-lens-info.json"), v)
7452
7452
  );
7453
- await capture("streamCombinationTest", async () => {
7453
+ if (!params.skipStreamCombinationTest) await capture("streamCombinationTest", async () => {
7454
7454
  let dualLensInfo;
7455
7455
  try {
7456
7456
  dualLensInfo = await api.getDualLensChannelInfo(channel);
@@ -8070,6 +8070,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8070
8070
  flow;
8071
8071
  deviceId;
8072
8072
  dedicatedSessionRelease;
8073
+ externalListener;
8073
8074
  // Authentication
8074
8075
  authCredentials = [];
8075
8076
  requireAuth;
@@ -8112,6 +8113,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8112
8113
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
8113
8114
  nativeFanout = null;
8114
8115
  noClientAutoStopTimer;
8116
+ /** After last RTSP client; 0 = never auto-stop native stream. */
8117
+ nativeStreamIdleStopMs;
8118
+ /** Primed-but-no-PLAY timeout; 0 = disabled. */
8119
+ nativeStreamPrimeIdleStopMs;
8115
8120
  // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
8116
8121
  // When a new client connects while the stream is already running it does not need
8117
8122
  // to wait up to one full GOP interval for the next keyframe — we replay frames
@@ -8237,6 +8242,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8237
8242
  this.logger = options.logger ?? console;
8238
8243
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
8239
8244
  this.deviceId = options.deviceId;
8245
+ this.externalListener = options.externalListener ?? false;
8246
+ this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
8247
+ this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
8240
8248
  this.authCredentials = options.credentials ?? [];
8241
8249
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
8242
8250
  const transport = this.api.client.getTransport();
@@ -8367,41 +8375,56 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8367
8375
  this.streamMetadata = { frameRate: 25 };
8368
8376
  this.setFlowVideoType("H264", "metadata unavailable");
8369
8377
  }
8370
- this.clientConnectionServer = net.createServer((socket) => {
8371
- this.handleRtspConnection(socket);
8372
- });
8373
- await new Promise((resolve, reject) => {
8374
- this.clientConnectionServer.listen(
8375
- this.listenPort,
8376
- this.listenHost,
8377
- () => {
8378
- const address = this.clientConnectionServer.address();
8379
- if (address && typeof address === "object" && "port" in address) {
8380
- this.listenPort = address.port;
8378
+ if (!this.externalListener) {
8379
+ this.clientConnectionServer = net.createServer((socket) => {
8380
+ this.handleRtspConnection(socket);
8381
+ });
8382
+ await new Promise((resolve, reject) => {
8383
+ this.clientConnectionServer.listen(
8384
+ this.listenPort,
8385
+ this.listenHost,
8386
+ () => {
8387
+ const address = this.clientConnectionServer.address();
8388
+ if (address && typeof address === "object" && "port" in address) {
8389
+ this.listenPort = address.port;
8390
+ }
8391
+ resolve();
8381
8392
  }
8382
- resolve();
8383
- }
8384
- );
8385
- this.clientConnectionServer.on("error", (error) => {
8386
- reject(error);
8393
+ );
8394
+ this.clientConnectionServer.on("error", (error) => {
8395
+ reject(error);
8396
+ });
8387
8397
  });
8388
- });
8398
+ }
8389
8399
  this.active = true;
8390
8400
  this.logger.info(
8391
8401
  `[BaichuanRtspServer] RTSP server started on ${this.listenHost}:${this.listenPort}, path: ${this.path}`
8392
8402
  );
8393
8403
  }
8404
+ /**
8405
+ * Accept an externally-routed RTSP connection.
8406
+ * Used in directHandoff mode where RtspProxyServer routes sockets here.
8407
+ * @param socket - The client TCP socket (already authenticated by proxy)
8408
+ * @param initialBuffer - Any bytes already read during path parsing/auth
8409
+ */
8410
+ acceptConnection(socket, initialBuffer) {
8411
+ if (!this.active) {
8412
+ socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
8413
+ return;
8414
+ }
8415
+ this.handleRtspConnection(socket, initialBuffer);
8416
+ }
8394
8417
  /**
8395
8418
  * Handle RTSP connection from a client.
8396
8419
  */
8397
- handleRtspConnection(socket) {
8420
+ handleRtspConnection(socket, initialBuffer) {
8398
8421
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
8399
8422
  const connectTime = Date.now();
8400
8423
  this.logger.info(
8401
8424
  `[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
8402
8425
  );
8403
8426
  let sessionId = "";
8404
- let buffer = Buffer.alloc(0);
8427
+ let buffer = initialBuffer ?? Buffer.alloc(0);
8405
8428
  let clientFfmpeg;
8406
8429
  let useTcpInterleaved = false;
8407
8430
  let clientUdpSocket = null;
@@ -8486,8 +8509,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8486
8509
  }
8487
8510
  cleanup();
8488
8511
  });
8489
- socket.on("data", async (data) => {
8490
- buffer = Buffer.concat([buffer, data]);
8512
+ const processBuffer = async () => {
8491
8513
  while (buffer.includes("\r\n\r\n")) {
8492
8514
  const endIndex = buffer.indexOf("\r\n\r\n");
8493
8515
  const requestText = buffer.subarray(0, endIndex).toString();
@@ -8762,7 +8784,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8762
8784
  sendResponse(501, "Not Implemented");
8763
8785
  }
8764
8786
  }
8787
+ };
8788
+ socket.on("data", (data) => {
8789
+ buffer = Buffer.concat([buffer, data]);
8790
+ void processBuffer();
8765
8791
  });
8792
+ if (buffer.includes("\r\n\r\n")) {
8793
+ void processBuffer();
8794
+ }
8766
8795
  }
8767
8796
  /**
8768
8797
  * Generate SDP (Session Description Protocol) for RTSP DESCRIBE.
@@ -9730,34 +9759,42 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9730
9759
  res.seenFirstVideoKeyframe = false;
9731
9760
  res.rtpSentVideoConfig = false;
9732
9761
  }
9733
- if (this.dedicatedSessionRelease) {
9734
- const release = this.dedicatedSessionRelease;
9735
- this.dedicatedSessionRelease = void 0;
9736
- release().catch(() => {
9737
- });
9738
- }
9739
9762
  this.logger.info(
9740
9763
  `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
9741
9764
  );
9742
- if (this.connectedClients.size > 0) {
9743
- this.logger.info(
9744
- `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
9745
- );
9746
- setImmediate(() => void this.startNativeStream());
9747
- }
9765
+ const releaseAndRestart = async () => {
9766
+ if (this.dedicatedSessionRelease) {
9767
+ const release = this.dedicatedSessionRelease;
9768
+ this.dedicatedSessionRelease = void 0;
9769
+ try {
9770
+ await release();
9771
+ } catch {
9772
+ }
9773
+ }
9774
+ if (this.connectedClients.size > 0) {
9775
+ this.logger.info(
9776
+ `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
9777
+ );
9778
+ await new Promise((r) => setTimeout(r, 500));
9779
+ void this.startNativeStream();
9780
+ }
9781
+ };
9782
+ void releaseAndRestart();
9748
9783
  }
9749
9784
  });
9750
9785
  this.nativeFanout.start();
9751
9786
  this.clearNoClientAutoStopTimer();
9752
- this.noClientAutoStopTimer = setTimeout(() => {
9753
- if (this.connectedClients.size === 0) {
9754
- this.rtspDebugLog(
9755
- `Auto-stopping primed native stream (no clients connected)`
9756
- );
9757
- void this.stopNativeStream();
9758
- }
9759
- }, 15e3);
9760
- this.noClientAutoStopTimer?.unref?.();
9787
+ if (this.nativeStreamPrimeIdleStopMs > 0) {
9788
+ this.noClientAutoStopTimer = setTimeout(() => {
9789
+ if (this.connectedClients.size === 0) {
9790
+ this.rtspDebugLog(
9791
+ `Auto-stopping primed native stream (no clients connected)`
9792
+ );
9793
+ void this.stopNativeStream();
9794
+ }
9795
+ }, this.nativeStreamPrimeIdleStopMs);
9796
+ this.noClientAutoStopTimer?.unref?.();
9797
+ }
9761
9798
  }
9762
9799
  markFirstFrameReceived() {
9763
9800
  if (!this.firstFrameReceived && this.firstFrameResolve) {
@@ -9832,12 +9869,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9832
9869
  this.emit("clientDisconnected", clientId);
9833
9870
  if (this.connectedClients.size === 0) {
9834
9871
  this.clearNoClientAutoStopTimer();
9835
- this.noClientAutoStopTimer = setTimeout(() => {
9836
- if (this.connectedClients.size === 0) {
9837
- void this.stopNativeStream();
9838
- }
9839
- }, 3e4);
9840
- this.noClientAutoStopTimer?.unref?.();
9872
+ if (this.nativeStreamIdleStopMs > 0) {
9873
+ this.noClientAutoStopTimer = setTimeout(() => {
9874
+ if (this.connectedClients.size === 0) {
9875
+ void this.stopNativeStream();
9876
+ }
9877
+ }, this.nativeStreamIdleStopMs);
9878
+ this.noClientAutoStopTimer?.unref?.();
9879
+ }
9841
9880
  }
9842
9881
  }
9843
9882
  }
@@ -9968,6 +10007,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9968
10007
  getClientCount() {
9969
10008
  return this.connectedClients.size;
9970
10009
  }
10010
+ /**
10011
+ * Subscribe to the raw native stream for diagnostic purposes.
10012
+ * The subscriber receives the same frames as RTSP clients.
10013
+ * Counts as a "consumer" for lifecycle — prevents auto-stop while subscribed.
10014
+ * If the native stream is not active, starts it automatically.
10015
+ */
10016
+ async subscribeDiagnostic(id) {
10017
+ this.connectedClients.add(`diag:${id}`);
10018
+ if (!this.nativeStreamActive) {
10019
+ await this.startNativeStream();
10020
+ }
10021
+ return this.nativeFanout.subscribe(`diag:${id}`);
10022
+ }
10023
+ /**
10024
+ * Unsubscribe a diagnostic session.
10025
+ */
10026
+ unsubscribeDiagnostic(id) {
10027
+ this.removeClient(`diag:${id}`);
10028
+ }
10029
+ /**
10030
+ * Returns detected audio metadata (available after first audio frame).
10031
+ */
10032
+ getAudioInfo() {
10033
+ return this.audioInfo;
10034
+ }
9971
10035
  };
9972
10036
 
9973
10037
  // src/reolink/baichuan/ReolinkBaichuanApi.ts