@apocaliss92/nodelink-js 0.4.6 → 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({
@@ -10130,6 +10381,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10130
10381
  * - "replay:XXX" - dedicated per replay session
10131
10382
  */
10132
10383
  socketPool = /* @__PURE__ */ new Map();
10384
+ /**
10385
+ * Consecutive stream-start (cmdId=3) timeout counter per socket tag.
10386
+ * When a streaming socket has N consecutive timeouts, the socket is force-closed
10387
+ * so the next attempt creates a fresh connection. Resets on success.
10388
+ */
10389
+ consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
10390
+ static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
10133
10391
  /** BaichuanClientOptions to use when creating new sockets */
10134
10392
  clientOptions;
10135
10393
  /**
@@ -10284,14 +10542,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10284
10542
  if (!xml) return;
10285
10543
  const channel = frame.header.channelId;
10286
10544
  const battery = this.parseBatteryInfoXml(xml, channel);
10287
- if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
10288
- this.dispatchSimpleEvent({
10289
- type: "battery",
10290
- channel,
10291
- timestamp: Date.now(),
10292
- battery
10293
- });
10545
+ if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
10546
+ return;
10547
+ }
10548
+ const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
10549
+ if (this.lastBatteryPushKey.get(channel) === key) {
10550
+ return;
10294
10551
  }
10552
+ this.lastBatteryPushKey.set(channel, key);
10553
+ this.dispatchSimpleEvent({
10554
+ type: "battery",
10555
+ channel,
10556
+ timestamp: Date.now(),
10557
+ battery
10558
+ });
10295
10559
  } catch (e) {
10296
10560
  this.logger.debug?.(
10297
10561
  "[ReolinkBaichuanApi] Error parsing battery push",
@@ -10424,7 +10688,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10424
10688
  statePollingInterval;
10425
10689
  udpSleepInferenceInterval;
10426
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();
10427
10699
  udpSleepInferenceIntervalMs = 2e3;
10700
+ /** Consecutive inference polls required to commit a new sleeping/awake state. */
10701
+ udpSleepInferenceHysteresisPolls = 2;
10428
10702
  lastMotionState;
10429
10703
  lastAiState;
10430
10704
  aiStatePollingDisabled = false;
@@ -10457,6 +10731,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10457
10731
  deviceCapabilitiesCache = /* @__PURE__ */ new Map();
10458
10732
  static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
10459
10733
  // 5 minutes
10734
+ /**
10735
+ * Dedupe key for battery push events (cmd_id 252), per channel.
10736
+ * Cameras emit BatteryInfoList frequently while streaming (every few
10737
+ * seconds). We only forward an event when the meaningful fields change
10738
+ * (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
10739
+ * consumers and the UI event log.
10740
+ */
10741
+ lastBatteryPushKey = /* @__PURE__ */ new Map();
10460
10742
  // ─────────────────────────────────────────────────────────────────────────────
10461
10743
  // SOCKET POOL CONSTANTS
10462
10744
  // ─────────────────────────────────────────────────────────────────────────────
@@ -10883,6 +11165,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10883
11165
  */
10884
11166
  attachD2cDiscListener(client) {
10885
11167
  client.on("d2c_disc", () => this.notifyD2cDisc());
11168
+ client.on("error", () => {
11169
+ });
10886
11170
  }
10887
11171
  /**
10888
11172
  * Acquire a socket from the pool by tag.
@@ -11001,6 +11285,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11001
11285
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
11002
11286
  const newClient = new BaichuanClient(clientOpts);
11003
11287
  this.attachD2cDiscListener(newClient);
11288
+ newClient.on("error", (err) => {
11289
+ log?.debug?.(
11290
+ `[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
11291
+ );
11292
+ });
11004
11293
  await newClient.login();
11005
11294
  const existingCooldown = this.socketPoolCooldowns.get(this.host);
11006
11295
  if (existingCooldown) {
@@ -11516,6 +11805,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11516
11805
  * Only counts sessions from our own IP address.
11517
11806
  */
11518
11807
  async maybeRebootOnTooManySessions() {
11808
+ if (!this.client.isSocketConnected?.()) return;
11519
11809
  const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
11520
11810
  if (this.sessionGuardRebootInFlight) return;
11521
11811
  const cooldownMs = 10 * 6e4;
@@ -11957,6 +12247,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11957
12247
  }
11958
12248
  async renewSimpleEventSubscription() {
11959
12249
  if (this.simpleEventListeners.size === 0) return;
12250
+ if (!this.client.isSocketConnected?.()) return;
11960
12251
  if (this.simpleEventResubscribeInFlight)
11961
12252
  return await this.simpleEventResubscribeInFlight;
11962
12253
  this.simpleEventResubscribeInFlight = (async () => {
@@ -15724,23 +16015,32 @@ ${stderr}`)
15724
16015
  return;
15725
16016
  }
15726
16017
  const channel = this.client.getConfiguredChannel?.() ?? 0;
16018
+ if (!this.client.isSocketConnected?.()) {
16019
+ this.udpPendingSleepStateByChannel.delete(channel);
16020
+ return;
16021
+ }
15727
16022
  const status = this.getSleepStatus({ channel });
15728
16023
  if (status.state === "unknown") return;
15729
- const prev = this.udpLastInferredSleepStateByChannel.get(channel);
15730
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
15731
- if (prev === void 0) {
15732
- if (status.state === "sleeping") {
15733
- this.dispatchSimpleEvent({
15734
- type: "sleeping",
15735
- channel,
15736
- timestamp: Date.now()
15737
- });
15738
- }
15739
- 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);
15740
16040
  }
15741
- if (prev !== status.state) {
16041
+ if (decision.emit) {
15742
16042
  this.dispatchSimpleEvent({
15743
- type: status.state === "sleeping" ? "sleeping" : "awake",
16043
+ type: decision.emit,
15744
16044
  channel,
15745
16045
  timestamp: Date.now()
15746
16046
  });
@@ -15763,6 +16063,7 @@ ${stderr}`)
15763
16063
  this.udpSleepInferenceInterval = void 0;
15764
16064
  }
15765
16065
  this.udpLastInferredSleepStateByChannel.clear();
16066
+ this.udpPendingSleepStateByChannel.clear();
15766
16067
  }
15767
16068
  /**
15768
16069
  * GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
@@ -15929,6 +16230,7 @@ ${stderr}`)
15929
16230
  `${ch}:${profile}:${variant}`,
15930
16231
  frame.header.msgNum
15931
16232
  );
16233
+ this.resetStreamTimeoutCounter(targetClient);
15932
16234
  return;
15933
16235
  } catch (error) {
15934
16236
  lastError = error;
@@ -15943,6 +16245,10 @@ ${stderr}`)
15943
16245
  }
15944
16246
  }
15945
16247
  }
16248
+ const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
16249
+ if (isTimeout) {
16250
+ this.trackStreamTimeout(targetClient);
16251
+ }
15946
16252
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
15947
16253
  }
15948
16254
  /**
@@ -16402,6 +16708,18 @@ ${stderr}`)
16402
16708
  notifyD2cDisc() {
16403
16709
  const now = Date.now();
16404
16710
  this.lastD2cDiscAtMs = now;
16711
+ const streamingTags = Array.from(this.socketPool.keys()).filter(
16712
+ (tag) => tag.startsWith("streaming:")
16713
+ );
16714
+ if (streamingTags.length > 0) {
16715
+ this.logger?.log?.(
16716
+ `[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
16717
+ );
16718
+ for (const tag of streamingTags) {
16719
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
16720
+ });
16721
+ }
16722
+ }
16405
16723
  const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
16406
16724
  const existing = this.socketPoolCooldowns.get(this.host);
16407
16725
  if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
@@ -16434,6 +16752,43 @@ ${stderr}`)
16434
16752
  }
16435
16753
  }
16436
16754
  }
16755
+ /**
16756
+ * Find the socket pool tag for a given BaichuanClient instance.
16757
+ * Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
16758
+ */
16759
+ findSocketTagForClient(client) {
16760
+ for (const [tag, entry] of this.socketPool) {
16761
+ if (entry.client === client) return tag;
16762
+ }
16763
+ return void 0;
16764
+ }
16765
+ /**
16766
+ * Reset the consecutive stream-start timeout counter for a streaming socket.
16767
+ * Called on successful stream start.
16768
+ */
16769
+ resetStreamTimeoutCounter(client) {
16770
+ const tag = this.findSocketTagForClient(client);
16771
+ if (tag) this.consecutiveStreamTimeouts.delete(tag);
16772
+ }
16773
+ /**
16774
+ * Track a stream-start timeout on a streaming socket.
16775
+ * After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
16776
+ * socket so the next attempt creates a fresh connection.
16777
+ */
16778
+ trackStreamTimeout(client) {
16779
+ const tag = this.findSocketTagForClient(client);
16780
+ if (!tag || !tag.startsWith("streaming:")) return;
16781
+ const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
16782
+ this.consecutiveStreamTimeouts.set(tag, count);
16783
+ if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
16784
+ this.logger?.warn?.(
16785
+ `[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
16786
+ );
16787
+ this.consecutiveStreamTimeouts.delete(tag);
16788
+ this.forceClosePooledSocket(tag, this.logger).catch(() => {
16789
+ });
16790
+ }
16791
+ }
16437
16792
  /**
16438
16793
  * Best-effort sleeping inference for battery/BCUDP cameras.
16439
16794
  *
@@ -18105,7 +18460,7 @@ ${xml}`
18105
18460
  * @returns Test results for all stream types and profiles
18106
18461
  */
18107
18462
  async testChannelStreams(channel, logger) {
18108
- const { testChannelStreams } = await import("./DiagnosticsTools-UMN4C7SY.js");
18463
+ const { testChannelStreams } = await import("./DiagnosticsTools-HJDH4GPP.js");
18109
18464
  return await testChannelStreams({
18110
18465
  api: this,
18111
18466
  channel: this.normalizeChannel(channel),
@@ -18121,7 +18476,7 @@ ${xml}`
18121
18476
  * @returns Complete diagnostics for all channels and streams
18122
18477
  */
18123
18478
  async collectMultifocalDiagnostics(logger) {
18124
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-UMN4C7SY.js");
18479
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-HJDH4GPP.js");
18125
18480
  return await collectMultifocalDiagnostics({
18126
18481
  api: this,
18127
18482
  logger
@@ -19307,8 +19662,8 @@ ${scheduleItems}
19307
19662
  );
19308
19663
  let args;
19309
19664
  if (useMpegTsMuxer) {
19310
- MpegTsMuxer.resetCounters();
19311
- tsMuxer = new MpegTsMuxer({ videoType });
19665
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
19666
+ tsMuxer.reset();
19312
19667
  args = [
19313
19668
  "-hide_banner",
19314
19669
  "-loglevel",
@@ -19472,7 +19827,7 @@ ${scheduleItems}
19472
19827
  startFfmpeg(videoType);
19473
19828
  frameCount++;
19474
19829
  if (useMpegTsMuxer && tsMuxer) {
19475
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
19830
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
19476
19831
  input.write(tsData);
19477
19832
  } else {
19478
19833
  if (videoType === "H264") input.write(H264_AUD);
@@ -19761,8 +20116,8 @@ ${scheduleItems}
19761
20116
  logger?.log?.(
19762
20117
  `[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
19763
20118
  );
19764
- MpegTsMuxer.resetCounters();
19765
- tsMuxer = new MpegTsMuxer({ videoType });
20119
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
20120
+ tsMuxer.reset();
19766
20121
  const args = [
19767
20122
  "-hide_banner",
19768
20123
  "-loglevel",
@@ -19927,7 +20282,7 @@ ${scheduleItems}
19927
20282
  startFfmpeg(videoType);
19928
20283
  frameCount++;
19929
20284
  if (tsMuxer) {
19930
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
20285
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
19931
20286
  input.write(tsData);
19932
20287
  }
19933
20288
  if (frameCount === 1) {
@@ -21248,16 +21603,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
21248
21603
  return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("socket hang up") || message.includes("TCP connection timeout") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
21249
21604
  }
21250
21605
  async function pingHost(host, timeoutMs = 3e3) {
21606
+ const { exec } = await import("child_process");
21607
+ const platform2 = process.platform;
21608
+ const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
21609
+ // macOS: -W is in milliseconds (Linux: seconds)
21610
+ `ping -c 1 -W ${timeoutMs} ${host}`
21611
+ ) : (
21612
+ // Linux/BSD-ish: -W is in seconds on most distros
21613
+ `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
21614
+ );
21251
21615
  return new Promise((resolve) => {
21252
- const { exec } = __require("child_process");
21253
- const platform2 = process.platform;
21254
- const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
21255
- // macOS: -W is in milliseconds (Linux: seconds)
21256
- `ping -c 1 -W ${timeoutMs} ${host}`
21257
- ) : (
21258
- // Linux/BSD-ish: -W is in seconds on most distros
21259
- `ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
21260
- );
21261
21616
  exec(pingCmd, (error) => {
21262
21617
  resolve(!error);
21263
21618
  });
@@ -21339,9 +21694,10 @@ async function autoDetectDeviceType(inputs) {
21339
21694
  const msg = fmtErr(e);
21340
21695
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
21341
21696
  };
21342
- const withRetries = async (label, max, op, shouldRetry) => {
21697
+ const withRetries = async (label, max, op, shouldRetry, isAborted) => {
21343
21698
  let lastErr;
21344
21699
  for (let attempt = 1; attempt <= max; attempt++) {
21700
+ if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
21345
21701
  try {
21346
21702
  if (attempt > 1) {
21347
21703
  logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
@@ -21350,7 +21706,7 @@ async function autoDetectDeviceType(inputs) {
21350
21706
  } catch (e) {
21351
21707
  lastErr = e;
21352
21708
  const msg = fmtErr(e);
21353
- const retryable = attempt < max && shouldRetry(e);
21709
+ const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
21354
21710
  logger?.log?.(
21355
21711
  `[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
21356
21712
  );
@@ -21360,6 +21716,31 @@ async function autoDetectDeviceType(inputs) {
21360
21716
  }
21361
21717
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
21362
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
+ };
21363
21744
  const effectiveUid = normalizeUid(uid);
21364
21745
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
21365
21746
  const isReachable = await pingHost(host);
@@ -21389,9 +21770,9 @@ async function autoDetectDeviceType(inputs) {
21389
21770
  normalizedUid = normalizedDiscovered;
21390
21771
  }
21391
21772
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
21392
- const udpErrors = [];
21393
- for (const m of methodsToTry) {
21394
- try {
21773
+ return await runUdpMethodsParallel(
21774
+ methodsToTry,
21775
+ async (m, isAborted) => {
21395
21776
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
21396
21777
  const udpApi = await withRetries(
21397
21778
  `UDP(${m})`,
@@ -21414,11 +21795,14 @@ async function autoDetectDeviceType(inputs) {
21414
21795
  throw e;
21415
21796
  }
21416
21797
  },
21417
- shouldRetryUdp
21798
+ shouldRetryUdp,
21799
+ isAborted
21418
21800
  );
21419
- const deviceInfo = await udpApi.getInfo();
21420
- const capabilities = await udpApi.getDeviceCapabilities();
21421
- 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
+ ]);
21422
21806
  const channelNum = capabilities?.support?.channelNum ?? 1;
21423
21807
  const model = deviceInfo.type?.trim();
21424
21808
  const normalizedModel = model ? model.trim() : void 0;
@@ -21457,14 +21841,8 @@ async function autoDetectDeviceType(inputs) {
21457
21841
  channelNum: 1,
21458
21842
  api: udpApi
21459
21843
  };
21460
- } catch (e) {
21461
- const msg = fmtErr(e);
21462
- udpErrors.push(`${m}: ${msg}`);
21463
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
21464
- }
21465
- }
21466
- throw new Error(
21467
- `Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
21844
+ },
21845
+ "Forced UDP autodetect failed for all methods."
21468
21846
  );
21469
21847
  }
21470
21848
  let tcpApi;
@@ -21517,54 +21895,57 @@ async function autoDetectDeviceType(inputs) {
21517
21895
  }
21518
21896
  return void 0;
21519
21897
  };
21520
- const infoProbe = await runProbeVariants(
21521
- "getInfo",
21522
- [
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", [
21523
21934
  {
21524
- variant: "cmd80 class=0x6414",
21525
- op: () => api.getInfo(void 0, {
21935
+ variant: "cmd199 class=0x6414",
21936
+ op: () => api.getSupportInfo({
21526
21937
  timeoutMs: 2500,
21527
21938
  messageClass: BC_CLASS_MODERN_24
21528
21939
  })
21529
21940
  },
21530
21941
  {
21531
- variant: "cmd80 class=0x6614",
21532
- op: () => api.getInfo(void 0, {
21533
- timeoutMs: 3e3,
21534
- messageClass: BC_CLASS_MODERN_20
21535
- })
21536
- },
21537
- {
21538
- variant: "cmd318(ch0) class=0x6414",
21539
- op: () => api.getInfo(0, {
21540
- timeoutMs: 3e3,
21541
- messageClass: BC_CLASS_MODERN_24
21542
- })
21543
- },
21544
- {
21545
- variant: "cmd318(ch0) class=0x6614",
21546
- op: () => api.getInfo(0, {
21942
+ variant: "cmd199 class=0x6614",
21943
+ op: () => api.getSupportInfo({
21547
21944
  timeoutMs: 3500,
21548
21945
  messageClass: BC_CLASS_MODERN_20
21549
21946
  })
21550
21947
  }
21551
- ]
21552
- );
21553
- const supportProbe = await runProbeVariants("getSupportInfo", [
21554
- {
21555
- variant: "cmd199 class=0x6414",
21556
- op: () => api.getSupportInfo({
21557
- timeoutMs: 2500,
21558
- messageClass: BC_CLASS_MODERN_24
21559
- })
21560
- },
21561
- {
21562
- variant: "cmd199 class=0x6614",
21563
- op: () => api.getSupportInfo({
21564
- timeoutMs: 3500,
21565
- messageClass: BC_CLASS_MODERN_20
21566
- })
21567
- }
21948
+ ])
21568
21949
  ]);
21569
21950
  const deviceInfo = infoProbe?.value;
21570
21951
  const support = supportProbe?.value;
@@ -21656,9 +22037,11 @@ async function autoDetectDeviceType(inputs) {
21656
22037
  }
21657
22038
  try {
21658
22039
  const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
21659
- const deviceInfo = await udpApi.getInfo();
21660
- const capabilities = await udpApi.getDeviceCapabilities();
21661
- 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
+ ]);
21662
22045
  const channelNum = capabilities?.support?.channelNum ?? 1;
21663
22046
  const model = deviceInfo.type?.trim();
21664
22047
  const normalizedModel = model ? model.trim() : void 0;
@@ -21702,21 +22085,17 @@ async function autoDetectDeviceType(inputs) {
21702
22085
  };
21703
22086
  };
21704
22087
  const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
21705
- const udpErrors = [];
21706
- for (const m of methodsToTry) {
21707
- 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) => {
21708
22092
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
21709
22093
  const udpApi = await withRetries(
21710
22094
  `UDP(${m})`,
21711
22095
  maxRetries,
21712
22096
  async (attempt) => {
21713
- const apiInputs = {
21714
- ...inputs,
21715
- udpDiscoveryMethod: m
21716
- };
21717
- if (normalizedUid) {
21718
- apiInputs.uid = normalizedUid;
21719
- }
22097
+ const apiInputs = { ...inputs, udpDiscoveryMethod: m };
22098
+ if (normalizedUid) apiInputs.uid = normalizedUid;
21720
22099
  const api = createBaichuanApi(apiInputs, "udp");
21721
22100
  try {
21722
22101
  await api.login();
@@ -21731,20 +22110,12 @@ async function autoDetectDeviceType(inputs) {
21731
22110
  throw e;
21732
22111
  }
21733
22112
  },
21734
- shouldRetryUdp
22113
+ shouldRetryUdp,
22114
+ isAborted
21735
22115
  );
21736
- return await detectOverUdpApi(udpApi, m);
21737
- } catch (e) {
21738
- const msg = e?.message || e?.toString?.() || String(e);
21739
- udpErrors.push(`${m}: ${msg}`);
21740
- try {
21741
- } catch {
21742
- }
21743
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
21744
- }
21745
- }
21746
- throw new Error(
21747
- `UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
22116
+ return detectOverUdpApi(udpApi, m);
22117
+ },
22118
+ "UDP discovery failed for all methods."
21748
22119
  );
21749
22120
  } catch (udpError) {
21750
22121
  logger?.log?.(
@@ -21776,12 +22147,14 @@ export {
21776
22147
  BaichuanEventEmitter,
21777
22148
  createNativeStream,
21778
22149
  BaichuanRtspServer,
22150
+ MpegTsMuxer,
21779
22151
  flattenAbilitiesForChannel,
21780
22152
  abilitiesHasAny,
21781
22153
  parseSupportXml,
21782
22154
  getSupportItemForChannel,
21783
22155
  computeDeviceCapabilities,
21784
22156
  xmlIndicatesFloodlight,
22157
+ decideSleepInferenceTransition,
21785
22158
  DUAL_LENS_DUAL_MOTION_MODELS,
21786
22159
  DUAL_LENS_SINGLE_MOTION_MODELS,
21787
22160
  DUAL_LENS_MODELS,
@@ -21803,4 +22176,4 @@ export {
21803
22176
  isTcpFailureThatShouldFallbackToUdp,
21804
22177
  autoDetectDeviceType
21805
22178
  };
21806
- //# sourceMappingURL=chunk-F2Y5U3YP.js.map
22179
+ //# sourceMappingURL=chunk-VBYF3BQX.js.map