@camstack/addon-pipeline 1.2.363 → 1.2.365

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.
Files changed (35) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-nx2TXAGS.js → custom-model-catalog-C34VqcPo.js} +2 -2
  4. package/dist/{custom-model-catalog-DHFPSu0E.mjs → custom-model-catalog-CtpK3FKL.mjs} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-BX9VTm12.mjs → dist-BshC0NZR.mjs} +411 -291
  8. package/dist/{dist-CGOb--t-.js → dist-i4kS_3O7.js} +411 -291
  9. package/dist/{model-catalogs-DHV48cdY.js → model-catalogs-Cy7mZRWh.js} +1 -1
  10. package/dist/{model-catalogs-DNL4ydAg.mjs → model-catalogs-DNx5ATgm.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-C8bbmV3c.mjs → node-Dqmrb990.mjs} +1 -1
  14. package/dist/{node-UBMRK8SK.js → node-TzgGsCvW.js} +1 -1
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-1BwIq2Ks.js → process-memory-DLCPFTGC.js} +1 -1
  18. package/dist/{process-memory-BHG2uCeq.mjs → process-memory-DyfrbFY4.mjs} +1 -1
  19. package/dist/recorder/index.js +2 -2
  20. package/dist/recorder/index.mjs +2 -2
  21. package/dist/{segment-demux-js-PkDP-xQg.js → segment-demux-js-C9AYdKkD.js} +1 -1
  22. package/dist/{segment-demux-js-BhnT__dA.mjs → segment-demux-js-COoPqx93.mjs} +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vdmrukBk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DVgnss_e.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DWI1BCfa.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUBmyDde.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-D_DBY5wu.mjs → hostInit-ZvA7Cl_1.mjs} +2 -2
  30. package/dist/stream-broker/index.js +513 -174
  31. package/dist/stream-broker/index.mjs +513 -174
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +4 -2
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ScPS8ZWM.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Cq5NPjBX.mjs +0 -26
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CGOb--t-.js");
7
- const require_node = require("../node-UBMRK8SK.js");
6
+ const require_dist = require("../dist-i4kS_3O7.js");
7
+ const require_node = require("../node-TzgGsCvW.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-PkDP-xQg.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-C9AYdKkD.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -3085,6 +3085,68 @@ var RtpPacketizer = class {
3085
3085
  }
3086
3086
  };
3087
3087
  //#endregion
3088
+ //#region src/stream-broker/stream-broker/owned-buffer.ts
3089
+ /**
3090
+ * Buffers that enter a LONG-LIVED ring own their backing store.
3091
+ *
3092
+ * Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
3093
+ * below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
3094
+ * That is the right choice for the ingest path — a start code, a 4-byte
3095
+ * interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
3096
+ * within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
3097
+ * for 60 s in a clip ring pins its whole slab, and with it every short-lived
3098
+ * packet that happened to be allocated beside it. On the hub that held
3099
+ * 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
3100
+ * pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
3101
+ *
3102
+ * The ring byte caps counted payload bytes, so they never saw it. Both rings
3103
+ * now pass every entry through {@link detachFromPool} on the way in and
3104
+ * account it with {@link backingStoreBytes}, so the cap measures what the
3105
+ * entry costs. Short-lived traffic stays pooled.
3106
+ */
3107
+ /** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
3108
+ function ownsBackingStore(buf) {
3109
+ return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
3110
+ }
3111
+ /**
3112
+ * What retaining `buf` really costs: its own length when it owns its
3113
+ * ArrayBuffer, the whole shared slab when it is a view of one.
3114
+ */
3115
+ function backingStoreBytes(buf) {
3116
+ return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
3117
+ }
3118
+ /**
3119
+ * `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
3120
+ * buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
3121
+ * runtime detail), else a copy that does. One copy, never two: a buffer that is
3122
+ * already unpooled is returned as-is.
3123
+ */
3124
+ function detachFromPool(buf) {
3125
+ if (ownsBackingStore(buf)) return buf;
3126
+ const owned = Buffer.allocUnsafeSlow(buf.length);
3127
+ buf.copy(owned);
3128
+ return owned;
3129
+ }
3130
+ /**
3131
+ * A copy of `src[start, end)` that OWNS its backing store — the shape a reader
3132
+ * hands out when the packet it cuts from its socket buffer is going to be
3133
+ * retained anyway.
3134
+ *
3135
+ * Every source video packet is: `captureRtpForPreBuffer` keeps it for the life
3136
+ * of a GOP, and the restreamer's keyframe cache keeps the keyframe ones. With
3137
+ * the readers cutting packets as `Buffer.from(subarray)` — a pooled view — the
3138
+ * ring then had to copy each one a second time to own it: two copies of every
3139
+ * packet on the ingest path (measured 15.1 + 14.8 MB/s on the hub's load).
3140
+ * Cutting the packet owned up front makes the ring's and the cache's
3141
+ * `detachFromPool` an identity, so the packet is copied once and shared by the
3142
+ * reader's fan-out, the ring, the cache and every session's write queue.
3143
+ */
3144
+ function copyOwned(src, start, end) {
3145
+ const owned = Buffer.allocUnsafeSlow(end - start);
3146
+ src.copy(owned, 0, start, end);
3147
+ return owned;
3148
+ }
3149
+ //#endregion
3088
3150
  //#region src/stream-broker/rtsp/sdp-builder.ts
3089
3151
  function buildSdp(options) {
3090
3152
  const { codec, parameterSets, streamName } = options;
@@ -3731,7 +3793,12 @@ var RtspRestreamer = class {
3731
3793
  * Forwarded directly to all connected RTSP sessions — zero re-packetization.
3732
3794
  *
3733
3795
  * @param rtpData Complete RTP packet (header + payload)
3734
- * @param isKeyframe True if this packet is part of a keyframe access unit
3796
+ * @param isKeyframe True if this packet is part of a keyframe ACCESS UNIT —
3797
+ * judged per picture by the broker's `RtpAccessUnitTracker`: every
3798
+ * fragment of a fragmented IDR and a prefix SEI inside the picture carry
3799
+ * it, not only the packet that completed an IRAP NAL. The keyframe cache
3800
+ * below collects exactly that run, and the transition into it is the
3801
+ * once-per-picture `keyframeAuStart` the sessions resume and count on.
3735
3802
  * @param isParameterSet True if this packet contains SPS/PPS/VPS
3736
3803
  */
3737
3804
  pushRtpPassthrough(rtpData, isKeyframe, isParameterSet) {
@@ -3747,14 +3814,14 @@ var RtspRestreamer = class {
3747
3814
  this.lastKeyframeRtpBytes = 0;
3748
3815
  }
3749
3816
  }
3817
+ if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
3750
3818
  if (this.collectingKeyframeRtp) {
3751
3819
  this.lastKeyframeRtpPackets.push({
3752
- data: Buffer.from(rtpData),
3820
+ data: detachFromPool(rtpData),
3753
3821
  marker: rtpPacket.marker
3754
3822
  });
3755
3823
  this.lastKeyframeRtpBytes += rtpData.length;
3756
- if (!isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
3757
- else if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
3824
+ if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
3758
3825
  this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
3759
3826
  this.lastKeyframeRtpPackets = [];
3760
3827
  this.lastKeyframeRtpBytes = 0;
@@ -3767,17 +3834,16 @@ var RtspRestreamer = class {
3767
3834
  if (!session.isPlaying()) continue;
3768
3835
  this.pendingKeyframe.delete(session.getSessionId());
3769
3836
  }
3770
- const keyframeAu = isKeyframe || isParameterSet;
3771
3837
  for (const session of this.sessions.values()) {
3772
3838
  const sessionId = session.getSessionId();
3773
3839
  if (this.activeReplays.get(sessionId)?.enqueue({
3774
3840
  packet: rtpPacket,
3775
3841
  prime: false,
3776
- keyframeAuStart: keyframeAu
3842
+ keyframeAuStart
3777
3843
  })) continue;
3778
3844
  if (this.pendingKeyframe.has(sessionId)) continue;
3779
3845
  if (!session.isPlaying()) continue;
3780
- this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAu));
3846
+ this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAuStart));
3781
3847
  }
3782
3848
  }
3783
3849
  /**
@@ -4353,6 +4419,19 @@ var RtspSession = class {
4353
4419
  lastRtpAt = 0;
4354
4420
  /** Total video RTP bytes written to the socket. */
4355
4421
  bytesSent = 0;
4422
+ /**
4423
+ * The socket is corked for the access unit in progress — see
4424
+ * {@link writeInterleaved}. Cleared by the packet that ends the picture (RTP
4425
+ * marker, or any audio packet) or by the next-tick safety net, whichever
4426
+ * comes first.
4427
+ */
4428
+ accessUnitCorked = false;
4429
+ /** Flush the access unit in progress. Idempotent: the marker and the safety net both call it. */
4430
+ uncorkAccessUnit = () => {
4431
+ if (!this.accessUnitCorked) return;
4432
+ this.accessUnitCorked = false;
4433
+ this.socket.uncork();
4434
+ };
4356
4435
  /** Client `User-Agent` (from the first request that carries one). Lets the
4357
4436
  * broker panel identify a consumer by purpose — e.g. the recorder sets
4358
4437
  * `-user_agent recorder` on its RTSP pull — instead of a bare address. */
@@ -4600,7 +4679,7 @@ var RtspSession = class {
4600
4679
  return "dropped-backlog";
4601
4680
  }
4602
4681
  }
4603
- this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
4682
+ this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
4604
4683
  this.missedKeyframes = 0;
4605
4684
  return "written";
4606
4685
  }
@@ -4609,7 +4688,7 @@ var RtspSession = class {
4609
4688
  if (!this.playing || this.closed || this.muted) return "not-playing";
4610
4689
  if (this.tracks.length < 2) return "not-playing";
4611
4690
  if (this.socket.writableLength > 2097152) return "dropped-backlog";
4612
- this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
4691
+ this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
4613
4692
  return "written";
4614
4693
  }
4615
4694
  /**
@@ -4631,14 +4710,13 @@ var RtspSession = class {
4631
4710
  * ## Why sharing the payload is safe, and what would make it unsafe
4632
4711
  *
4633
4712
  * The payload buffer handed here is a private allocation per packet —
4634
- * `Buffer.from(readBuffer.subarray(...))` in `rtsp-client.ts`'s `onData`, for
4635
- * video and audio alike — never a view of the reader's socket buffer, which
4636
- * advances on the next read. Nothing in the broker writes into a captured
4637
- * packet: every mutation in this path targets a freshly allocated buffer.
4638
- * (The broker's `rtpRing` keeps its OWN copy of a pooled packet —
4639
- * `detachFromPool`, so a ring entry never pins a shared pool slab — so the
4640
- * ring no longer vouches for THIS object's lifetime, and it does not need
4641
- * to: a pending write holds its own reference, and the rule below is about
4713
+ * `copyOwned(readBuffer, …)` in the readers' `onData`, for video and audio
4714
+ * alike — never a view of the reader's socket buffer, which advances on the
4715
+ * next read. Nothing in the broker writes into a captured packet: every
4716
+ * mutation in this path targets a freshly allocated buffer. (The same object
4717
+ * is what the broker's `rtpRing` and the restreamer's keyframe cache retain —
4718
+ * it owns its backing store, so `detachFromPool` keeps it as is — and that is
4719
+ * fine: a pending write holds its own reference, and the rule below is about
4642
4720
  * mutation, not retention.)
4643
4721
  *
4644
4722
  * The rule to keep: **`socket.write` forbids MUTATION before the write
@@ -4648,18 +4726,36 @@ var RtspSession = class {
4648
4726
  * queued. Four bytes from the Buffer pool is a pointer bump; the copy this
4649
4727
  * replaces was ~1.4 KB.
4650
4728
  *
4651
- * `cork`/`uncork` keeps the pair a single `writev` syscall, so removing the
4652
- * copy does not buy an extra write syscall per packet.
4729
+ * ## One cork per ACCESS UNIT, not per packet
4730
+ *
4731
+ * The pair used to be corked and uncorked on its own: one `writev` per RTP
4732
+ * packet per session, so a 4K keyframe fragmented into a hundred packets and
4733
+ * fanned out to three sessions was three hundred syscalls for one picture no
4734
+ * decoder can use before it is whole. The socket is corked on the first write
4735
+ * of an access unit and uncorked by the packet that ENDS it
4736
+ * (`endOfAccessUnit`: the RTP marker on video, every audio packet), so the
4737
+ * picture leaves in one `writev`. A `process.nextTick` safety net uncorks
4738
+ * whatever is still open before the tick yields to I/O — a marker-less camera
4739
+ * or a lost marker degrades to one `writev` per tick, never to a socket held
4740
+ * corked across a read.
4741
+ *
4742
+ * Backpressure sees through the cork: `writableLength` counts corked bytes,
4743
+ * so the `MAX_WRITE_BUFFER` gate, the latch and the missed-keyframe close
4744
+ * (D447) judge exactly what they judged per packet.
4653
4745
  */
4654
- writeInterleaved(channel, payload) {
4746
+ writeInterleaved(channel, payload, endOfAccessUnit) {
4655
4747
  const header = Buffer.allocUnsafe(4);
4656
4748
  header[0] = 36;
4657
4749
  header[1] = channel;
4658
4750
  header.writeUInt16BE(payload.length, 2);
4659
- this.socket.cork();
4751
+ if (!this.accessUnitCorked) {
4752
+ this.accessUnitCorked = true;
4753
+ this.socket.cork();
4754
+ process.nextTick(this.uncorkAccessUnit);
4755
+ }
4660
4756
  this.socket.write(header);
4661
4757
  this.socket.write(payload);
4662
- this.socket.uncork();
4758
+ if (endOfAccessUnit) this.uncorkAccessUnit();
4663
4759
  this.lastRtpAt = Date.now();
4664
4760
  this.bytesSent += 4 + payload.length;
4665
4761
  }
@@ -10083,7 +10179,7 @@ var Rfc4571Reader = class {
10083
10179
  while (this.readBuffer.length >= 2) {
10084
10180
  const length = this.readBuffer.readUInt16BE(0);
10085
10181
  if (this.readBuffer.length < 2 + length) return;
10086
- const rtpPacket = Buffer.from(this.readBuffer.subarray(2, 2 + length));
10182
+ const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
10087
10183
  this.readBuffer = this.readBuffer.subarray(2 + length);
10088
10184
  this.dispatchRtp(rtpPacket);
10089
10185
  }
@@ -10128,12 +10224,109 @@ function allocAnnexBNal(byteLength) {
10128
10224
  buf[3] = 1;
10129
10225
  return buf;
10130
10226
  }
10227
+ var NO_PAYLOAD_CLASS = {
10228
+ keyframe: false,
10229
+ parameterSet: false
10230
+ };
10231
+ function isH265IrapType(type) {
10232
+ return type >= H265_NAL_IRAP_MIN && type <= H265_NAL_IRAP_MAX;
10233
+ }
10234
+ function isH265ParameterSetType(type) {
10235
+ return type === H265_NAL_VPS || type === H265_NAL_SPS || type === H265_NAL_PPS;
10236
+ }
10131
10237
  /** Total byte length of the reassembly fragments, without allocating a closure. */
10132
10238
  function totalFragmentBytes(fragments) {
10133
10239
  let total = 0;
10134
10240
  for (const fragment of fragments) total += fragment.length;
10135
10241
  return total;
10136
10242
  }
10243
+ /** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
10244
+ function classifyH264Payload(payload) {
10245
+ const nalType = payload[0] & 31;
10246
+ if (nalType === H264_NAL_STAP_A) {
10247
+ let keyframe = false;
10248
+ let parameterSet = false;
10249
+ let offset = 1;
10250
+ while (offset + 2 <= payload.length) {
10251
+ const nalSize = payload.readUInt16BE(offset);
10252
+ offset += 2;
10253
+ if (nalSize === 0 || offset + nalSize > payload.length) break;
10254
+ const type = payload[offset] & 31;
10255
+ keyframe ||= type === H264_NAL_IDR;
10256
+ parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
10257
+ offset += nalSize;
10258
+ }
10259
+ return {
10260
+ keyframe,
10261
+ parameterSet
10262
+ };
10263
+ }
10264
+ if (nalType === 28) {
10265
+ if (payload.length < 2) return NO_PAYLOAD_CLASS;
10266
+ return {
10267
+ keyframe: (payload[1] & 31) === H264_NAL_IDR,
10268
+ parameterSet: false
10269
+ };
10270
+ }
10271
+ return {
10272
+ keyframe: nalType === H264_NAL_IDR,
10273
+ parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
10274
+ };
10275
+ }
10276
+ /** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
10277
+ function classifyH265Payload(payload) {
10278
+ if (payload.length < 2) return NO_PAYLOAD_CLASS;
10279
+ const nalType = payload[0] >> 1 & 63;
10280
+ if (nalType === H265_NAL_AP) {
10281
+ let keyframe = false;
10282
+ let parameterSet = false;
10283
+ let offset = 2;
10284
+ while (offset + 2 <= payload.length) {
10285
+ const nalSize = payload.readUInt16BE(offset);
10286
+ offset += 2;
10287
+ if (nalSize === 0 || offset + nalSize > payload.length) break;
10288
+ const type = payload[offset] >> 1 & 63;
10289
+ keyframe ||= isH265IrapType(type);
10290
+ parameterSet ||= isH265ParameterSetType(type);
10291
+ offset += nalSize;
10292
+ }
10293
+ return {
10294
+ keyframe,
10295
+ parameterSet
10296
+ };
10297
+ }
10298
+ if (nalType === 49) {
10299
+ if (payload.length < 3) return NO_PAYLOAD_CLASS;
10300
+ return {
10301
+ keyframe: isH265IrapType(payload[2] & 63),
10302
+ parameterSet: false
10303
+ };
10304
+ }
10305
+ return {
10306
+ keyframe: isH265IrapType(nalType),
10307
+ parameterSet: isH265ParameterSetType(nalType)
10308
+ };
10309
+ }
10310
+ /**
10311
+ * Classify one raw RTP packet (12-byte header + payload) by what it carries —
10312
+ * the per-packet half of {@link RtpDepacketizer.processPacket}, without the
10313
+ * reassembly half. Pure: reads the headers, allocates nothing, keeps no state.
10314
+ * `null` for a packet too short to be RTP with a payload.
10315
+ */
10316
+ function classifyRtpPacket(codec, rtpData) {
10317
+ if (rtpData.length < 12) return null;
10318
+ const marker = (rtpData[1] & 128) !== 0;
10319
+ const timestamp = rtpData.readUInt32BE(4);
10320
+ const payload = rtpData.subarray(12);
10321
+ if (payload.length === 0) return null;
10322
+ const carried = codec === "h264" ? classifyH264Payload(payload) : classifyH265Payload(payload);
10323
+ return {
10324
+ timestamp,
10325
+ marker,
10326
+ keyframe: carried.keyframe,
10327
+ parameterSet: carried.parameterSet
10328
+ };
10329
+ }
10137
10330
  /**
10138
10331
  * RTP → Annex-B NAL depacketizer.
10139
10332
  *
@@ -10173,16 +10366,27 @@ var RtpDepacketizer = class {
10173
10366
  }
10174
10367
  /**
10175
10368
  * Process a raw RTP packet (including 12-byte RTP header).
10176
- * Emits zero or more complete NAL units via the callback.
10369
+ * Emits zero or more complete NAL units via the callback and returns what
10370
+ * THIS packet carried ({@link RtpPacketClass}) — `null` for a packet too
10371
+ * short to be RTP with a payload.
10177
10372
  */
10178
10373
  processPacket(rtpData) {
10179
- if (rtpData.length < 12) return;
10180
- const marker = (rtpData[1] & 128) !== 0;
10181
- const timestamp = rtpData.readUInt32BE(4);
10374
+ const carried = this.classifyPacket(rtpData);
10375
+ if (carried === null) return null;
10182
10376
  const payload = rtpData.subarray(12);
10183
- if (payload.length === 0) return;
10184
- if (this.codec === "h264") this.processH264(payload, timestamp, marker);
10185
- else this.processH265(payload, timestamp, marker);
10377
+ if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
10378
+ else this.processH265(payload, carried.timestamp, carried.marker);
10379
+ return carried;
10380
+ }
10381
+ /**
10382
+ * What this packet CARRIES, and nothing else: no NAL is reassembled, no buffer
10383
+ * allocated, no reassembly state touched. The same verdict `processPacket`
10384
+ * returns, for a broker whose Annex-B plane has no consumer — the source-RTP
10385
+ * ring, the restreamer's keyframe cache and the stats need exactly this and
10386
+ * none of the copies.
10387
+ */
10388
+ classifyPacket(rtpData) {
10389
+ return classifyRtpPacket(this.codec, rtpData);
10186
10390
  }
10187
10391
  processH264(payload, timestamp, marker) {
10188
10392
  const nalType = payload[0] & 31;
@@ -10328,6 +10532,140 @@ var RtpDepacketizer = class {
10328
10532
  }
10329
10533
  };
10330
10534
  //#endregion
10535
+ //#region src/stream-broker/rtsp/rtp-access-unit-tracker.ts
10536
+ /**
10537
+ * Tracks access-unit membership across the source RTP packets of ONE stream.
10538
+ *
10539
+ * The RTSP restreamer forwards source RTP untouched and must know, per
10540
+ * packet, whether it is part of a keyframe picture: that is what its keyframe
10541
+ * cache collects and what a backlog-latched session resumes on. The
10542
+ * depacketizer can only say so about a NAL that COMPLETED, so the fragments of
10543
+ * a fragmented IDR before the last one, and a prefix SEI between the parameter
10544
+ * sets and the slice, were reported as plain packets: the cache latch closed
10545
+ * on them and re-opened on the final fragment, and a backlogged session
10546
+ * counted every IRAP slice as a missed keyframe. Here a packet is classified
10547
+ * by what it carries ({@link RtpPacketClass}) and the verdict is carried
10548
+ * across the whole picture, bounded by the RTP timestamp and marker bit.
10549
+ *
10550
+ * A parameter-set-only run is not a picture: a camera that stamps SPS/PPS
10551
+ * with the previous timestamp still announces ONE keyframe, so the keyframe
10552
+ * verdict carries over into the slice that follows instead of starting twice.
10553
+ */
10554
+ var RtpAccessUnitTracker = class {
10555
+ timestamp = -1;
10556
+ previousMarker = true;
10557
+ keyframeAu = false;
10558
+ /** The picture in progress has received something other than a parameter set. */
10559
+ pictureSeen = false;
10560
+ classify(packet) {
10561
+ const auStart = this.previousMarker || packet.timestamp !== this.timestamp;
10562
+ if (auStart && this.pictureSeen) this.keyframeAu = false;
10563
+ if (auStart) this.pictureSeen = false;
10564
+ this.timestamp = packet.timestamp;
10565
+ this.previousMarker = packet.marker;
10566
+ const keyframeContent = packet.keyframe || packet.parameterSet;
10567
+ const keyframeAuStart = keyframeContent && !this.keyframeAu;
10568
+ if (keyframeContent) this.keyframeAu = true;
10569
+ const pictureStart = !packet.parameterSet && !this.pictureSeen;
10570
+ if (!packet.parameterSet) this.pictureSeen = true;
10571
+ return {
10572
+ auStart,
10573
+ keyframeAu: this.keyframeAu,
10574
+ keyframeAuStart,
10575
+ pictureStart,
10576
+ parameterSet: packet.parameterSet
10577
+ };
10578
+ }
10579
+ /** A redial re-bases the clock; nothing carries over. */
10580
+ reset() {
10581
+ this.timestamp = -1;
10582
+ this.previousMarker = true;
10583
+ this.keyframeAu = false;
10584
+ this.pictureSeen = false;
10585
+ }
10586
+ };
10587
+ //#endregion
10588
+ //#region src/stream-broker/stream-broker/access-unit-assembler.ts
10589
+ /**
10590
+ * Groups depacketized NALs into ACCESS UNITS — one per picture.
10591
+ *
10592
+ * The broker's `EncodedPacket` is one access unit, and every consumer judges it
10593
+ * that way: `keyframe` gates where a clip ring widens to, where a late joiner
10594
+ * can start, how the pre-roll window is cut; the stats count it as one picture
10595
+ * and time the GOP from one `keyframe` to the next. A multi-slice camera (a
10596
+ * Hikvision G2 main stream is 3 slices per picture) used to hand each slice —
10597
+ * and each prefix SEI — down as its own "frame": 75 input fps on a 25 fps
10598
+ * camera, an 8–34 ms "GOP", a clip ring that widened to the LAST IRAP slice
10599
+ * (a third of the picture, without its parameter sets), and a prefix SEI that
10600
+ * inherited the welded VPS/SPS/PPS instead of the IDR.
10601
+ *
10602
+ * Boundary rule (RFC 3550 / 6184 / 7798): the RTP marker bit closes the
10603
+ * picture it ends — flushed on the spot, so a single-slice stream pays no
10604
+ * latency — and a timestamp change closes the one before it, for cameras that
10605
+ * never set the marker. Parameter sets are held and welded onto the picture
10606
+ * that follows them, so a keyframe access unit is self-contained
10607
+ * (VPS/SPS/PPS + IDR) exactly as before; a prefix SEI stays inside its
10608
+ * picture and is never a frame of its own.
10609
+ */
10610
+ var AccessUnitAssembler = class {
10611
+ onAccessUnit;
10612
+ /** Parameter sets seen since the last picture closed; welded onto the next one. */
10613
+ pendingParamNals = [];
10614
+ /** NALs of the picture in progress (parameter sets first when welded). */
10615
+ pictureNals = [];
10616
+ pictureTimestamp = 0;
10617
+ pictureKeyframe = false;
10618
+ constructor(onAccessUnit) {
10619
+ this.onAccessUnit = onAccessUnit;
10620
+ }
10621
+ push(depacketized) {
10622
+ const { annexB, keyframe, parameterSet, timestamp, marker } = depacketized;
10623
+ if (this.pictureNals.length > 0 && timestamp !== this.pictureTimestamp) this.flush();
10624
+ if (parameterSet) {
10625
+ this.pendingParamNals.push(annexB);
10626
+ return;
10627
+ }
10628
+ if (this.pictureNals.length === 0) {
10629
+ this.pictureTimestamp = timestamp;
10630
+ this.pictureKeyframe = false;
10631
+ }
10632
+ if (this.pendingParamNals.length > 0) {
10633
+ for (const paramNal of this.pendingParamNals) this.pictureNals.push(paramNal);
10634
+ this.pendingParamNals = [];
10635
+ this.pictureKeyframe = true;
10636
+ }
10637
+ this.pictureNals.push(annexB);
10638
+ if (keyframe) this.pictureKeyframe = true;
10639
+ if (marker) this.flush();
10640
+ }
10641
+ /**
10642
+ * Emit the picture in progress, if any. Called on the marker bit, on a
10643
+ * timestamp change, and by the broker when the stream stops or redials so
10644
+ * the last picture of a marker-less stream is not lost.
10645
+ */
10646
+ flush() {
10647
+ if (this.pictureNals.length === 0) return;
10648
+ const nals = this.pictureNals;
10649
+ const timestamp = this.pictureTimestamp;
10650
+ const keyframe = this.pictureKeyframe;
10651
+ this.pictureNals = [];
10652
+ this.pictureKeyframe = false;
10653
+ const annexB = nals.length === 1 ? nals[0] : Buffer.concat(nals);
10654
+ this.onAccessUnit({
10655
+ annexB,
10656
+ timestamp,
10657
+ keyframe
10658
+ });
10659
+ }
10660
+ /** Drop everything held — a redial re-bases the clock, nothing carries over. */
10661
+ reset() {
10662
+ this.pendingParamNals = [];
10663
+ this.pictureNals = [];
10664
+ this.pictureTimestamp = 0;
10665
+ this.pictureKeyframe = false;
10666
+ }
10667
+ };
10668
+ //#endregion
10331
10669
  //#region src/stream-broker/rtsp/rtsp-client.ts
10332
10670
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
10333
10671
  var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
@@ -10560,7 +10898,7 @@ var NativeRtspClient = class {
10560
10898
  const channel = this.readBuffer[1];
10561
10899
  const length = this.readBuffer.readUInt16BE(2);
10562
10900
  if (this.readBuffer.length < 4 + length) return;
10563
- const payload = Buffer.from(this.readBuffer.subarray(4, 4 + length));
10901
+ const payload = copyOwned(this.readBuffer, 4, 4 + length);
10564
10902
  this.readBuffer = this.readBuffer.subarray(4 + length);
10565
10903
  this.handleInterleavedPacket(channel, payload);
10566
10904
  } else {
@@ -11296,49 +11634,6 @@ var AudioCodecSession = class {
11296
11634
  }
11297
11635
  };
11298
11636
  //#endregion
11299
- //#region src/stream-broker/stream-broker/owned-buffer.ts
11300
- /**
11301
- * Buffers that enter a LONG-LIVED ring own their backing store.
11302
- *
11303
- * Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
11304
- * below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
11305
- * That is the right choice for the ingest path — a start code, a 4-byte
11306
- * interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
11307
- * within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
11308
- * for 60 s in a clip ring pins its whole slab, and with it every short-lived
11309
- * packet that happened to be allocated beside it. On the hub that held
11310
- * 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
11311
- * pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
11312
- *
11313
- * The ring byte caps counted payload bytes, so they never saw it. Both rings
11314
- * now pass every entry through {@link detachFromPool} on the way in and
11315
- * account it with {@link backingStoreBytes}, so the cap measures what the
11316
- * entry costs. Short-lived traffic stays pooled.
11317
- */
11318
- /** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
11319
- function ownsBackingStore(buf) {
11320
- return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
11321
- }
11322
- /**
11323
- * What retaining `buf` really costs: its own length when it owns its
11324
- * ArrayBuffer, the whole shared slab when it is a view of one.
11325
- */
11326
- function backingStoreBytes(buf) {
11327
- return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
11328
- }
11329
- /**
11330
- * `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
11331
- * buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
11332
- * runtime detail), else a copy that does. One copy, never two: a buffer that is
11333
- * already unpooled is returned as-is.
11334
- */
11335
- function detachFromPool(buf) {
11336
- if (ownsBackingStore(buf)) return buf;
11337
- const owned = Buffer.allocUnsafeSlow(buf.length);
11338
- buf.copy(owned);
11339
- return owned;
11340
- }
11341
- //#endregion
11342
11637
  //#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
11343
11638
  /**
11344
11639
  * Time-based ring buffer for encoded video packets.
@@ -12278,9 +12573,6 @@ var StreamBroker = class StreamBroker {
12278
12573
  */
12279
12574
  ffmpegBinaryPathSource = () => null;
12280
12575
  rtpDepacketizer = null;
12281
- /** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
12282
- * (sets keyframe/param-set flags without re-emitting AnnexB packets). */
12283
- pushRtpFlagDepacketizer = null;
12284
12576
  audioRtpDecoder = null;
12285
12577
  audioCodecSession = null;
12286
12578
  /**
@@ -12648,10 +12940,24 @@ var StreamBroker = class StreamBroker {
12648
12940
  * log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
12649
12941
  * back but never video is observable from the structured logs. */
12650
12942
  videoRtpSeen = 0;
12651
- /** Tracking flags set synchronously by the RTP depacketizer callback. */
12943
+ /**
12944
+ * What the source RTP packet being handled CARRIES (keyframe / parameter-set
12945
+ * content, read from its payload headers by `classifyRtpPacket`) — the flags
12946
+ * `captureRtpForPreBuffer` reads. Per packet, not per completed NAL: every
12947
+ * fragment of a fragmented IDR carries them, and the ring marks one keyframe
12948
+ * AU for the lot.
12949
+ */
12652
12950
  _lastNalKeyframe = false;
12653
12951
  _lastNalParamSet = false;
12654
12952
  /**
12953
+ * Whether the Annex-B plane (NAL reassembly + access-unit assembly →
12954
+ * `EncodedPacket`) is being BUILT for this stream. False while nothing
12955
+ * consumes it: the source-RTP path then classifies each packet and copies
12956
+ * nothing. Armed at the next keyframe access unit once a consumer attaches
12957
+ * (`needsAnnexBPlane`), so the first packet it emits is decodable.
12958
+ */
12959
+ annexBPlaneActive = false;
12960
+ /**
12655
12961
  * Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
12656
12962
  * begin at the most recent keyframe access unit — i.e. the current GOP so
12657
12963
  * far. A late-joining WebRTC viewer on the repacketizer path replays this
@@ -13153,7 +13459,7 @@ var StreamBroker = class StreamBroker {
13153
13459
  this.rtspRestreamer.destroy();
13154
13460
  this.preBuffer.clear();
13155
13461
  this.clipBuffer?.clear();
13156
- this.pendingParamNals = [];
13462
+ this.annexBAssembler.reset();
13157
13463
  this.encodedCallbacks.clear();
13158
13464
  this.audioChunkPlane.dispose();
13159
13465
  }
@@ -13616,7 +13922,7 @@ var StreamBroker = class StreamBroker {
13616
13922
  this.reconcilePrestreamWindow();
13617
13923
  this.preBuffer.clear();
13618
13924
  this.clipBuffer?.clear();
13619
- this.pendingParamNals = [];
13925
+ this.annexBAssembler.reset();
13620
13926
  this.rtspRestreamer.resetStreamState();
13621
13927
  this.totalBytes = 0;
13622
13928
  this.bytesInWindow = 0;
@@ -13727,26 +14033,7 @@ var StreamBroker = class StreamBroker {
13727
14033
  return;
13728
14034
  }
13729
14035
  }
13730
- if (packet.type === "video") {
13731
- this.packetCount++;
13732
- this.totalBytes += packet.data.length;
13733
- this.bytesInWindow += packet.data.length;
13734
- this.packetsInWindow++;
13735
- this.lastPacketAt = Date.now();
13736
- const now = Date.now();
13737
- const windowMs = now - this.windowStartMs;
13738
- if (windowMs >= 2e3) {
13739
- this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
13740
- this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
13741
- this.bytesInWindow = 0;
13742
- this.packetsInWindow = 0;
13743
- this.windowStartMs = now;
13744
- }
13745
- if (packet.keyframe) {
13746
- if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
13747
- this.lastKeyframeMs = now;
13748
- }
13749
- }
14036
+ if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
13750
14037
  if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
13751
14038
  if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
13752
14039
  this.fanOutEncoded(packet);
@@ -13937,8 +14224,8 @@ var StreamBroker = class StreamBroker {
13937
14224
  index: auStart,
13938
14225
  ts: auPacket.readUInt32BE(4)
13939
14226
  });
14227
+ this.trimRtpRingToPreRollWindow();
13940
14228
  }
13941
- this.trimRtpRingToPreRollWindow();
13942
14229
  this.rtpRingHasKeyframe = true;
13943
14230
  }
13944
14231
  this.rtpRingPrevMarker = marker;
@@ -14207,10 +14494,10 @@ var StreamBroker = class StreamBroker {
14207
14494
  *
14208
14495
  * `push-rtp` providers deliver AnnexB-equivalents separately via
14209
14496
  * `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
14210
- * does NOT re-emit EncodedPackets; it only derives the per-packet
14211
- * keyframe/param-set flags the pre-buffer capture needs (via a
14212
- * flag-only depacketizer that never touches the AnnexB plane), keeping
14213
- * the AnnexB consumers (pipe, restreamer) single-fed by the provider.
14497
+ * does NOT re-emit EncodedPackets; it only classifies each packet for the
14498
+ * keyframe/param-set flags the pre-buffer capture needs
14499
+ * (`classifyRtpPacket`, no reassembly), keeping the AnnexB consumers
14500
+ * (pipe, restreamer) single-fed by the provider.
14214
14501
  *
14215
14502
  * The pre-buffer is captured even with zero subscribers attached — a
14216
14503
  * viewer that connects mid-GOP must be able to replay the GOP that
@@ -14220,9 +14507,9 @@ var StreamBroker = class StreamBroker {
14220
14507
  pushVideoRtp(rtpData) {
14221
14508
  if (this.stopping) return;
14222
14509
  this.kickRtpActivityWatchdog();
14223
- this._lastNalKeyframe = false;
14224
- this._lastNalParamSet = false;
14225
- this.ensurePushRtpFlagDepacketizer().processPacket(rtpData);
14510
+ const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
14511
+ this._lastNalKeyframe = carried?.keyframe ?? false;
14512
+ this._lastNalParamSet = carried?.parameterSet ?? false;
14226
14513
  this.captureRtpForPreBuffer(rtpData);
14227
14514
  if (this.rtpVideoCallbacks.size === 0) return;
14228
14515
  for (const [cb, entry] of this.rtpVideoCallbacks) try {
@@ -14232,27 +14519,6 @@ var StreamBroker = class StreamBroker {
14232
14519
  this.logger?.warn("rtp video subscriber threw", { meta: { error: require_dist.errMsg(err) } });
14233
14520
  }
14234
14521
  }
14235
- /**
14236
- * Lazily build a flag-only RTP depacketizer for the `push-rtp` source
14237
- * path. Unlike the native reader's depacketizer (which emits AnnexB
14238
- * EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
14239
- * `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
14240
- * AnnexB plane through `pushEncodedPacket`, so re-emitting here would
14241
- * double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
14242
- * so the codec comes from the source declaration (`detectedCodec`, seeded
14243
- * in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
14244
- * starts with clean FU reassembly state.
14245
- */
14246
- ensurePushRtpFlagDepacketizer() {
14247
- if (!this.pushRtpFlagDepacketizer) {
14248
- const codec = this.detectedCodec === "h265" ? "h265" : "h264";
14249
- this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
14250
- if (keyframe) this._lastNalKeyframe = true;
14251
- if (parameterSet) this._lastNalParamSet = true;
14252
- });
14253
- }
14254
- return this.pushRtpFlagDepacketizer;
14255
- }
14256
14522
  /** Toggle the per-device streaming debug flag. */
14257
14523
  setStreamingDebug(enabled) {
14258
14524
  this.streamingDebug = enabled;
@@ -14695,10 +14961,18 @@ var StreamBroker = class StreamBroker {
14695
14961
  seq: this.videoRtpSeen,
14696
14962
  bytes: rtpData.length
14697
14963
  } });
14698
- this._lastNalKeyframe = false;
14699
- this._lastNalParamSet = false;
14700
- this.rtpDepacketizer?.processPacket(rtpData);
14701
- this.rtspRestreamer.pushRtpPassthrough(rtpData, this._lastNalKeyframe, this._lastNalParamSet);
14964
+ const wantPlane = this.needsAnnexBPlane();
14965
+ if (!wantPlane && this.annexBPlaneActive) this.idleAnnexBPlane();
14966
+ const carried = this.annexBPlaneActive ? this.rtpDepacketizer?.processPacket(rtpData) ?? null : this.rtpDepacketizer?.classifyPacket(rtpData) ?? null;
14967
+ this._lastNalKeyframe = carried?.keyframe ?? false;
14968
+ this._lastNalParamSet = carried?.parameterSet ?? false;
14969
+ const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
14970
+ if (wantPlane && !this.annexBPlaneActive && auFlags?.keyframeAuStart === true) {
14971
+ this.armAnnexBPlane();
14972
+ this.rtpDepacketizer?.processPacket(rtpData);
14973
+ }
14974
+ if (auFlags) this.recordVideoStats(rtpData.length, auFlags.pictureStart, auFlags.keyframeAu);
14975
+ this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
14702
14976
  this.captureRtpForPreBuffer(rtpData);
14703
14977
  if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
14704
14978
  cb(rtpData);
@@ -14894,51 +15168,116 @@ var StreamBroker = class StreamBroker {
14894
15168
  * RTSP restreamer's pushRtpPassthrough.
14895
15169
  */
14896
15170
  /**
14897
- * Pending parameter-set NALs (SPS/PPS for H.264, VPS/SPS/PPS for H.265).
14898
- * Cameras emit these as separate RTP packets before the IDR/IRAP frame.
14899
- * We buffer them here and prepend to the next VCL NAL so downstream
14900
- * consumers (WebRTC, prebuffer, restream) receive complete access units
14901
- * with a single `EncodedPacket` that contains SPS+PPS+IDR together.
15171
+ * Groups the depacketizer's NALs into access units — ONE `EncodedPacket`
15172
+ * per picture, parameter sets welded onto the keyframe picture they
15173
+ * precede. See `access-unit-assembler.ts` for the boundary rule and the
15174
+ * per-NAL emit it replaced.
14902
15175
  */
14903
- pendingParamNals = [];
15176
+ annexBAssembler = new AccessUnitAssembler((accessUnit) => this.emitAccessUnit(accessUnit));
15177
+ /** Access-unit membership of the source RTP packets, for the restreamer's flags. */
15178
+ rtpAuTracker = new RtpAccessUnitTracker();
14904
15179
  /**
14905
15180
  * handleDepacketizedNal receives individual NALs from the RTP depacketizer
14906
- * and emits them as EncodedPackets.
14907
- *
14908
- * Parameter-set NALs (SPS/PPS type 7/8 for H.264, VPS/SPS/PPS type 32/33/34
14909
- * for H.265) are buffered and prepended to the next VCL NAL (slice/IDR).
14910
- * This guarantees every keyframe EncodedPacket is a self-contained access
14911
- * unit that a decoder can initialise from — critical for WebRTC (Chrome
14912
- * can't decode an IDR without preceding SPS/PPS).
15181
+ * and hands them to the access-unit assembler, which emits one
15182
+ * `EncodedPacket` per PICTURE (`emitAccessUnit`) — never per NAL. A
15183
+ * multi-slice IDR, its parameter sets and its prefix SEI are one keyframe
15184
+ * packet; three P slices are one P packet.
14913
15185
  *
14914
- * The RTSP restreamer handles its own RTP passthrough separately and is
14915
- * not affected by this merging.
15186
+ * The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
15187
+ * not fed from here.
14916
15188
  */
14917
15189
  handleDepacketizedNal(depacketized) {
14918
- const { nal, keyframe, parameterSet, timestamp } = depacketized;
14919
- const codec = this.detectedCodec ?? "h264";
14920
- if (keyframe) this._lastNalKeyframe = true;
14921
- if (parameterSet) this._lastNalParamSet = true;
14922
- const isH264 = codec === "h264";
14923
- const nalType = isH264 ? nal[0] & 31 : (nal[0] & 126) >> 1;
14924
- if (isH264 ? nalType === 7 || nalType === 8 : nalType === 32 || nalType === 33 || nalType === 34) {
14925
- this.pendingParamNals.push(depacketized.annexB);
14926
- return;
15190
+ this.annexBAssembler.push(depacketized);
15191
+ }
15192
+ /**
15193
+ * Does anything consume the Annex-B plane right now? An encoded subscriber
15194
+ * (WebRTC Annex-B, fragments, a decode session), a pipe client, a clip ring
15195
+ * (it retains access units continuously, so it needs them built even with
15196
+ * nobody attached), or a restreamer still in Annex-B mode (no camera SDP yet).
15197
+ * The RTSP restream and the WebRTC RTP path are served from the source RTP
15198
+ * and never count. Cheap — four field reads — because it runs per packet.
15199
+ */
15200
+ needsAnnexBPlane() {
15201
+ return this.encodedCallbacks.size > 0 || this.clipBuffer !== null || this.pipeServer.getClientCount() > 0 || !this.rtspRestreamer.rtpPassthroughMode;
15202
+ }
15203
+ /** The consumers that want the plane, for the arm/idle lines. */
15204
+ annexBPlaneConsumers() {
15205
+ return {
15206
+ encodedSubscribers: this.encodedCallbacks.size,
15207
+ clipRing: this.clipBuffer !== null,
15208
+ pipeClients: this.pipeServer.getClientCount(),
15209
+ restreamerAnnexB: !this.rtspRestreamer.rtpPassthroughMode
15210
+ };
15211
+ }
15212
+ /**
15213
+ * Start building access units from the keyframe AU the current packet opens.
15214
+ * The depacketizer and the assembler are clean here (reset when the plane
15215
+ * went idle, or never used), so nothing stale precedes that keyframe.
15216
+ */
15217
+ armAnnexBPlane() {
15218
+ this.annexBPlaneActive = true;
15219
+ this.logger?.info("annex-b plane armed at a keyframe access unit", {
15220
+ tags: { deviceId: this.numericDeviceId },
15221
+ meta: this.annexBPlaneConsumers()
15222
+ });
15223
+ }
15224
+ /**
15225
+ * Stop building access units: the last consumer left. The picture in
15226
+ * progress is dropped, not flushed — there is nobody to hand it to, and a
15227
+ * partial picture must never reach the next consumer's clean start. Logged,
15228
+ * once per transition: a plane that is idle on purpose must not read as a
15229
+ * stream that stopped.
15230
+ */
15231
+ idleAnnexBPlane() {
15232
+ this.annexBPlaneActive = false;
15233
+ this.rtpDepacketizer?.reset();
15234
+ this.annexBAssembler.reset();
15235
+ this.logger?.info("annex-b plane idle — no consumer, NAL reassembly skipped", { tags: { deviceId: this.numericDeviceId } });
15236
+ }
15237
+ /**
15238
+ * One place for the input stats, fed from whichever path owns the pictures:
15239
+ * the source RTP packets while the native reader is up (`onVideoRtp`), the
15240
+ * `EncodedPacket`s otherwise (`pushEncodedPacket`). `bytes` accumulate per
15241
+ * call; `inputFps`, `packetCount` and the IDR interval advance once per
15242
+ * picture — the window is closed on a picture start so a window of N frames
15243
+ * reads N, exactly as it did when every call was one access unit.
15244
+ */
15245
+ recordVideoStats(bytes, pictureStart, keyframe) {
15246
+ this.totalBytes += bytes;
15247
+ this.bytesInWindow += bytes;
15248
+ if (!pictureStart) return;
15249
+ this.packetCount++;
15250
+ this.packetsInWindow++;
15251
+ const now = Date.now();
15252
+ this.lastPacketAt = now;
15253
+ const windowMs = now - this.windowStartMs;
15254
+ if (windowMs >= 2e3) {
15255
+ this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
15256
+ this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
15257
+ this.bytesInWindow = 0;
15258
+ this.packetsInWindow = 0;
15259
+ this.windowStartMs = now;
14927
15260
  }
14928
- const hadParamSets = this.pendingParamNals.length > 0;
14929
- let annexBData;
14930
- if (hadParamSets) {
14931
- this.pendingParamNals.push(depacketized.annexB);
14932
- annexBData = Buffer.concat(this.pendingParamNals);
14933
- this.pendingParamNals = [];
14934
- } else annexBData = depacketized.annexB;
14935
- const pts = Math.floor(timestamp / 90);
15261
+ if (keyframe) {
15262
+ if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
15263
+ this.lastKeyframeMs = now;
15264
+ }
15265
+ }
15266
+ /**
15267
+ * One assembled access unit → one `EncodedPacket` for every Annex-B
15268
+ * consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
15269
+ * self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
15270
+ * initialises from and what a clip or a pre-roll window may start at.
15271
+ */
15272
+ emitAccessUnit(accessUnit) {
15273
+ const codec = this.detectedCodec ?? "h264";
15274
+ const pts = Math.floor(accessUnit.timestamp / 90);
14936
15275
  const packet = {
14937
15276
  type: "video",
14938
- data: annexBData,
15277
+ data: accessUnit.annexB,
14939
15278
  pts,
14940
15279
  dts: pts,
14941
- keyframe: keyframe || hadParamSets,
15280
+ keyframe: accessUnit.keyframe,
14942
15281
  codec
14943
15282
  };
14944
15283
  this.pushEncodedPacket(packet);
@@ -15342,10 +15681,10 @@ var StreamBroker = class StreamBroker {
15342
15681
  this.rtpDepacketizer.reset();
15343
15682
  this.rtpDepacketizer = null;
15344
15683
  }
15345
- if (this.pushRtpFlagDepacketizer) {
15346
- this.pushRtpFlagDepacketizer.reset();
15347
- this.pushRtpFlagDepacketizer = null;
15348
- }
15684
+ this.annexBAssembler.flush();
15685
+ this.annexBAssembler.reset();
15686
+ this.rtpAuTracker.reset();
15687
+ this.annexBPlaneActive = false;
15349
15688
  this.resetRtpPreBuffer();
15350
15689
  if (this.audioRtpDecoder) {
15351
15690
  this.audioRtpDecoder.flush();