@apocaliss92/nodelink-js 0.4.7 → 0.4.8

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.
@@ -144,7 +144,7 @@ import {
144
144
  talkTraceLog,
145
145
  traceLog,
146
146
  xmlEscape
147
- } from "./chunk-TR3V5FTO.js";
147
+ } from "./chunk-YKKQDUKU.js";
148
148
 
149
149
  // src/protocol/framing.ts
150
150
  function encodeHeader(h) {
@@ -5325,19 +5325,34 @@ async function* createNativeStream(api, channel, profile, options) {
5325
5325
  }
5326
5326
  });
5327
5327
  streamStarted = true;
5328
- while (!closed) {
5328
+ const signal = options?.signal;
5329
+ while (!closed && !signal?.aborted) {
5329
5330
  if (frameQueue.length > 0) {
5330
5331
  const frame = frameQueue.shift();
5331
5332
  yield frame;
5332
5333
  } else {
5333
5334
  await new Promise((resolve) => {
5334
5335
  frameResolve = resolve;
5335
- setTimeout(() => {
5336
+ const timer = setTimeout(() => {
5336
5337
  if (frameResolve === resolve) {
5337
5338
  frameResolve = null;
5338
5339
  resolve();
5339
5340
  }
5340
5341
  }, 1e3);
5342
+ if (signal) {
5343
+ const onAbort = () => {
5344
+ clearTimeout(timer);
5345
+ if (frameResolve === resolve) frameResolve = null;
5346
+ resolve();
5347
+ };
5348
+ if (signal.aborted) {
5349
+ clearTimeout(timer);
5350
+ frameResolve = null;
5351
+ resolve();
5352
+ } else {
5353
+ signal.addEventListener("abort", onAbort, { once: true });
5354
+ }
5355
+ }
5341
5356
  });
5342
5357
  }
5343
5358
  }
@@ -5513,13 +5528,14 @@ var NativeStreamFanout = class {
5513
5528
  source = null;
5514
5529
  running = false;
5515
5530
  pumpPromise = null;
5531
+ abort = new AbortController();
5516
5532
  constructor(opts) {
5517
5533
  this.opts = opts;
5518
5534
  }
5519
5535
  start() {
5520
5536
  if (this.running) return;
5521
5537
  this.running = true;
5522
- this.source = this.opts.createSource();
5538
+ this.source = this.opts.createSource(this.abort.signal);
5523
5539
  this.pumpPromise = (async () => {
5524
5540
  try {
5525
5541
  for await (const frame of this.source) {
@@ -5565,6 +5581,7 @@ var NativeStreamFanout = class {
5565
5581
  this.source = null;
5566
5582
  for (const q of this.queues.values()) q.close();
5567
5583
  this.queues.clear();
5584
+ this.abort.abort();
5568
5585
  try {
5569
5586
  await src?.return(void 0);
5570
5587
  } catch {
@@ -5603,9 +5620,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5603
5620
  requireAuth;
5604
5621
  authNonces = /* @__PURE__ */ new Map();
5605
5622
  // Track nonces per client
5606
- AUTH_REALM = "BaichuanRtspServer";
5623
+ AUTH_REALM;
5607
5624
  NONCE_TIMEOUT_MS = 3e5;
5608
5625
  // 5 minutes
5626
+ lazyMetadata;
5609
5627
  // Client tracking
5610
5628
  connectedClients = /* @__PURE__ */ new Set();
5611
5629
  // Set of client IDs (IP:port)
@@ -5617,8 +5635,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5617
5635
  // Track all client resources for cleanup
5618
5636
  clientResources = /* @__PURE__ */ new Map();
5619
5637
  isRtspDebugEnabled() {
5620
- const dbg = this.api.client.getDebugConfig();
5621
- return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5638
+ try {
5639
+ if (this.api.isClosed) {
5640
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5641
+ }
5642
+ const dbg = this.api.client.getDebugConfig();
5643
+ return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5644
+ } catch {
5645
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
5646
+ }
5622
5647
  }
5623
5648
  rtspDebugLog(message) {
5624
5649
  if (!this.isRtspDebugEnabled()) return;
@@ -5640,10 +5665,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5640
5665
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
5641
5666
  nativeFanout = null;
5642
5667
  noClientAutoStopTimer;
5668
+ /** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
5669
+ noFrameDeadlineTimer;
5643
5670
  /** After last RTSP client; 0 = never auto-stop native stream. */
5644
5671
  nativeStreamIdleStopMs;
5645
5672
  /** Primed-but-no-PLAY timeout; 0 = disabled. */
5646
5673
  nativeStreamPrimeIdleStopMs;
5674
+ /**
5675
+ * Max time to wait for the first camera frame after stream start.
5676
+ * If no frames arrive within this window, the native stream is stopped
5677
+ * (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
5678
+ * firing and waking the camera when no real viewer is watching.
5679
+ * 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
5680
+ */
5681
+ nativeStreamNoFrameDeadlineMs;
5647
5682
  // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
5648
5683
  // When a new client connects while the stream is already running it does not need
5649
5684
  // to wait up to one full GOP interval for the next keyframe — we replay frames
@@ -5769,14 +5804,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5769
5804
  this.logger = options.logger ?? console;
5770
5805
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
5771
5806
  this.deviceId = options.deviceId;
5772
- this.externalListener = options.externalListener ?? false;
5807
+ this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
5773
5808
  this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
5774
5809
  this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
5775
- this.authCredentials = options.credentials ?? [];
5810
+ this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
5811
+ this.authCredentials = (options.credentials ?? []).map((c) => ({
5812
+ username: c.username,
5813
+ ...c.password !== void 0 ? { password: c.password } : {},
5814
+ ...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
5815
+ }));
5776
5816
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
5817
+ this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
5818
+ this.lazyMetadata = options.lazyMetadata ?? false;
5777
5819
  const transport = this.api.client.getTransport();
5778
5820
  this.flow = createRtspFlow(transport, "H264");
5779
5821
  }
5822
+ /** Number of currently connected RTSP clients. */
5823
+ get clientCount() {
5824
+ return this.connectedClients.size;
5825
+ }
5780
5826
  // --- Authentication helpers ---
5781
5827
  /**
5782
5828
  * Generate a new nonce for Digest authentication
@@ -5837,9 +5883,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5837
5883
  this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
5838
5884
  return false;
5839
5885
  }
5886
+ if (realm !== this.AUTH_REALM) {
5887
+ this.rtspDebugLog(
5888
+ `Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
5889
+ );
5890
+ return false;
5891
+ }
5840
5892
  for (const cred of this.authCredentials) {
5841
5893
  if (username !== cred.username) continue;
5842
- const ha1 = this.md5(`${cred.username}:${realm}:${cred.password}`);
5894
+ const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
5895
+ if (!ha1) continue;
5843
5896
  const ha2 = this.md5(`${method}:${authUri || uri}`);
5844
5897
  const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
5845
5898
  if (response === expectedResponse) {
@@ -5868,6 +5921,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5868
5921
  this.noClientAutoStopTimer = void 0;
5869
5922
  }
5870
5923
  }
5924
+ clearNoFrameDeadlineTimer() {
5925
+ if (this.noFrameDeadlineTimer) {
5926
+ clearTimeout(this.noFrameDeadlineTimer);
5927
+ this.noFrameDeadlineTimer = void 0;
5928
+ }
5929
+ }
5871
5930
  setFlowVideoType(videoType, reason) {
5872
5931
  if (this.flow.videoType === videoType) return;
5873
5932
  const transport = this.api.client.getTransport();
@@ -5882,25 +5941,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5882
5941
  if (this.active) {
5883
5942
  throw new Error("RTSP server is already active");
5884
5943
  }
5885
- try {
5886
- const metadata = await this.api.getStreamMetadata(this.channel);
5887
- const stream = metadata.streams.find((s) => s.profile === this.profile);
5888
- if (stream) {
5889
- this.streamMetadata = {
5890
- frameRate: stream.frameRate || 25,
5891
- width: stream.width,
5892
- height: stream.height
5893
- };
5894
- const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
5895
- const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
5896
- this.setFlowVideoType(metaVideoType, "metadata");
5897
- }
5898
- } catch (error) {
5899
- this.logger.warn(
5900
- `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5944
+ if (this.lazyMetadata) {
5945
+ this.logger.info(
5946
+ `[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
5901
5947
  );
5902
- this.streamMetadata = { frameRate: 25 };
5903
- this.setFlowVideoType("H264", "metadata unavailable");
5948
+ } else {
5949
+ try {
5950
+ const metadata = await this.api.getStreamMetadata(this.channel);
5951
+ const stream = metadata.streams.find((s) => s.profile === this.profile);
5952
+ if (stream) {
5953
+ this.streamMetadata = {
5954
+ frameRate: stream.frameRate || 25,
5955
+ width: stream.width,
5956
+ height: stream.height
5957
+ };
5958
+ const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
5959
+ const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
5960
+ this.setFlowVideoType(metaVideoType, "metadata");
5961
+ }
5962
+ } catch (error) {
5963
+ this.logger.warn(
5964
+ `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5965
+ );
5966
+ this.streamMetadata = { frameRate: 25 };
5967
+ this.setFlowVideoType("H264", "metadata unavailable");
5968
+ }
5904
5969
  }
5905
5970
  if (!this.externalListener) {
5906
5971
  this.clientConnectionServer = net2.createServer((socket) => {
@@ -5941,6 +6006,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5941
6006
  }
5942
6007
  this.handleRtspConnection(socket, initialBuffer);
5943
6008
  }
6009
+ /**
6010
+ * Inject an already-accepted client socket from a multiplexer
6011
+ * (e.g. `LocalRtspMux`) that owns the listening port.
6012
+ *
6013
+ * The mux reads the first RTSP request line to determine the target path,
6014
+ * then hands the socket over. Any bytes already consumed during routing
6015
+ * are replayed back onto the socket via `unshift()` so the RTSP parser in
6016
+ * `handleRtspConnection` sees the complete original request.
6017
+ *
6018
+ * @param socket - Client TCP socket, already accepted by the mux.
6019
+ * @param preReadData - Bytes the mux has already pulled off the socket
6020
+ * while parsing the request line. Replayed via `socket.unshift()`
6021
+ * before any further reads.
6022
+ */
6023
+ injectSocket(socket, preReadData) {
6024
+ if (!this.active) {
6025
+ socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
6026
+ return;
6027
+ }
6028
+ if (preReadData && preReadData.length > 0) {
6029
+ socket.unshift(preReadData);
6030
+ }
6031
+ this.handleRtspConnection(socket);
6032
+ }
5944
6033
  /**
5945
6034
  * Handle RTSP connection from a client.
5946
6035
  */
@@ -6105,6 +6194,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6105
6194
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
6106
6195
  });
6107
6196
  } else if (method === "DESCRIBE") {
6197
+ if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
6198
+ void this.api.ensureConnected().catch(() => {
6199
+ });
6200
+ }
6108
6201
  if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
6109
6202
  try {
6110
6203
  if (!this.nativeStreamActive) {
@@ -6142,6 +6235,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6142
6235
  }
6143
6236
  }
6144
6237
  }
6238
+ if (!this.hasAudio && this.firstAudioPromise) {
6239
+ const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
6240
+ const audioPrimingStart = Date.now();
6241
+ try {
6242
+ await Promise.race([
6243
+ this.firstAudioPromise,
6244
+ new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
6245
+ ]);
6246
+ } catch {
6247
+ }
6248
+ const audioPrimingElapsed = Date.now() - audioPrimingStart;
6249
+ if (this.hasAudio) {
6250
+ this.logger.info(
6251
+ `[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
6252
+ );
6253
+ } else {
6254
+ this.logger.info(
6255
+ `[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
6256
+ );
6257
+ }
6258
+ }
6145
6259
  {
6146
6260
  const { fmtp, hasParamSets } = this.flow.getFmtp();
6147
6261
  const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
@@ -6150,12 +6264,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6150
6264
  );
6151
6265
  }
6152
6266
  const sdp = this.generateSdp();
6267
+ const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
6153
6268
  sendResponse(
6154
6269
  200,
6155
6270
  "OK",
6156
6271
  {
6157
6272
  "Content-Type": "application/sdp",
6158
- "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
6273
+ "Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
6159
6274
  },
6160
6275
  sdp
6161
6276
  );
@@ -6178,7 +6293,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6178
6293
  this.emit("client", clientId);
6179
6294
  this.clearNoClientAutoStopTimer();
6180
6295
  if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
6181
- await this.startNativeStream();
6296
+ void this.startNativeStream();
6182
6297
  }
6183
6298
  const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
6184
6299
  const transport = (transportMatch?.[1] ?? "").trim();
@@ -6312,12 +6427,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6312
6427
  }
6313
6428
  }
6314
6429
  };
6430
+ const runProcessBuffer = () => {
6431
+ processBuffer().catch((err) => {
6432
+ this.logger.debug(
6433
+ `[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
6434
+ );
6435
+ try {
6436
+ socket.destroy();
6437
+ } catch {
6438
+ }
6439
+ });
6440
+ };
6315
6441
  socket.on("data", (data) => {
6316
6442
  buffer = Buffer.concat([buffer, data]);
6317
- void processBuffer();
6443
+ runProcessBuffer();
6318
6444
  });
6319
6445
  if (buffer.includes("\r\n\r\n")) {
6320
- void processBuffer();
6446
+ runProcessBuffer();
6321
6447
  }
6322
6448
  }
6323
6449
  /**
@@ -7185,6 +7311,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7185
7311
  if (this.nativeStreamActive) {
7186
7312
  return;
7187
7313
  }
7314
+ if (!this.api.isReady) {
7315
+ if (this.api.isClosed) {
7316
+ this.logger.warn?.(
7317
+ `[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
7318
+ );
7319
+ return;
7320
+ }
7321
+ try {
7322
+ this.logger.info?.(
7323
+ `[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
7324
+ );
7325
+ await this.api.ensureConnected();
7326
+ } catch (e) {
7327
+ this.logger.warn?.(
7328
+ `[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
7329
+ );
7330
+ return;
7331
+ }
7332
+ }
7188
7333
  this.nativeStreamActive = true;
7189
7334
  this.firstFrameReceived = false;
7190
7335
  this.firstAudioDetected = false;
@@ -7219,13 +7364,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7219
7364
  await this.flow.startKeepAlive(this.api);
7220
7365
  this.nativeFanout = new NativeStreamFanout({
7221
7366
  maxQueueItems: 200,
7222
- createSource: () => createNativeStream(this.api, this.channel, this.profile, {
7367
+ createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
7223
7368
  variant: this.variant,
7224
- ...dedicatedClient ? { client: dedicatedClient } : {}
7369
+ ...dedicatedClient ? { client: dedicatedClient } : {},
7370
+ signal
7225
7371
  }),
7226
7372
  onFrame: (frame) => {
7227
7373
  if (frame.audio) {
7228
- if (!this.hasAudio && this.api.client.getTransport() === "tcp" && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
7374
+ if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
7229
7375
  const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
7230
7376
  if (info) {
7231
7377
  this.hasAudio = true;
@@ -7274,6 +7420,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7274
7420
  onEnd: () => {
7275
7421
  if (!this.nativeStreamActive) return;
7276
7422
  this.nativeStreamActive = false;
7423
+ this.clearNoFrameDeadlineTimer();
7424
+ const hadFrames = this.firstFrameReceived;
7277
7425
  this.firstFrameReceived = false;
7278
7426
  this.firstFramePromise = null;
7279
7427
  this.firstFrameResolve = null;
@@ -7298,7 +7446,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7298
7446
  } catch {
7299
7447
  }
7300
7448
  }
7301
- if (this.connectedClients.size > 0) {
7449
+ if (this.connectedClients.size > 0 && hadFrames) {
7302
7450
  this.logger.info(
7303
7451
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
7304
7452
  );
@@ -7310,6 +7458,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7310
7458
  }
7311
7459
  });
7312
7460
  this.nativeFanout.start();
7461
+ this.clearNoFrameDeadlineTimer();
7462
+ if (this.nativeStreamNoFrameDeadlineMs > 0) {
7463
+ this.noFrameDeadlineTimer = setTimeout(() => {
7464
+ this.noFrameDeadlineTimer = void 0;
7465
+ if (!this.firstFrameReceived && this.nativeStreamActive) {
7466
+ this.logger.info(
7467
+ `[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
7468
+ );
7469
+ void this.stopNativeStream();
7470
+ }
7471
+ }, this.nativeStreamNoFrameDeadlineMs);
7472
+ this.noFrameDeadlineTimer?.unref?.();
7473
+ }
7313
7474
  this.clearNoClientAutoStopTimer();
7314
7475
  if (this.nativeStreamPrimeIdleStopMs > 0) {
7315
7476
  this.noClientAutoStopTimer = setTimeout(() => {
@@ -7326,6 +7487,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7326
7487
  markFirstFrameReceived() {
7327
7488
  if (!this.firstFrameReceived && this.firstFrameResolve) {
7328
7489
  this.firstFrameReceived = true;
7490
+ this.clearNoFrameDeadlineTimer();
7329
7491
  this.rtspDebugLog(
7330
7492
  `First frame received from camera for profile ${this.profile}`
7331
7493
  );
@@ -7352,6 +7514,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7352
7514
  );
7353
7515
  this.flow.stopKeepAlive();
7354
7516
  this.clearNoClientAutoStopTimer();
7517
+ this.clearNoFrameDeadlineTimer();
7355
7518
  this.nativeStreamActive = false;
7356
7519
  this.firstFrameReceived = false;
7357
7520
  this.firstFramePromise = null;
@@ -7561,6 +7724,249 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7561
7724
  }
7562
7725
  };
7563
7726
 
7727
+ // src/baichuan/stream/MpegTsMuxer.ts
7728
+ var TS_PACKET_SIZE = 188;
7729
+ var TS_SYNC_BYTE = 71;
7730
+ var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
7731
+ var PID_PAT = 0;
7732
+ var PID_PMT = 4096;
7733
+ var PID_VIDEO = 256;
7734
+ var PID_AUDIO = 257;
7735
+ var STREAM_TYPE_H264 = 27;
7736
+ var STREAM_TYPE_H265 = 36;
7737
+ var STREAM_TYPE_AAC = 15;
7738
+ var PES_STREAM_ID_VIDEO = 224;
7739
+ var PES_STREAM_ID_AUDIO = 192;
7740
+ var PAT_PMT_INTERVAL = 40;
7741
+ function crc32Mpeg(data) {
7742
+ let crc = 4294967295;
7743
+ for (let i = 0; i < data.length; i++) {
7744
+ crc ^= data[i] << 24;
7745
+ for (let j = 0; j < 8; j++) {
7746
+ if (crc & 2147483648) {
7747
+ crc = (crc << 1 ^ 79764919) >>> 0;
7748
+ } else {
7749
+ crc = crc << 1 >>> 0;
7750
+ }
7751
+ }
7752
+ }
7753
+ return crc >>> 0;
7754
+ }
7755
+ function usToPts(us) {
7756
+ return Math.floor(us * 90 / 1e3) & 8589934591;
7757
+ }
7758
+ function encodePts(buf, offset, pts, prefix) {
7759
+ buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
7760
+ buf[offset + 1] = pts >>> 22 & 255;
7761
+ buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
7762
+ buf[offset + 3] = pts >>> 7 & 255;
7763
+ buf[offset + 4] = (pts & 127) << 1 | 1;
7764
+ }
7765
+ function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
7766
+ buf[0] = TS_SYNC_BYTE;
7767
+ buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
7768
+ buf[2] = pid & 255;
7769
+ buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
7770
+ }
7771
+ function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
7772
+ const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
7773
+ const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
7774
+ let pesOffset = 0;
7775
+ let outOffset = 0;
7776
+ let isFirst = true;
7777
+ while (pesOffset < pesData.length) {
7778
+ const remaining = pesData.length - pesOffset;
7779
+ const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
7780
+ outOffset += TS_PACKET_SIZE;
7781
+ if (remaining >= TS_PAYLOAD_SIZE) {
7782
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
7783
+ ccRef.cc = ccRef.cc + 1 & 15;
7784
+ pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
7785
+ pesOffset += TS_PAYLOAD_SIZE;
7786
+ } else {
7787
+ const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
7788
+ if (paddingNeeded === 1) {
7789
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
7790
+ ccRef.cc = ccRef.cc + 1 & 15;
7791
+ packet[4] = 0;
7792
+ pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
7793
+ } else {
7794
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
7795
+ ccRef.cc = ccRef.cc + 1 & 15;
7796
+ const adaptLen = paddingNeeded - 1;
7797
+ packet[4] = adaptLen;
7798
+ packet[5] = isFirst && isKeyframe ? 64 : 0;
7799
+ packet.fill(255, 6, 4 + paddingNeeded);
7800
+ pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
7801
+ }
7802
+ pesOffset += remaining;
7803
+ }
7804
+ isFirst = false;
7805
+ }
7806
+ return out;
7807
+ }
7808
+ function buildPat(cc) {
7809
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
7810
+ pkt[0] = TS_SYNC_BYTE;
7811
+ pkt[1] = 64 | PID_PAT >> 8 & 31;
7812
+ pkt[2] = PID_PAT & 255;
7813
+ pkt[3] = 16 | cc & 15;
7814
+ pkt[4] = 0;
7815
+ const sectionStart = 5;
7816
+ let i = sectionStart;
7817
+ pkt[i++] = 0;
7818
+ pkt[i++] = 176;
7819
+ pkt[i++] = 13;
7820
+ pkt[i++] = 0;
7821
+ pkt[i++] = 1;
7822
+ pkt[i++] = 193;
7823
+ pkt[i++] = 0;
7824
+ pkt[i++] = 0;
7825
+ pkt[i++] = 0;
7826
+ pkt[i++] = 1;
7827
+ pkt[i++] = 224 | PID_PMT >> 8 & 31;
7828
+ pkt[i++] = PID_PMT & 255;
7829
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
7830
+ pkt.writeUInt32BE(crc, i);
7831
+ return pkt;
7832
+ }
7833
+ function buildPmt(videoStreamType, includeAudio, cc) {
7834
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
7835
+ pkt[0] = TS_SYNC_BYTE;
7836
+ pkt[1] = 64 | PID_PMT >> 8 & 31;
7837
+ pkt[2] = PID_PMT & 255;
7838
+ pkt[3] = 16 | cc & 15;
7839
+ pkt[4] = 0;
7840
+ const sectionStart = 5;
7841
+ let i = sectionStart;
7842
+ pkt[i++] = 2;
7843
+ pkt[i++] = 176;
7844
+ const sectionLenPos = i;
7845
+ i += 1;
7846
+ pkt[i++] = 0;
7847
+ pkt[i++] = 1;
7848
+ pkt[i++] = 193;
7849
+ pkt[i++] = 0;
7850
+ pkt[i++] = 0;
7851
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
7852
+ pkt[i++] = PID_VIDEO & 255;
7853
+ pkt[i++] = 240;
7854
+ pkt[i++] = 0;
7855
+ pkt[i++] = videoStreamType;
7856
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
7857
+ pkt[i++] = PID_VIDEO & 255;
7858
+ pkt[i++] = 240;
7859
+ pkt[i++] = 0;
7860
+ if (includeAudio) {
7861
+ pkt[i++] = STREAM_TYPE_AAC;
7862
+ pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
7863
+ pkt[i++] = PID_AUDIO & 255;
7864
+ pkt[i++] = 240;
7865
+ pkt[i++] = 0;
7866
+ }
7867
+ const sectionLen = i - sectionStart - 3 + 4;
7868
+ pkt[sectionLenPos] = sectionLen;
7869
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
7870
+ pkt.writeUInt32BE(crc, i);
7871
+ return pkt;
7872
+ }
7873
+ function buildVideoPes(annexBData, ptsUs, isKeyframe) {
7874
+ const pts = usToPts(ptsUs);
7875
+ const pesHeader = Buffer.allocUnsafe(14);
7876
+ pesHeader[0] = 0;
7877
+ pesHeader[1] = 0;
7878
+ pesHeader[2] = 1;
7879
+ pesHeader[3] = PES_STREAM_ID_VIDEO;
7880
+ pesHeader[4] = 0;
7881
+ pesHeader[5] = 0;
7882
+ pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
7883
+ pesHeader[7] = 128;
7884
+ pesHeader[8] = 5;
7885
+ encodePts(pesHeader, 9, pts, 2);
7886
+ return Buffer.concat([pesHeader, annexBData]);
7887
+ }
7888
+ function buildAudioPes(adtsData, ptsUs) {
7889
+ const pts = usToPts(ptsUs);
7890
+ const pesPayloadLen = 8 + adtsData.length;
7891
+ const pesHeader = Buffer.allocUnsafe(14);
7892
+ pesHeader[0] = 0;
7893
+ pesHeader[1] = 0;
7894
+ pesHeader[2] = 1;
7895
+ pesHeader[3] = PES_STREAM_ID_AUDIO;
7896
+ pesHeader[4] = pesPayloadLen >> 8 & 255;
7897
+ pesHeader[5] = pesPayloadLen & 255;
7898
+ pesHeader[6] = 128;
7899
+ pesHeader[7] = 128;
7900
+ pesHeader[8] = 5;
7901
+ encodePts(pesHeader, 9, pts, 2);
7902
+ return Buffer.concat([pesHeader, adtsData]);
7903
+ }
7904
+ var MpegTsMuxer = class {
7905
+ videoStreamType;
7906
+ includeAudio;
7907
+ // Per-instance continuity counters (4-bit, wrap at 16)
7908
+ patCc = 0;
7909
+ pmtCc = 0;
7910
+ videoCc = 0;
7911
+ audioCc = 0;
7912
+ framesSinceTableSend = 0;
7913
+ tablesSent = false;
7914
+ constructor(options) {
7915
+ this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
7916
+ this.includeAudio = options.includeAudio ?? true;
7917
+ }
7918
+ /**
7919
+ * Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
7920
+ * PAT and PMT are emitted before keyframes and periodically.
7921
+ *
7922
+ * @param annexBData - Annex-B video data (with start codes)
7923
+ * @param ptsUs - Presentation timestamp in microseconds
7924
+ * @param isKeyframe - Whether this is an IDR / IRAP frame
7925
+ */
7926
+ muxVideo(annexBData, ptsUs, isKeyframe) {
7927
+ const chunks = [];
7928
+ const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
7929
+ if (needTables) {
7930
+ chunks.push(buildPat(this.patCc));
7931
+ this.patCc = this.patCc + 1 & 15;
7932
+ chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
7933
+ this.pmtCc = this.pmtCc + 1 & 15;
7934
+ this.tablesSent = true;
7935
+ this.framesSinceTableSend = 0;
7936
+ }
7937
+ this.framesSinceTableSend++;
7938
+ const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
7939
+ const ccRef = { cc: this.videoCc };
7940
+ chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
7941
+ this.videoCc = ccRef.cc;
7942
+ return Buffer.concat(chunks);
7943
+ }
7944
+ /**
7945
+ * Mux an audio frame (ADTS AAC) into MPEG-TS packets.
7946
+ * Returns an empty Buffer when includeAudio is false.
7947
+ *
7948
+ * @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
7949
+ * @param ptsUs - Presentation timestamp in microseconds
7950
+ */
7951
+ muxAudio(adtsData, ptsUs) {
7952
+ if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
7953
+ const pes = buildAudioPes(adtsData, ptsUs);
7954
+ const ccRef = { cc: this.audioCc };
7955
+ const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
7956
+ this.audioCc = ccRef.cc;
7957
+ return result;
7958
+ }
7959
+ /** Reset all continuity counters and table state (e.g. after stream restart). */
7960
+ reset() {
7961
+ this.patCc = 0;
7962
+ this.pmtCc = 0;
7963
+ this.videoCc = 0;
7964
+ this.audioCc = 0;
7965
+ this.framesSinceTableSend = 0;
7966
+ this.tablesSent = false;
7967
+ }
7968
+ };
7969
+
7564
7970
  // src/reolink/baichuan/capabilities.ts
7565
7971
  function toNumberOrUndefined(value) {
7566
7972
  if (value == null) return void 0;
@@ -7774,214 +8180,59 @@ function xmlIndicatesFloodlight(xml) {
7774
8180
  return false;
7775
8181
  }
7776
8182
 
8183
+ // src/reolink/baichuan/utils/sleepInference.ts
8184
+ function decideSleepInferenceTransition(input) {
8185
+ const { inferred, committed, pending, hysteresisPolls } = input;
8186
+ if (committed === void 0) {
8187
+ return {
8188
+ emit: inferred === "sleeping" ? "sleeping" : null,
8189
+ nextCommitted: inferred,
8190
+ nextPending: void 0
8191
+ };
8192
+ }
8193
+ if (inferred === committed) {
8194
+ return {
8195
+ emit: null,
8196
+ nextCommitted: committed,
8197
+ nextPending: void 0
8198
+ };
8199
+ }
8200
+ const effectivePolls = Math.max(1, hysteresisPolls);
8201
+ if (!pending || pending.state !== inferred) {
8202
+ if (effectivePolls <= 1) {
8203
+ return {
8204
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
8205
+ nextCommitted: inferred,
8206
+ nextPending: void 0
8207
+ };
8208
+ }
8209
+ return {
8210
+ emit: null,
8211
+ nextCommitted: committed,
8212
+ nextPending: { state: inferred, count: 1 }
8213
+ };
8214
+ }
8215
+ const nextCount = pending.count + 1;
8216
+ if (nextCount < effectivePolls) {
8217
+ return {
8218
+ emit: null,
8219
+ nextCommitted: committed,
8220
+ nextPending: { state: inferred, count: nextCount }
8221
+ };
8222
+ }
8223
+ return {
8224
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
8225
+ nextCommitted: inferred,
8226
+ nextPending: void 0
8227
+ };
8228
+ }
8229
+
7777
8230
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
7778
8231
  import { spawn as spawn2 } from "child_process";
7779
8232
  import { mkdir } from "fs/promises";
7780
8233
  import { dirname } from "path";
7781
8234
  import { PassThrough } from "stream";
7782
8235
 
7783
- // src/baichuan/stream/MpegTsMuxer.ts
7784
- var TS_PACKET_SIZE = 188;
7785
- var TS_SYNC_BYTE = 71;
7786
- var PAT_PID = 0;
7787
- var PMT_PID = 4096;
7788
- var VIDEO_PID = 256;
7789
- var STREAM_TYPE_H264 = 27;
7790
- var STREAM_TYPE_H265 = 36;
7791
- var patCc = 0;
7792
- var pmtCc = 0;
7793
- var videoCc = 0;
7794
- function createPat() {
7795
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
7796
- packet[0] = TS_SYNC_BYTE;
7797
- packet[1] = 64 | PAT_PID >> 8 & 31;
7798
- packet[2] = PAT_PID & 255;
7799
- packet[3] = 16 | patCc & 15;
7800
- patCc = patCc + 1 & 15;
7801
- packet[4] = 0;
7802
- let idx = 5;
7803
- packet[idx++] = 0;
7804
- packet[idx++] = 176;
7805
- packet[idx++] = 13;
7806
- packet[idx++] = 0;
7807
- packet[idx++] = 1;
7808
- packet[idx++] = 193;
7809
- packet[idx++] = 0;
7810
- packet[idx++] = 0;
7811
- packet[idx++] = 0;
7812
- packet[idx++] = 1;
7813
- packet[idx++] = 224 | PMT_PID >> 8 & 31;
7814
- packet[idx++] = PMT_PID & 255;
7815
- const crc = crc32Mpeg(packet.subarray(5, idx));
7816
- packet.writeUInt32BE(crc, idx);
7817
- return packet;
7818
- }
7819
- function createPmt(streamType) {
7820
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
7821
- packet[0] = TS_SYNC_BYTE;
7822
- packet[1] = 64 | PMT_PID >> 8 & 31;
7823
- packet[2] = PMT_PID & 255;
7824
- packet[3] = 16 | pmtCc & 15;
7825
- pmtCc = pmtCc + 1 & 15;
7826
- packet[4] = 0;
7827
- let idx = 5;
7828
- packet[idx++] = 2;
7829
- packet[idx++] = 176;
7830
- packet[idx++] = 18;
7831
- packet[idx++] = 0;
7832
- packet[idx++] = 1;
7833
- packet[idx++] = 193;
7834
- packet[idx++] = 0;
7835
- packet[idx++] = 0;
7836
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
7837
- packet[idx++] = VIDEO_PID & 255;
7838
- packet[idx++] = 240;
7839
- packet[idx++] = 0;
7840
- packet[idx++] = streamType;
7841
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
7842
- packet[idx++] = VIDEO_PID & 255;
7843
- packet[idx++] = 240;
7844
- packet[idx++] = 0;
7845
- const crc = crc32Mpeg(packet.subarray(5, idx));
7846
- packet.writeUInt32BE(crc, idx);
7847
- return packet;
7848
- }
7849
- function createVideoPes(data, pts, isKeyframe) {
7850
- const packets = [];
7851
- const pts90k = Math.floor(pts * 9e4 / 1e6);
7852
- const pesHeaderLen = 14;
7853
- const pesHeader = Buffer.alloc(pesHeaderLen);
7854
- let idx = 0;
7855
- pesHeader[idx++] = 0;
7856
- pesHeader[idx++] = 0;
7857
- pesHeader[idx++] = 1;
7858
- pesHeader[idx++] = 224;
7859
- pesHeader[idx++] = 0;
7860
- pesHeader[idx++] = 0;
7861
- pesHeader[idx++] = 128;
7862
- pesHeader[idx++] = 128;
7863
- pesHeader[idx++] = 5;
7864
- pesHeader[idx++] = 33 | pts90k >> 29 & 14;
7865
- pesHeader[idx++] = pts90k >> 22 & 255;
7866
- pesHeader[idx++] = 1 | pts90k >> 14 & 254;
7867
- pesHeader[idx++] = pts90k >> 7 & 255;
7868
- pesHeader[idx++] = 1 | pts90k << 1 & 254;
7869
- const pesData = Buffer.concat([pesHeader, data]);
7870
- let pesOffset = 0;
7871
- let isFirst = true;
7872
- while (pesOffset < pesData.length) {
7873
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
7874
- let pktIdx = 0;
7875
- packet[pktIdx++] = TS_SYNC_BYTE;
7876
- packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
7877
- packet[pktIdx++] = VIDEO_PID & 255;
7878
- const remaining = pesData.length - pesOffset;
7879
- const maxPayload = TS_PACKET_SIZE - 4;
7880
- if (remaining >= maxPayload) {
7881
- packet[pktIdx++] = 16 | videoCc & 15;
7882
- videoCc = videoCc + 1 & 15;
7883
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
7884
- pesOffset += maxPayload;
7885
- } else {
7886
- const adaptLen = maxPayload - remaining - 1;
7887
- if (adaptLen < 0) {
7888
- packet[pktIdx++] = 48 | videoCc & 15;
7889
- videoCc = videoCc + 1 & 15;
7890
- packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
7891
- if (isFirst && isKeyframe) {
7892
- packet[pktIdx++] = 64;
7893
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
7894
- packet[i] = 255;
7895
- }
7896
- } else {
7897
- packet[pktIdx++] = 0;
7898
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
7899
- packet[i] = 255;
7900
- }
7901
- }
7902
- pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
7903
- pesOffset += remaining;
7904
- } else {
7905
- packet[pktIdx++] = 48 | videoCc & 15;
7906
- videoCc = videoCc + 1 & 15;
7907
- if (adaptLen === 0) {
7908
- packet[pktIdx++] = 0;
7909
- } else {
7910
- packet[pktIdx++] = adaptLen;
7911
- if (isFirst && isKeyframe) {
7912
- packet[pktIdx++] = 64;
7913
- } else {
7914
- packet[pktIdx++] = 0;
7915
- }
7916
- for (let i = 0; i < adaptLen - 1; i++) {
7917
- packet[pktIdx++] = 255;
7918
- }
7919
- }
7920
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
7921
- pesOffset += remaining;
7922
- }
7923
- }
7924
- packets.push(packet);
7925
- isFirst = false;
7926
- }
7927
- return packets;
7928
- }
7929
- function crc32Mpeg(data) {
7930
- let crc = 4294967295;
7931
- for (let i = 0; i < data.length; i++) {
7932
- crc ^= data[i] << 24;
7933
- for (let j = 0; j < 8; j++) {
7934
- if (crc & 2147483648) {
7935
- crc = (crc << 1 ^ 79764919) >>> 0;
7936
- } else {
7937
- crc = crc << 1 >>> 0;
7938
- }
7939
- }
7940
- }
7941
- return crc >>> 0;
7942
- }
7943
- var MpegTsMuxer = class {
7944
- streamType;
7945
- patSent = false;
7946
- pmtSent = false;
7947
- patPmtInterval = 0;
7948
- patPmtIntervalMax = 40;
7949
- // Send PAT/PMT every ~40 frames
7950
- constructor(options) {
7951
- this.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
7952
- }
7953
- /**
7954
- * Reset continuity counters (call when starting a new stream).
7955
- */
7956
- static resetCounters() {
7957
- patCc = 0;
7958
- pmtCc = 0;
7959
- videoCc = 0;
7960
- }
7961
- /**
7962
- * Mux a video frame into MPEG-TS packets.
7963
- *
7964
- * @param data - Annex-B video data (with start codes)
7965
- * @param microseconds - Frame timestamp in microseconds
7966
- * @param isKeyframe - Whether this is a keyframe
7967
- * @returns Buffer containing all TS packets for this frame
7968
- */
7969
- mux(data, microseconds, isKeyframe) {
7970
- const packets = [];
7971
- if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
7972
- packets.push(createPat());
7973
- packets.push(createPmt(this.streamType));
7974
- this.patSent = true;
7975
- this.pmtSent = true;
7976
- this.patPmtInterval = 0;
7977
- }
7978
- this.patPmtInterval++;
7979
- const videoPackets = createVideoPes(data, microseconds, isKeyframe);
7980
- packets.push(...videoPackets);
7981
- return Buffer.concat(packets);
7982
- }
7983
- };
7984
-
7985
8236
  // src/reolink/baichuan/utils/xml.ts
7986
8237
  import { XMLParser } from "fast-xml-parser";
7987
8238
  var parser = new XMLParser({
@@ -10437,7 +10688,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10437
10688
  statePollingInterval;
10438
10689
  udpSleepInferenceInterval;
10439
10690
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
10691
+ /**
10692
+ * Per-channel pending sleep-state candidate for hysteresis.
10693
+ * When the inference flips to a new state we require N consecutive polls
10694
+ * of that same state before committing it — this filters out transient
10695
+ * flapping caused by non-waking traffic drifting in/out of the 10 s
10696
+ * getSleepStatus() observation window during stream teardown.
10697
+ */
10698
+ udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
10440
10699
  udpSleepInferenceIntervalMs = 2e3;
10700
+ /** Consecutive inference polls required to commit a new sleeping/awake state. */
10701
+ udpSleepInferenceHysteresisPolls = 2;
10441
10702
  lastMotionState;
10442
10703
  lastAiState;
10443
10704
  aiStatePollingDisabled = false;
@@ -10904,6 +11165,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10904
11165
  */
10905
11166
  attachD2cDiscListener(client) {
10906
11167
  client.on("d2c_disc", () => this.notifyD2cDisc());
11168
+ client.on("error", () => {
11169
+ });
10907
11170
  }
10908
11171
  /**
10909
11172
  * Acquire a socket from the pool by tag.
@@ -11022,6 +11285,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11022
11285
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
11023
11286
  const newClient = new BaichuanClient(clientOpts);
11024
11287
  this.attachD2cDiscListener(newClient);
11288
+ newClient.on("error", (err) => {
11289
+ log?.debug?.(
11290
+ `[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
11291
+ );
11292
+ });
11025
11293
  await newClient.login();
11026
11294
  const existingCooldown = this.socketPoolCooldowns.get(this.host);
11027
11295
  if (existingCooldown) {
@@ -11537,6 +11805,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11537
11805
  * Only counts sessions from our own IP address.
11538
11806
  */
11539
11807
  async maybeRebootOnTooManySessions() {
11808
+ if (!this.client.isSocketConnected?.()) return;
11540
11809
  const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
11541
11810
  if (this.sessionGuardRebootInFlight) return;
11542
11811
  const cooldownMs = 10 * 6e4;
@@ -11978,6 +12247,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11978
12247
  }
11979
12248
  async renewSimpleEventSubscription() {
11980
12249
  if (this.simpleEventListeners.size === 0) return;
12250
+ if (!this.client.isSocketConnected?.()) return;
11981
12251
  if (this.simpleEventResubscribeInFlight)
11982
12252
  return await this.simpleEventResubscribeInFlight;
11983
12253
  this.simpleEventResubscribeInFlight = (async () => {
@@ -15745,23 +16015,32 @@ ${stderr}`)
15745
16015
  return;
15746
16016
  }
15747
16017
  const channel = this.client.getConfiguredChannel?.() ?? 0;
16018
+ if (!this.client.isSocketConnected?.()) {
16019
+ this.udpPendingSleepStateByChannel.delete(channel);
16020
+ return;
16021
+ }
15748
16022
  const status = this.getSleepStatus({ channel });
15749
16023
  if (status.state === "unknown") return;
15750
- const prev = this.udpLastInferredSleepStateByChannel.get(channel);
15751
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
15752
- if (prev === void 0) {
15753
- if (status.state === "sleeping") {
15754
- this.dispatchSimpleEvent({
15755
- type: "sleeping",
15756
- channel,
15757
- timestamp: Date.now()
15758
- });
15759
- }
15760
- return;
16024
+ const committed = this.udpLastInferredSleepStateByChannel.get(channel);
16025
+ const pending = this.udpPendingSleepStateByChannel.get(channel);
16026
+ const decision = decideSleepInferenceTransition({
16027
+ inferred: status.state,
16028
+ committed,
16029
+ pending,
16030
+ hysteresisPolls: this.udpSleepInferenceHysteresisPolls
16031
+ });
16032
+ this.udpLastInferredSleepStateByChannel.set(
16033
+ channel,
16034
+ decision.nextCommitted
16035
+ );
16036
+ if (decision.nextPending === void 0) {
16037
+ this.udpPendingSleepStateByChannel.delete(channel);
16038
+ } else {
16039
+ this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
15761
16040
  }
15762
- if (prev !== status.state) {
16041
+ if (decision.emit) {
15763
16042
  this.dispatchSimpleEvent({
15764
- type: status.state === "sleeping" ? "sleeping" : "awake",
16043
+ type: decision.emit,
15765
16044
  channel,
15766
16045
  timestamp: Date.now()
15767
16046
  });
@@ -15784,6 +16063,7 @@ ${stderr}`)
15784
16063
  this.udpSleepInferenceInterval = void 0;
15785
16064
  }
15786
16065
  this.udpLastInferredSleepStateByChannel.clear();
16066
+ this.udpPendingSleepStateByChannel.clear();
15787
16067
  }
15788
16068
  /**
15789
16069
  * GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
@@ -18180,7 +18460,7 @@ ${xml}`
18180
18460
  * @returns Test results for all stream types and profiles
18181
18461
  */
18182
18462
  async testChannelStreams(channel, logger) {
18183
- const { testChannelStreams } = await import("./DiagnosticsTools-UMN4C7SY.js");
18463
+ const { testChannelStreams } = await import("./DiagnosticsTools-HJDH4GPP.js");
18184
18464
  return await testChannelStreams({
18185
18465
  api: this,
18186
18466
  channel: this.normalizeChannel(channel),
@@ -18196,7 +18476,7 @@ ${xml}`
18196
18476
  * @returns Complete diagnostics for all channels and streams
18197
18477
  */
18198
18478
  async collectMultifocalDiagnostics(logger) {
18199
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-UMN4C7SY.js");
18479
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-HJDH4GPP.js");
18200
18480
  return await collectMultifocalDiagnostics({
18201
18481
  api: this,
18202
18482
  logger
@@ -19382,8 +19662,8 @@ ${scheduleItems}
19382
19662
  );
19383
19663
  let args;
19384
19664
  if (useMpegTsMuxer) {
19385
- MpegTsMuxer.resetCounters();
19386
- tsMuxer = new MpegTsMuxer({ videoType });
19665
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
19666
+ tsMuxer.reset();
19387
19667
  args = [
19388
19668
  "-hide_banner",
19389
19669
  "-loglevel",
@@ -19547,7 +19827,7 @@ ${scheduleItems}
19547
19827
  startFfmpeg(videoType);
19548
19828
  frameCount++;
19549
19829
  if (useMpegTsMuxer && tsMuxer) {
19550
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
19830
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
19551
19831
  input.write(tsData);
19552
19832
  } else {
19553
19833
  if (videoType === "H264") input.write(H264_AUD);
@@ -19836,8 +20116,8 @@ ${scheduleItems}
19836
20116
  logger?.log?.(
19837
20117
  `[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
19838
20118
  );
19839
- MpegTsMuxer.resetCounters();
19840
- tsMuxer = new MpegTsMuxer({ videoType });
20119
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
20120
+ tsMuxer.reset();
19841
20121
  const args = [
19842
20122
  "-hide_banner",
19843
20123
  "-loglevel",
@@ -20002,7 +20282,7 @@ ${scheduleItems}
20002
20282
  startFfmpeg(videoType);
20003
20283
  frameCount++;
20004
20284
  if (tsMuxer) {
20005
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
20285
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
20006
20286
  input.write(tsData);
20007
20287
  }
20008
20288
  if (frameCount === 1) {
@@ -21414,9 +21694,10 @@ async function autoDetectDeviceType(inputs) {
21414
21694
  const msg = fmtErr(e);
21415
21695
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
21416
21696
  };
21417
- const withRetries = async (label, max, op, shouldRetry) => {
21697
+ const withRetries = async (label, max, op, shouldRetry, isAborted) => {
21418
21698
  let lastErr;
21419
21699
  for (let attempt = 1; attempt <= max; attempt++) {
21700
+ if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
21420
21701
  try {
21421
21702
  if (attempt > 1) {
21422
21703
  logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
@@ -21425,7 +21706,7 @@ async function autoDetectDeviceType(inputs) {
21425
21706
  } catch (e) {
21426
21707
  lastErr = e;
21427
21708
  const msg = fmtErr(e);
21428
- const retryable = attempt < max && shouldRetry(e);
21709
+ const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
21429
21710
  logger?.log?.(
21430
21711
  `[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
21431
21712
  );
@@ -21435,6 +21716,31 @@ async function autoDetectDeviceType(inputs) {
21435
21716
  }
21436
21717
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
21437
21718
  };
21719
+ const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
21720
+ let raceWon = false;
21721
+ const methodErrors = /* @__PURE__ */ new Map();
21722
+ try {
21723
+ return await Promise.any(
21724
+ methods.map(async (m) => {
21725
+ try {
21726
+ const result = await loginAndDetect(m, () => raceWon);
21727
+ raceWon = true;
21728
+ return result;
21729
+ } catch (e) {
21730
+ if (!raceWon) methodErrors.set(m, fmtErr(e));
21731
+ logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
21732
+ throw e;
21733
+ }
21734
+ })
21735
+ );
21736
+ } catch (e) {
21737
+ if (e instanceof AggregateError) {
21738
+ const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
21739
+ throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
21740
+ }
21741
+ throw e;
21742
+ }
21743
+ };
21438
21744
  const effectiveUid = normalizeUid(uid);
21439
21745
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
21440
21746
  const isReachable = await pingHost(host);
@@ -21464,9 +21770,9 @@ async function autoDetectDeviceType(inputs) {
21464
21770
  normalizedUid = normalizedDiscovered;
21465
21771
  }
21466
21772
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
21467
- const udpErrors = [];
21468
- for (const m of methodsToTry) {
21469
- try {
21773
+ return await runUdpMethodsParallel(
21774
+ methodsToTry,
21775
+ async (m, isAborted) => {
21470
21776
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
21471
21777
  const udpApi = await withRetries(
21472
21778
  `UDP(${m})`,
@@ -21489,11 +21795,14 @@ async function autoDetectDeviceType(inputs) {
21489
21795
  throw e;
21490
21796
  }
21491
21797
  },
21492
- shouldRetryUdp
21798
+ shouldRetryUdp,
21799
+ isAborted
21493
21800
  );
21494
- const deviceInfo = await udpApi.getInfo();
21495
- const capabilities = await udpApi.getDeviceCapabilities();
21496
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
21801
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
21802
+ udpApi.getInfo(),
21803
+ udpApi.getDeviceCapabilities(),
21804
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
21805
+ ]);
21497
21806
  const channelNum = capabilities?.support?.channelNum ?? 1;
21498
21807
  const model = deviceInfo.type?.trim();
21499
21808
  const normalizedModel = model ? model.trim() : void 0;
@@ -21532,14 +21841,8 @@ async function autoDetectDeviceType(inputs) {
21532
21841
  channelNum: 1,
21533
21842
  api: udpApi
21534
21843
  };
21535
- } catch (e) {
21536
- const msg = fmtErr(e);
21537
- udpErrors.push(`${m}: ${msg}`);
21538
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
21539
- }
21540
- }
21541
- throw new Error(
21542
- `Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
21844
+ },
21845
+ "Forced UDP autodetect failed for all methods."
21543
21846
  );
21544
21847
  }
21545
21848
  let tcpApi;
@@ -21592,54 +21895,57 @@ async function autoDetectDeviceType(inputs) {
21592
21895
  }
21593
21896
  return void 0;
21594
21897
  };
21595
- const infoProbe = await runProbeVariants(
21596
- "getInfo",
21597
- [
21898
+ const [infoProbe, supportProbe] = await Promise.all([
21899
+ runProbeVariants(
21900
+ "getInfo",
21901
+ [
21902
+ {
21903
+ variant: "cmd80 class=0x6414",
21904
+ op: () => api.getInfo(void 0, {
21905
+ timeoutMs: 2500,
21906
+ messageClass: BC_CLASS_MODERN_24
21907
+ })
21908
+ },
21909
+ {
21910
+ variant: "cmd80 class=0x6614",
21911
+ op: () => api.getInfo(void 0, {
21912
+ timeoutMs: 3e3,
21913
+ messageClass: BC_CLASS_MODERN_20
21914
+ })
21915
+ },
21916
+ {
21917
+ variant: "cmd318(ch0) class=0x6414",
21918
+ op: () => api.getInfo(0, {
21919
+ timeoutMs: 3e3,
21920
+ messageClass: BC_CLASS_MODERN_24
21921
+ })
21922
+ },
21923
+ {
21924
+ variant: "cmd318(ch0) class=0x6614",
21925
+ op: () => api.getInfo(0, {
21926
+ timeoutMs: 3500,
21927
+ messageClass: BC_CLASS_MODERN_20
21928
+ })
21929
+ }
21930
+ ]
21931
+ ),
21932
+ // Support probes (cmd 199). Some firmwares may not support it or are slow.
21933
+ runProbeVariants("getSupportInfo", [
21598
21934
  {
21599
- variant: "cmd80 class=0x6414",
21600
- op: () => api.getInfo(void 0, {
21935
+ variant: "cmd199 class=0x6414",
21936
+ op: () => api.getSupportInfo({
21601
21937
  timeoutMs: 2500,
21602
21938
  messageClass: BC_CLASS_MODERN_24
21603
21939
  })
21604
21940
  },
21605
21941
  {
21606
- variant: "cmd80 class=0x6614",
21607
- op: () => api.getInfo(void 0, {
21608
- timeoutMs: 3e3,
21609
- messageClass: BC_CLASS_MODERN_20
21610
- })
21611
- },
21612
- {
21613
- variant: "cmd318(ch0) class=0x6414",
21614
- op: () => api.getInfo(0, {
21615
- timeoutMs: 3e3,
21616
- messageClass: BC_CLASS_MODERN_24
21617
- })
21618
- },
21619
- {
21620
- variant: "cmd318(ch0) class=0x6614",
21621
- op: () => api.getInfo(0, {
21942
+ variant: "cmd199 class=0x6614",
21943
+ op: () => api.getSupportInfo({
21622
21944
  timeoutMs: 3500,
21623
21945
  messageClass: BC_CLASS_MODERN_20
21624
21946
  })
21625
21947
  }
21626
- ]
21627
- );
21628
- const supportProbe = await runProbeVariants("getSupportInfo", [
21629
- {
21630
- variant: "cmd199 class=0x6414",
21631
- op: () => api.getSupportInfo({
21632
- timeoutMs: 2500,
21633
- messageClass: BC_CLASS_MODERN_24
21634
- })
21635
- },
21636
- {
21637
- variant: "cmd199 class=0x6614",
21638
- op: () => api.getSupportInfo({
21639
- timeoutMs: 3500,
21640
- messageClass: BC_CLASS_MODERN_20
21641
- })
21642
- }
21948
+ ])
21643
21949
  ]);
21644
21950
  const deviceInfo = infoProbe?.value;
21645
21951
  const support = supportProbe?.value;
@@ -21731,9 +22037,11 @@ async function autoDetectDeviceType(inputs) {
21731
22037
  }
21732
22038
  try {
21733
22039
  const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
21734
- const deviceInfo = await udpApi.getInfo();
21735
- const capabilities = await udpApi.getDeviceCapabilities();
21736
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
22040
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
22041
+ udpApi.getInfo(),
22042
+ udpApi.getDeviceCapabilities(),
22043
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
22044
+ ]);
21737
22045
  const channelNum = capabilities?.support?.channelNum ?? 1;
21738
22046
  const model = deviceInfo.type?.trim();
21739
22047
  const normalizedModel = model ? model.trim() : void 0;
@@ -21777,21 +22085,17 @@ async function autoDetectDeviceType(inputs) {
21777
22085
  };
21778
22086
  };
21779
22087
  const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
21780
- const udpErrors = [];
21781
- for (const m of methodsToTry) {
21782
- try {
22088
+ const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
22089
+ return await runUdpMethodsParallel(
22090
+ viableMethods,
22091
+ async (m, isAborted) => {
21783
22092
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
21784
22093
  const udpApi = await withRetries(
21785
22094
  `UDP(${m})`,
21786
22095
  maxRetries,
21787
22096
  async (attempt) => {
21788
- const apiInputs = {
21789
- ...inputs,
21790
- udpDiscoveryMethod: m
21791
- };
21792
- if (normalizedUid) {
21793
- apiInputs.uid = normalizedUid;
21794
- }
22097
+ const apiInputs = { ...inputs, udpDiscoveryMethod: m };
22098
+ if (normalizedUid) apiInputs.uid = normalizedUid;
21795
22099
  const api = createBaichuanApi(apiInputs, "udp");
21796
22100
  try {
21797
22101
  await api.login();
@@ -21806,20 +22110,12 @@ async function autoDetectDeviceType(inputs) {
21806
22110
  throw e;
21807
22111
  }
21808
22112
  },
21809
- shouldRetryUdp
22113
+ shouldRetryUdp,
22114
+ isAborted
21810
22115
  );
21811
- return await detectOverUdpApi(udpApi, m);
21812
- } catch (e) {
21813
- const msg = e?.message || e?.toString?.() || String(e);
21814
- udpErrors.push(`${m}: ${msg}`);
21815
- try {
21816
- } catch {
21817
- }
21818
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
21819
- }
21820
- }
21821
- throw new Error(
21822
- `UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
22116
+ return detectOverUdpApi(udpApi, m);
22117
+ },
22118
+ "UDP discovery failed for all methods."
21823
22119
  );
21824
22120
  } catch (udpError) {
21825
22121
  logger?.log?.(
@@ -21851,12 +22147,14 @@ export {
21851
22147
  BaichuanEventEmitter,
21852
22148
  createNativeStream,
21853
22149
  BaichuanRtspServer,
22150
+ MpegTsMuxer,
21854
22151
  flattenAbilitiesForChannel,
21855
22152
  abilitiesHasAny,
21856
22153
  parseSupportXml,
21857
22154
  getSupportItemForChannel,
21858
22155
  computeDeviceCapabilities,
21859
22156
  xmlIndicatesFloodlight,
22157
+ decideSleepInferenceTransition,
21860
22158
  DUAL_LENS_DUAL_MOTION_MODELS,
21861
22159
  DUAL_LENS_SINGLE_MOTION_MODELS,
21862
22160
  DUAL_LENS_MODELS,
@@ -21878,4 +22176,4 @@ export {
21878
22176
  isTcpFailureThatShouldFallbackToUdp,
21879
22177
  autoDetectDeviceType
21880
22178
  };
21881
- //# sourceMappingURL=chunk-GKLOJJ34.js.map
22179
+ //# sourceMappingURL=chunk-VBYF3BQX.js.map