@apocaliss92/nodelink-js 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-MN7GUZT7.js";
6
+ } from "../chunk-RWYEGEWG.js";
7
7
  import {
8
8
  __require
9
9
  } from "../chunk-NLTB7GTA.js";
package/dist/index.cjs CHANGED
@@ -12951,6 +12951,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
12951
12951
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
12952
12952
  nativeFanout = null;
12953
12953
  noClientAutoStopTimer;
12954
+ // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
12955
+ // When a new client connects while the stream is already running it does not need
12956
+ // to wait up to one full GOP interval for the next keyframe — we replay frames
12957
+ // from the last IDR in the prebuffer immediately.
12958
+ PREBUFFER_MAX_MS = 3e3;
12959
+ prebuffer = [];
12954
12960
  static isAdtsAacFrame(b) {
12955
12961
  return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
12956
12962
  }
@@ -12985,6 +12991,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
12985
12991
  );
12986
12992
  return { sampleRate, channels, configHex };
12987
12993
  }
12994
+ /** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
12995
+ isRawFrameKeyframe(frame) {
12996
+ try {
12997
+ if (frame.videoType === "H264") {
12998
+ const nals = _BaichuanRtspServer.splitAnnexBNals(
12999
+ convertToAnnexB(frame.data)
13000
+ );
13001
+ return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
13002
+ }
13003
+ if (frame.videoType === "H265") {
13004
+ const nals = splitAnnexBToNalPayloads2(convertToAnnexB2(frame.data));
13005
+ return nals.some(
13006
+ (n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
13007
+ );
13008
+ }
13009
+ } catch {
13010
+ }
13011
+ return false;
13012
+ }
12988
13013
  static parseInterleavedChannels(transportHeader) {
12989
13014
  const m = transportHeader.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
12990
13015
  if (!m) return null;
@@ -13379,7 +13404,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13379
13404
  }
13380
13405
  const { hasParamSets: hasParamSets2 } = this.flow.getFmtp();
13381
13406
  if (!hasParamSets2) {
13382
- const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 1500;
13407
+ const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 3e3;
13408
+ const primingStart = Date.now();
13409
+ this.logger.info(
13410
+ `[rebroadcast] DESCRIBE priming: waiting up to ${primingMs}ms for SPS/PPS client=${clientId} path=${this.path}`
13411
+ );
13383
13412
  try {
13384
13413
  await Promise.race([
13385
13414
  this.firstFramePromise || Promise.resolve(),
@@ -13387,6 +13416,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13387
13416
  ]);
13388
13417
  } catch {
13389
13418
  }
13419
+ const primingElapsed = Date.now() - primingStart;
13420
+ const { hasParamSets: hasParamSetsAfter } = this.flow.getFmtp();
13421
+ if (hasParamSetsAfter) {
13422
+ this.logger.info(
13423
+ `[rebroadcast] DESCRIBE priming: SPS/PPS received after ${primingElapsed}ms client=${clientId} path=${this.path}`
13424
+ );
13425
+ } else {
13426
+ this.logger.warn(
13427
+ `[rebroadcast] DESCRIBE priming: timed out after ${primingElapsed}ms without SPS/PPS \u2014 SDP will lack sprop-parameter-sets, downstream decoder may hang client=${clientId} path=${this.path}`
13428
+ );
13429
+ }
13390
13430
  }
13391
13431
  }
13392
13432
  {
@@ -13395,11 +13435,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13395
13435
  this.logger.info(
13396
13436
  `[BaichuanRtspServer] DESCRIBE SDP for ${clientId} path=${this.path} codec=${this.flow.sdpCodec} hasParamSets=${hasParamSets2} fmtp=${fmtpPreview}`
13397
13437
  );
13398
- if (!hasParamSets2) {
13399
- this.rtspDebugLog(
13400
- `DESCRIBE responding without parameter sets yet (client=${clientId}, path=${this.path}, flow=${this.flow.key})`
13401
- );
13402
- }
13403
13438
  }
13404
13439
  const sdp = this.generateSdp();
13405
13440
  sendResponse(
@@ -13604,10 +13639,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13604
13639
  sdp += `a=control:track1\r
13605
13640
  `;
13606
13641
  }
13607
- sdp += `a=setup:passive\r
13608
- `;
13609
- sdp += `a=connection:new\r
13610
- `;
13611
13642
  return sdp;
13612
13643
  }
13613
13644
  /**
@@ -13678,6 +13709,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13678
13709
  return false;
13679
13710
  if (channel === audioRtpChannel && !resources2?.setupTrack1)
13680
13711
  return false;
13712
+ const buffered = rtspSocket.writableLength;
13713
+ if (buffered > 10 * 1024 * 1024) {
13714
+ this.logger.warn(
13715
+ `[rebroadcast] backpressure: ${Math.round(buffered / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
13716
+ );
13717
+ rtspSocket.destroy();
13718
+ return false;
13719
+ }
13681
13720
  try {
13682
13721
  return rtspSocket.write(frameRtpOverTcp(channel, msg));
13683
13722
  } catch (error) {
@@ -14107,6 +14146,24 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14107
14146
  let frameCount = 0;
14108
14147
  let lastFrameTime = Date.now();
14109
14148
  const targetFrameInterval = streamMetadata && streamMetadata.frameRate > 0 ? 1e3 / streamMetadata.frameRate : 40;
14149
+ const prebufferSnap = this.prebuffer.slice();
14150
+ let lastIdrIdx = -1;
14151
+ for (let i = prebufferSnap.length - 1; i >= 0; i--) {
14152
+ if (prebufferSnap[i].isKeyframe) {
14153
+ lastIdrIdx = i;
14154
+ break;
14155
+ }
14156
+ }
14157
+ const prebufferFrames = lastIdrIdx >= 0 ? prebufferSnap.slice(lastIdrIdx) : [];
14158
+ if (prebufferFrames.length > 0) {
14159
+ this.logger.info(
14160
+ `[rebroadcast] prebuffer replay client=${clientId} frames=${prebufferFrames.length} starting from IDR`
14161
+ );
14162
+ }
14163
+ const combined = async function* () {
14164
+ for (const entry of prebufferFrames) yield entry.frame;
14165
+ for await (const f of clientGenerator) yield f;
14166
+ };
14110
14167
  const feedFrames = async () => {
14111
14168
  try {
14112
14169
  this.rtspDebugLog(
@@ -14118,7 +14175,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14118
14175
  let firstVideoFrameSeenLogged = false;
14119
14176
  let h265WaitParamSetsLogged = false;
14120
14177
  let h265WaitIrapLogged = false;
14121
- for await (const frame of clientGenerator) {
14178
+ for await (const frame of combined()) {
14122
14179
  if (!this.connectedClients.has(clientId)) {
14123
14180
  this.rtspDebugLog(
14124
14181
  `Client ${clientId} disconnected, stopping frame feed`
@@ -14442,6 +14499,18 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14442
14499
  if (hasParamSets2) {
14443
14500
  this.markFirstFrameReceived();
14444
14501
  }
14502
+ const isKeyframe = this.isRawFrameKeyframe(frame);
14503
+ this.prebuffer.push({
14504
+ frame: { ...frame, data: Buffer.from(frame.data) },
14505
+ time: Date.now(),
14506
+ isKeyframe
14507
+ });
14508
+ const cutoff = Date.now() - this.PREBUFFER_MAX_MS;
14509
+ let trimIdx = 0;
14510
+ while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
14511
+ trimIdx++;
14512
+ }
14513
+ if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
14445
14514
  },
14446
14515
  onError: (error) => {
14447
14516
  this.logger.warn(
@@ -14455,6 +14524,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14455
14524
  this.firstFramePromise = null;
14456
14525
  this.firstFrameResolve = null;
14457
14526
  this.nativeFanout = null;
14527
+ this.prebuffer = [];
14458
14528
  this.logger.info(
14459
14529
  `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
14460
14530
  );
@@ -14523,6 +14593,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14523
14593
  this.nativeFanout = null;
14524
14594
  await fanout.stop();
14525
14595
  }
14596
+ this.prebuffer = [];
14526
14597
  if (this.tempStreamGenerator) {
14527
14598
  try {
14528
14599
  await this.tempStreamGenerator.return(void 0);