@camstack/addon-pipeline 1.2.364 → 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 +325 -205
  31. package/dist/stream-broker/index.mjs +325 -205
  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;
@@ -3755,7 +3817,7 @@ var RtspRestreamer = class {
3755
3817
  if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
3756
3818
  if (this.collectingKeyframeRtp) {
3757
3819
  this.lastKeyframeRtpPackets.push({
3758
- data: Buffer.from(rtpData),
3820
+ data: detachFromPool(rtpData),
3759
3821
  marker: rtpPacket.marker
3760
3822
  });
3761
3823
  this.lastKeyframeRtpBytes += rtpData.length;
@@ -4357,6 +4419,19 @@ var RtspSession = class {
4357
4419
  lastRtpAt = 0;
4358
4420
  /** Total video RTP bytes written to the socket. */
4359
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
+ };
4360
4435
  /** Client `User-Agent` (from the first request that carries one). Lets the
4361
4436
  * broker panel identify a consumer by purpose — e.g. the recorder sets
4362
4437
  * `-user_agent recorder` on its RTSP pull — instead of a bare address. */
@@ -4604,7 +4679,7 @@ var RtspSession = class {
4604
4679
  return "dropped-backlog";
4605
4680
  }
4606
4681
  }
4607
- this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
4682
+ this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
4608
4683
  this.missedKeyframes = 0;
4609
4684
  return "written";
4610
4685
  }
@@ -4613,7 +4688,7 @@ var RtspSession = class {
4613
4688
  if (!this.playing || this.closed || this.muted) return "not-playing";
4614
4689
  if (this.tracks.length < 2) return "not-playing";
4615
4690
  if (this.socket.writableLength > 2097152) return "dropped-backlog";
4616
- this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
4691
+ this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
4617
4692
  return "written";
4618
4693
  }
4619
4694
  /**
@@ -4635,14 +4710,13 @@ var RtspSession = class {
4635
4710
  * ## Why sharing the payload is safe, and what would make it unsafe
4636
4711
  *
4637
4712
  * The payload buffer handed here is a private allocation per packet —
4638
- * `Buffer.from(readBuffer.subarray(...))` in `rtsp-client.ts`'s `onData`, for
4639
- * video and audio alike — never a view of the reader's socket buffer, which
4640
- * advances on the next read. Nothing in the broker writes into a captured
4641
- * packet: every mutation in this path targets a freshly allocated buffer.
4642
- * (The broker's `rtpRing` keeps its OWN copy of a pooled packet —
4643
- * `detachFromPool`, so a ring entry never pins a shared pool slab — so the
4644
- * ring no longer vouches for THIS object's lifetime, and it does not need
4645
- * 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
4646
4720
  * mutation, not retention.)
4647
4721
  *
4648
4722
  * The rule to keep: **`socket.write` forbids MUTATION before the write
@@ -4652,18 +4726,36 @@ var RtspSession = class {
4652
4726
  * queued. Four bytes from the Buffer pool is a pointer bump; the copy this
4653
4727
  * replaces was ~1.4 KB.
4654
4728
  *
4655
- * `cork`/`uncork` keeps the pair a single `writev` syscall, so removing the
4656
- * 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.
4657
4745
  */
4658
- writeInterleaved(channel, payload) {
4746
+ writeInterleaved(channel, payload, endOfAccessUnit) {
4659
4747
  const header = Buffer.allocUnsafe(4);
4660
4748
  header[0] = 36;
4661
4749
  header[1] = channel;
4662
4750
  header.writeUInt16BE(payload.length, 2);
4663
- this.socket.cork();
4751
+ if (!this.accessUnitCorked) {
4752
+ this.accessUnitCorked = true;
4753
+ this.socket.cork();
4754
+ process.nextTick(this.uncorkAccessUnit);
4755
+ }
4664
4756
  this.socket.write(header);
4665
4757
  this.socket.write(payload);
4666
- this.socket.uncork();
4758
+ if (endOfAccessUnit) this.uncorkAccessUnit();
4667
4759
  this.lastRtpAt = Date.now();
4668
4760
  this.bytesSent += 4 + payload.length;
4669
4761
  }
@@ -10087,7 +10179,7 @@ var Rfc4571Reader = class {
10087
10179
  while (this.readBuffer.length >= 2) {
10088
10180
  const length = this.readBuffer.readUInt16BE(0);
10089
10181
  if (this.readBuffer.length < 2 + length) return;
10090
- const rtpPacket = Buffer.from(this.readBuffer.subarray(2, 2 + length));
10182
+ const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
10091
10183
  this.readBuffer = this.readBuffer.subarray(2 + length);
10092
10184
  this.dispatchRtp(rtpPacket);
10093
10185
  }
@@ -10148,6 +10240,93 @@ function totalFragmentBytes(fragments) {
10148
10240
  for (const fragment of fragments) total += fragment.length;
10149
10241
  return total;
10150
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
+ }
10151
10330
  /**
10152
10331
  * RTP → Annex-B NAL depacketizer.
10153
10332
  *
@@ -10192,87 +10371,22 @@ var RtpDepacketizer = class {
10192
10371
  * short to be RTP with a payload.
10193
10372
  */
10194
10373
  processPacket(rtpData) {
10195
- if (rtpData.length < 12) return null;
10196
- const marker = (rtpData[1] & 128) !== 0;
10197
- const timestamp = rtpData.readUInt32BE(4);
10374
+ const carried = this.classifyPacket(rtpData);
10375
+ if (carried === null) return null;
10198
10376
  const payload = rtpData.subarray(12);
10199
- if (payload.length === 0) return null;
10200
- const carried = this.codec === "h264" ? this.classifyH264Payload(payload) : this.classifyH265Payload(payload);
10201
- if (this.codec === "h264") this.processH264(payload, timestamp, marker);
10202
- else this.processH265(payload, timestamp, marker);
10203
- return {
10204
- timestamp,
10205
- marker,
10206
- keyframe: carried.keyframe,
10207
- parameterSet: carried.parameterSet
10208
- };
10377
+ if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
10378
+ else this.processH265(payload, carried.timestamp, carried.marker);
10379
+ return carried;
10209
10380
  }
10210
- /** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
10211
- classifyH264Payload(payload) {
10212
- const nalType = payload[0] & 31;
10213
- if (nalType === H264_NAL_STAP_A) {
10214
- let keyframe = false;
10215
- let parameterSet = false;
10216
- let offset = 1;
10217
- while (offset + 2 <= payload.length) {
10218
- const nalSize = payload.readUInt16BE(offset);
10219
- offset += 2;
10220
- if (nalSize === 0 || offset + nalSize > payload.length) break;
10221
- const type = payload[offset] & 31;
10222
- keyframe ||= type === H264_NAL_IDR;
10223
- parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
10224
- offset += nalSize;
10225
- }
10226
- return {
10227
- keyframe,
10228
- parameterSet
10229
- };
10230
- }
10231
- if (nalType === 28) {
10232
- if (payload.length < 2) return NO_PAYLOAD_CLASS;
10233
- return {
10234
- keyframe: (payload[1] & 31) === H264_NAL_IDR,
10235
- parameterSet: false
10236
- };
10237
- }
10238
- return {
10239
- keyframe: nalType === H264_NAL_IDR,
10240
- parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
10241
- };
10242
- }
10243
- /** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
10244
- classifyH265Payload(payload) {
10245
- if (payload.length < 2) return NO_PAYLOAD_CLASS;
10246
- const nalType = payload[0] >> 1 & 63;
10247
- if (nalType === H265_NAL_AP) {
10248
- let keyframe = false;
10249
- let parameterSet = false;
10250
- let offset = 2;
10251
- while (offset + 2 <= payload.length) {
10252
- const nalSize = payload.readUInt16BE(offset);
10253
- offset += 2;
10254
- if (nalSize === 0 || offset + nalSize > payload.length) break;
10255
- const type = payload[offset] >> 1 & 63;
10256
- keyframe ||= isH265IrapType(type);
10257
- parameterSet ||= isH265ParameterSetType(type);
10258
- offset += nalSize;
10259
- }
10260
- return {
10261
- keyframe,
10262
- parameterSet
10263
- };
10264
- }
10265
- if (nalType === 49) {
10266
- if (payload.length < 3) return NO_PAYLOAD_CLASS;
10267
- return {
10268
- keyframe: isH265IrapType(payload[2] & 63),
10269
- parameterSet: false
10270
- };
10271
- }
10272
- return {
10273
- keyframe: isH265IrapType(nalType),
10274
- parameterSet: isH265ParameterSetType(nalType)
10275
- };
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);
10276
10390
  }
10277
10391
  processH264(payload, timestamp, marker) {
10278
10392
  const nalType = payload[0] & 31;
@@ -10452,11 +10566,13 @@ var RtpAccessUnitTracker = class {
10452
10566
  const keyframeContent = packet.keyframe || packet.parameterSet;
10453
10567
  const keyframeAuStart = keyframeContent && !this.keyframeAu;
10454
10568
  if (keyframeContent) this.keyframeAu = true;
10569
+ const pictureStart = !packet.parameterSet && !this.pictureSeen;
10455
10570
  if (!packet.parameterSet) this.pictureSeen = true;
10456
10571
  return {
10457
10572
  auStart,
10458
10573
  keyframeAu: this.keyframeAu,
10459
10574
  keyframeAuStart,
10575
+ pictureStart,
10460
10576
  parameterSet: packet.parameterSet
10461
10577
  };
10462
10578
  }
@@ -10782,7 +10898,7 @@ var NativeRtspClient = class {
10782
10898
  const channel = this.readBuffer[1];
10783
10899
  const length = this.readBuffer.readUInt16BE(2);
10784
10900
  if (this.readBuffer.length < 4 + length) return;
10785
- const payload = Buffer.from(this.readBuffer.subarray(4, 4 + length));
10901
+ const payload = copyOwned(this.readBuffer, 4, 4 + length);
10786
10902
  this.readBuffer = this.readBuffer.subarray(4 + length);
10787
10903
  this.handleInterleavedPacket(channel, payload);
10788
10904
  } else {
@@ -11518,49 +11634,6 @@ var AudioCodecSession = class {
11518
11634
  }
11519
11635
  };
11520
11636
  //#endregion
11521
- //#region src/stream-broker/stream-broker/owned-buffer.ts
11522
- /**
11523
- * Buffers that enter a LONG-LIVED ring own their backing store.
11524
- *
11525
- * Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
11526
- * below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
11527
- * That is the right choice for the ingest path — a start code, a 4-byte
11528
- * interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
11529
- * within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
11530
- * for 60 s in a clip ring pins its whole slab, and with it every short-lived
11531
- * packet that happened to be allocated beside it. On the hub that held
11532
- * 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
11533
- * pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
11534
- *
11535
- * The ring byte caps counted payload bytes, so they never saw it. Both rings
11536
- * now pass every entry through {@link detachFromPool} on the way in and
11537
- * account it with {@link backingStoreBytes}, so the cap measures what the
11538
- * entry costs. Short-lived traffic stays pooled.
11539
- */
11540
- /** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
11541
- function ownsBackingStore(buf) {
11542
- return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
11543
- }
11544
- /**
11545
- * What retaining `buf` really costs: its own length when it owns its
11546
- * ArrayBuffer, the whole shared slab when it is a view of one.
11547
- */
11548
- function backingStoreBytes(buf) {
11549
- return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
11550
- }
11551
- /**
11552
- * `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
11553
- * buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
11554
- * runtime detail), else a copy that does. One copy, never two: a buffer that is
11555
- * already unpooled is returned as-is.
11556
- */
11557
- function detachFromPool(buf) {
11558
- if (ownsBackingStore(buf)) return buf;
11559
- const owned = Buffer.allocUnsafeSlow(buf.length);
11560
- buf.copy(owned);
11561
- return owned;
11562
- }
11563
- //#endregion
11564
11637
  //#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
11565
11638
  /**
11566
11639
  * Time-based ring buffer for encoded video packets.
@@ -12500,9 +12573,6 @@ var StreamBroker = class StreamBroker {
12500
12573
  */
12501
12574
  ffmpegBinaryPathSource = () => null;
12502
12575
  rtpDepacketizer = null;
12503
- /** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
12504
- * (sets keyframe/param-set flags without re-emitting AnnexB packets). */
12505
- pushRtpFlagDepacketizer = null;
12506
12576
  audioRtpDecoder = null;
12507
12577
  audioCodecSession = null;
12508
12578
  /**
@@ -12870,10 +12940,24 @@ var StreamBroker = class StreamBroker {
12870
12940
  * log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
12871
12941
  * back but never video is observable from the structured logs. */
12872
12942
  videoRtpSeen = 0;
12873
- /** 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
+ */
12874
12950
  _lastNalKeyframe = false;
12875
12951
  _lastNalParamSet = false;
12876
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
+ /**
12877
12961
  * Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
12878
12962
  * begin at the most recent keyframe access unit — i.e. the current GOP so
12879
12963
  * far. A late-joining WebRTC viewer on the repacketizer path replays this
@@ -13949,26 +14033,7 @@ var StreamBroker = class StreamBroker {
13949
14033
  return;
13950
14034
  }
13951
14035
  }
13952
- if (packet.type === "video") {
13953
- this.packetCount++;
13954
- this.totalBytes += packet.data.length;
13955
- this.bytesInWindow += packet.data.length;
13956
- this.packetsInWindow++;
13957
- this.lastPacketAt = Date.now();
13958
- const now = Date.now();
13959
- const windowMs = now - this.windowStartMs;
13960
- if (windowMs >= 2e3) {
13961
- this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
13962
- this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
13963
- this.bytesInWindow = 0;
13964
- this.packetsInWindow = 0;
13965
- this.windowStartMs = now;
13966
- }
13967
- if (packet.keyframe) {
13968
- if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
13969
- this.lastKeyframeMs = now;
13970
- }
13971
- }
14036
+ if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
13972
14037
  if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
13973
14038
  if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
13974
14039
  this.fanOutEncoded(packet);
@@ -14159,8 +14224,8 @@ var StreamBroker = class StreamBroker {
14159
14224
  index: auStart,
14160
14225
  ts: auPacket.readUInt32BE(4)
14161
14226
  });
14227
+ this.trimRtpRingToPreRollWindow();
14162
14228
  }
14163
- this.trimRtpRingToPreRollWindow();
14164
14229
  this.rtpRingHasKeyframe = true;
14165
14230
  }
14166
14231
  this.rtpRingPrevMarker = marker;
@@ -14429,10 +14494,10 @@ var StreamBroker = class StreamBroker {
14429
14494
  *
14430
14495
  * `push-rtp` providers deliver AnnexB-equivalents separately via
14431
14496
  * `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
14432
- * does NOT re-emit EncodedPackets; it only derives the per-packet
14433
- * keyframe/param-set flags the pre-buffer capture needs (via a
14434
- * flag-only depacketizer that never touches the AnnexB plane), keeping
14435
- * 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.
14436
14501
  *
14437
14502
  * The pre-buffer is captured even with zero subscribers attached — a
14438
14503
  * viewer that connects mid-GOP must be able to replay the GOP that
@@ -14442,9 +14507,9 @@ var StreamBroker = class StreamBroker {
14442
14507
  pushVideoRtp(rtpData) {
14443
14508
  if (this.stopping) return;
14444
14509
  this.kickRtpActivityWatchdog();
14445
- this._lastNalKeyframe = false;
14446
- this._lastNalParamSet = false;
14447
- 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;
14448
14513
  this.captureRtpForPreBuffer(rtpData);
14449
14514
  if (this.rtpVideoCallbacks.size === 0) return;
14450
14515
  for (const [cb, entry] of this.rtpVideoCallbacks) try {
@@ -14454,27 +14519,6 @@ var StreamBroker = class StreamBroker {
14454
14519
  this.logger?.warn("rtp video subscriber threw", { meta: { error: require_dist.errMsg(err) } });
14455
14520
  }
14456
14521
  }
14457
- /**
14458
- * Lazily build a flag-only RTP depacketizer for the `push-rtp` source
14459
- * path. Unlike the native reader's depacketizer (which emits AnnexB
14460
- * EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
14461
- * `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
14462
- * AnnexB plane through `pushEncodedPacket`, so re-emitting here would
14463
- * double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
14464
- * so the codec comes from the source declaration (`detectedCodec`, seeded
14465
- * in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
14466
- * starts with clean FU reassembly state.
14467
- */
14468
- ensurePushRtpFlagDepacketizer() {
14469
- if (!this.pushRtpFlagDepacketizer) {
14470
- const codec = this.detectedCodec === "h265" ? "h265" : "h264";
14471
- this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
14472
- if (keyframe) this._lastNalKeyframe = true;
14473
- if (parameterSet) this._lastNalParamSet = true;
14474
- });
14475
- }
14476
- return this.pushRtpFlagDepacketizer;
14477
- }
14478
14522
  /** Toggle the per-device streaming debug flag. */
14479
14523
  setStreamingDebug(enabled) {
14480
14524
  this.streamingDebug = enabled;
@@ -14917,10 +14961,17 @@ var StreamBroker = class StreamBroker {
14917
14961
  seq: this.videoRtpSeen,
14918
14962
  bytes: rtpData.length
14919
14963
  } });
14920
- this._lastNalKeyframe = false;
14921
- this._lastNalParamSet = false;
14922
- const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
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;
14923
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);
14924
14975
  this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
14925
14976
  this.captureRtpForPreBuffer(rtpData);
14926
14977
  if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
@@ -15136,11 +15187,83 @@ var StreamBroker = class StreamBroker {
15136
15187
  * not fed from here.
15137
15188
  */
15138
15189
  handleDepacketizedNal(depacketized) {
15139
- if (depacketized.keyframe) this._lastNalKeyframe = true;
15140
- if (depacketized.parameterSet) this._lastNalParamSet = true;
15141
15190
  this.annexBAssembler.push(depacketized);
15142
15191
  }
15143
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;
15260
+ }
15261
+ if (keyframe) {
15262
+ if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
15263
+ this.lastKeyframeMs = now;
15264
+ }
15265
+ }
15266
+ /**
15144
15267
  * One assembled access unit → one `EncodedPacket` for every Annex-B
15145
15268
  * consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
15146
15269
  * self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
@@ -15561,10 +15684,7 @@ var StreamBroker = class StreamBroker {
15561
15684
  this.annexBAssembler.flush();
15562
15685
  this.annexBAssembler.reset();
15563
15686
  this.rtpAuTracker.reset();
15564
- if (this.pushRtpFlagDepacketizer) {
15565
- this.pushRtpFlagDepacketizer.reset();
15566
- this.pushRtpFlagDepacketizer = null;
15567
- }
15687
+ this.annexBPlaneActive = false;
15568
15688
  this.resetRtpPreBuffer();
15569
15689
  if (this.audioRtpDecoder) {
15570
15690
  this.audioRtpDecoder.flush();