@camstack/addon-pipeline 1.2.362 → 1.2.364

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 (34) 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-C77XAnt9.mjs → custom-model-catalog-DHFPSu0E.mjs} +2 -2
  4. package/dist/{custom-model-catalog-CL9Tcn2Y.js → custom-model-catalog-nx2TXAGS.js} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-D629dz5x.mjs → dist-BX9VTm12.mjs} +177 -3
  8. package/dist/{dist-tclsRHCk.js → dist-CGOb--t-.js} +177 -3
  9. package/dist/{model-catalogs-Y1WJrGyS.js → model-catalogs-DHV48cdY.js} +1 -1
  10. package/dist/{model-catalogs-BxQBJBt4.mjs → model-catalogs-DNL4ydAg.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-D2D0N7_h.mjs → node-C8bbmV3c.mjs} +1 -1
  14. package/dist/{node-BYrYuZdh.js → node-UBMRK8SK.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-eM4qLFdC.js → process-memory-1BwIq2Ks.js} +1 -1
  18. package/dist/{process-memory-BR1e6q_Q.mjs → process-memory-BHG2uCeq.mjs} +1 -1
  19. package/dist/recorder/index.js +3 -3
  20. package/dist/recorder/index.mjs +3 -3
  21. package/dist/{segment-demux-js-C5IOs4lZ.mjs → segment-demux-js-BhnT__dA.mjs} +1 -1
  22. package/dist/{segment-demux-js-B9EqRfBi.js → segment-demux-js-PkDP-xQg.js} +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-C4Cp_OAY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vdmrukBk.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-CICWhpXS.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ScPS8ZWM.mjs} +1 -1
  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-Cq5NPjBX.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-CZ_HBwki.mjs → hostInit-D_DBY5wu.mjs} +2 -2
  30. package/dist/stream-broker/index.js +357 -71
  31. package/dist/stream-broker/index.mjs +357 -71
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-7VuNrr9l.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { t as demuxSegmentFullJs } from "../segment-demux-js-C5IOs4lZ.mjs";
1
+ import { t as demuxSegmentFullJs } from "../segment-demux-js-BhnT__dA.mjs";
2
2
  import { parentPort } from "node:worker_threads";
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.358",
39
+ version: "1.2.360",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.282",
84
+ version: "1.2.284",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -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-tclsRHCk.js");
7
- const require_node = require("../node-BYrYuZdh.js");
6
+ const require_dist = require("../dist-CGOb--t-.js");
7
+ const require_node = require("../node-UBMRK8SK.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-B9EqRfBi.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-PkDP-xQg.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");
@@ -3731,7 +3731,12 @@ var RtspRestreamer = class {
3731
3731
  * Forwarded directly to all connected RTSP sessions — zero re-packetization.
3732
3732
  *
3733
3733
  * @param rtpData Complete RTP packet (header + payload)
3734
- * @param isKeyframe True if this packet is part of a keyframe access unit
3734
+ * @param isKeyframe True if this packet is part of a keyframe ACCESS UNIT —
3735
+ * judged per picture by the broker's `RtpAccessUnitTracker`: every
3736
+ * fragment of a fragmented IDR and a prefix SEI inside the picture carry
3737
+ * it, not only the packet that completed an IRAP NAL. The keyframe cache
3738
+ * below collects exactly that run, and the transition into it is the
3739
+ * once-per-picture `keyframeAuStart` the sessions resume and count on.
3735
3740
  * @param isParameterSet True if this packet contains SPS/PPS/VPS
3736
3741
  */
3737
3742
  pushRtpPassthrough(rtpData, isKeyframe, isParameterSet) {
@@ -3747,14 +3752,14 @@ var RtspRestreamer = class {
3747
3752
  this.lastKeyframeRtpBytes = 0;
3748
3753
  }
3749
3754
  }
3755
+ if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
3750
3756
  if (this.collectingKeyframeRtp) {
3751
3757
  this.lastKeyframeRtpPackets.push({
3752
3758
  data: Buffer.from(rtpData),
3753
3759
  marker: rtpPacket.marker
3754
3760
  });
3755
3761
  this.lastKeyframeRtpBytes += rtpData.length;
3756
- if (!isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
3757
- else if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
3762
+ if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
3758
3763
  this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
3759
3764
  this.lastKeyframeRtpPackets = [];
3760
3765
  this.lastKeyframeRtpBytes = 0;
@@ -3767,17 +3772,16 @@ var RtspRestreamer = class {
3767
3772
  if (!session.isPlaying()) continue;
3768
3773
  this.pendingKeyframe.delete(session.getSessionId());
3769
3774
  }
3770
- const keyframeAu = isKeyframe || isParameterSet;
3771
3775
  for (const session of this.sessions.values()) {
3772
3776
  const sessionId = session.getSessionId();
3773
3777
  if (this.activeReplays.get(sessionId)?.enqueue({
3774
3778
  packet: rtpPacket,
3775
3779
  prime: false,
3776
- keyframeAuStart: keyframeAu
3780
+ keyframeAuStart
3777
3781
  })) continue;
3778
3782
  if (this.pendingKeyframe.has(sessionId)) continue;
3779
3783
  if (!session.isPlaying()) continue;
3780
- this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAu));
3784
+ this.recordLiveOutcome(session.sendRtp(rtpPacket, false, keyframeAuStart));
3781
3785
  }
3782
3786
  }
3783
3787
  /**
@@ -4630,15 +4634,16 @@ var RtspSession = class {
4630
4634
  *
4631
4635
  * ## Why sharing the payload is safe, and what would make it unsafe
4632
4636
  *
4633
- * The payload buffer handed here is ALREADY retained, by reference, in the
4634
- * broker's `rtpRing` for the life of a GOP (`captureRtpForPreBuffer` pushes
4635
- * the same object). A pending `socket.write` cannot outlive that, so the
4636
- * lifetime requirement this adds is strictly weaker than one the broker
4637
- * already meets. Its provenance is a private allocation per packet —
4637
+ * The payload buffer handed here is a private allocation per packet —
4638
4638
  * `Buffer.from(readBuffer.subarray(...))` in `rtsp-client.ts`'s `onData`, for
4639
4639
  * video and audio alike — never a view of the reader's socket buffer, which
4640
4640
  * advances on the next read. Nothing in the broker writes into a captured
4641
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
4646
+ * mutation, not retention.)
4642
4647
  *
4643
4648
  * The rule to keep: **`socket.write` forbids MUTATION before the write
4644
4649
  * completes, not retention.** So the header, which IS built per call, must
@@ -10127,6 +10132,16 @@ function allocAnnexBNal(byteLength) {
10127
10132
  buf[3] = 1;
10128
10133
  return buf;
10129
10134
  }
10135
+ var NO_PAYLOAD_CLASS = {
10136
+ keyframe: false,
10137
+ parameterSet: false
10138
+ };
10139
+ function isH265IrapType(type) {
10140
+ return type >= H265_NAL_IRAP_MIN && type <= H265_NAL_IRAP_MAX;
10141
+ }
10142
+ function isH265ParameterSetType(type) {
10143
+ return type === H265_NAL_VPS || type === H265_NAL_SPS || type === H265_NAL_PPS;
10144
+ }
10130
10145
  /** Total byte length of the reassembly fragments, without allocating a closure. */
10131
10146
  function totalFragmentBytes(fragments) {
10132
10147
  let total = 0;
@@ -10172,16 +10187,92 @@ var RtpDepacketizer = class {
10172
10187
  }
10173
10188
  /**
10174
10189
  * Process a raw RTP packet (including 12-byte RTP header).
10175
- * Emits zero or more complete NAL units via the callback.
10190
+ * Emits zero or more complete NAL units via the callback and returns what
10191
+ * THIS packet carried ({@link RtpPacketClass}) — `null` for a packet too
10192
+ * short to be RTP with a payload.
10176
10193
  */
10177
10194
  processPacket(rtpData) {
10178
- if (rtpData.length < 12) return;
10195
+ if (rtpData.length < 12) return null;
10179
10196
  const marker = (rtpData[1] & 128) !== 0;
10180
10197
  const timestamp = rtpData.readUInt32BE(4);
10181
10198
  const payload = rtpData.subarray(12);
10182
- if (payload.length === 0) return;
10199
+ if (payload.length === 0) return null;
10200
+ const carried = this.codec === "h264" ? this.classifyH264Payload(payload) : this.classifyH265Payload(payload);
10183
10201
  if (this.codec === "h264") this.processH264(payload, timestamp, marker);
10184
10202
  else this.processH265(payload, timestamp, marker);
10203
+ return {
10204
+ timestamp,
10205
+ marker,
10206
+ keyframe: carried.keyframe,
10207
+ parameterSet: carried.parameterSet
10208
+ };
10209
+ }
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
+ };
10185
10276
  }
10186
10277
  processH264(payload, timestamp, marker) {
10187
10278
  const nalType = payload[0] & 31;
@@ -10327,6 +10418,138 @@ var RtpDepacketizer = class {
10327
10418
  }
10328
10419
  };
10329
10420
  //#endregion
10421
+ //#region src/stream-broker/rtsp/rtp-access-unit-tracker.ts
10422
+ /**
10423
+ * Tracks access-unit membership across the source RTP packets of ONE stream.
10424
+ *
10425
+ * The RTSP restreamer forwards source RTP untouched and must know, per
10426
+ * packet, whether it is part of a keyframe picture: that is what its keyframe
10427
+ * cache collects and what a backlog-latched session resumes on. The
10428
+ * depacketizer can only say so about a NAL that COMPLETED, so the fragments of
10429
+ * a fragmented IDR before the last one, and a prefix SEI between the parameter
10430
+ * sets and the slice, were reported as plain packets: the cache latch closed
10431
+ * on them and re-opened on the final fragment, and a backlogged session
10432
+ * counted every IRAP slice as a missed keyframe. Here a packet is classified
10433
+ * by what it carries ({@link RtpPacketClass}) and the verdict is carried
10434
+ * across the whole picture, bounded by the RTP timestamp and marker bit.
10435
+ *
10436
+ * A parameter-set-only run is not a picture: a camera that stamps SPS/PPS
10437
+ * with the previous timestamp still announces ONE keyframe, so the keyframe
10438
+ * verdict carries over into the slice that follows instead of starting twice.
10439
+ */
10440
+ var RtpAccessUnitTracker = class {
10441
+ timestamp = -1;
10442
+ previousMarker = true;
10443
+ keyframeAu = false;
10444
+ /** The picture in progress has received something other than a parameter set. */
10445
+ pictureSeen = false;
10446
+ classify(packet) {
10447
+ const auStart = this.previousMarker || packet.timestamp !== this.timestamp;
10448
+ if (auStart && this.pictureSeen) this.keyframeAu = false;
10449
+ if (auStart) this.pictureSeen = false;
10450
+ this.timestamp = packet.timestamp;
10451
+ this.previousMarker = packet.marker;
10452
+ const keyframeContent = packet.keyframe || packet.parameterSet;
10453
+ const keyframeAuStart = keyframeContent && !this.keyframeAu;
10454
+ if (keyframeContent) this.keyframeAu = true;
10455
+ if (!packet.parameterSet) this.pictureSeen = true;
10456
+ return {
10457
+ auStart,
10458
+ keyframeAu: this.keyframeAu,
10459
+ keyframeAuStart,
10460
+ parameterSet: packet.parameterSet
10461
+ };
10462
+ }
10463
+ /** A redial re-bases the clock; nothing carries over. */
10464
+ reset() {
10465
+ this.timestamp = -1;
10466
+ this.previousMarker = true;
10467
+ this.keyframeAu = false;
10468
+ this.pictureSeen = false;
10469
+ }
10470
+ };
10471
+ //#endregion
10472
+ //#region src/stream-broker/stream-broker/access-unit-assembler.ts
10473
+ /**
10474
+ * Groups depacketized NALs into ACCESS UNITS — one per picture.
10475
+ *
10476
+ * The broker's `EncodedPacket` is one access unit, and every consumer judges it
10477
+ * that way: `keyframe` gates where a clip ring widens to, where a late joiner
10478
+ * can start, how the pre-roll window is cut; the stats count it as one picture
10479
+ * and time the GOP from one `keyframe` to the next. A multi-slice camera (a
10480
+ * Hikvision G2 main stream is 3 slices per picture) used to hand each slice —
10481
+ * and each prefix SEI — down as its own "frame": 75 input fps on a 25 fps
10482
+ * camera, an 8–34 ms "GOP", a clip ring that widened to the LAST IRAP slice
10483
+ * (a third of the picture, without its parameter sets), and a prefix SEI that
10484
+ * inherited the welded VPS/SPS/PPS instead of the IDR.
10485
+ *
10486
+ * Boundary rule (RFC 3550 / 6184 / 7798): the RTP marker bit closes the
10487
+ * picture it ends — flushed on the spot, so a single-slice stream pays no
10488
+ * latency — and a timestamp change closes the one before it, for cameras that
10489
+ * never set the marker. Parameter sets are held and welded onto the picture
10490
+ * that follows them, so a keyframe access unit is self-contained
10491
+ * (VPS/SPS/PPS + IDR) exactly as before; a prefix SEI stays inside its
10492
+ * picture and is never a frame of its own.
10493
+ */
10494
+ var AccessUnitAssembler = class {
10495
+ onAccessUnit;
10496
+ /** Parameter sets seen since the last picture closed; welded onto the next one. */
10497
+ pendingParamNals = [];
10498
+ /** NALs of the picture in progress (parameter sets first when welded). */
10499
+ pictureNals = [];
10500
+ pictureTimestamp = 0;
10501
+ pictureKeyframe = false;
10502
+ constructor(onAccessUnit) {
10503
+ this.onAccessUnit = onAccessUnit;
10504
+ }
10505
+ push(depacketized) {
10506
+ const { annexB, keyframe, parameterSet, timestamp, marker } = depacketized;
10507
+ if (this.pictureNals.length > 0 && timestamp !== this.pictureTimestamp) this.flush();
10508
+ if (parameterSet) {
10509
+ this.pendingParamNals.push(annexB);
10510
+ return;
10511
+ }
10512
+ if (this.pictureNals.length === 0) {
10513
+ this.pictureTimestamp = timestamp;
10514
+ this.pictureKeyframe = false;
10515
+ }
10516
+ if (this.pendingParamNals.length > 0) {
10517
+ for (const paramNal of this.pendingParamNals) this.pictureNals.push(paramNal);
10518
+ this.pendingParamNals = [];
10519
+ this.pictureKeyframe = true;
10520
+ }
10521
+ this.pictureNals.push(annexB);
10522
+ if (keyframe) this.pictureKeyframe = true;
10523
+ if (marker) this.flush();
10524
+ }
10525
+ /**
10526
+ * Emit the picture in progress, if any. Called on the marker bit, on a
10527
+ * timestamp change, and by the broker when the stream stops or redials so
10528
+ * the last picture of a marker-less stream is not lost.
10529
+ */
10530
+ flush() {
10531
+ if (this.pictureNals.length === 0) return;
10532
+ const nals = this.pictureNals;
10533
+ const timestamp = this.pictureTimestamp;
10534
+ const keyframe = this.pictureKeyframe;
10535
+ this.pictureNals = [];
10536
+ this.pictureKeyframe = false;
10537
+ const annexB = nals.length === 1 ? nals[0] : Buffer.concat(nals);
10538
+ this.onAccessUnit({
10539
+ annexB,
10540
+ timestamp,
10541
+ keyframe
10542
+ });
10543
+ }
10544
+ /** Drop everything held — a redial re-bases the clock, nothing carries over. */
10545
+ reset() {
10546
+ this.pendingParamNals = [];
10547
+ this.pictureNals = [];
10548
+ this.pictureTimestamp = 0;
10549
+ this.pictureKeyframe = false;
10550
+ }
10551
+ };
10552
+ //#endregion
10330
10553
  //#region src/stream-broker/rtsp/rtsp-client.ts
10331
10554
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
10332
10555
  var DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
@@ -11295,6 +11518,49 @@ var AudioCodecSession = class {
11295
11518
  }
11296
11519
  };
11297
11520
  //#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
11298
11564
  //#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
11299
11565
  /**
11300
11566
  * Time-based ring buffer for encoded video packets.
@@ -11305,6 +11571,12 @@ var AudioCodecSession = class {
11305
11571
  *
11306
11572
  * The buffer always starts from the most recent keyframe to ensure
11307
11573
  * consumers can decode the returned data.
11574
+ *
11575
+ * Every entry OWNS its backing store (`owned-buffer.ts`): a sub-stream NAL
11576
+ * arrives as a view of a shared 8 KiB pool slab, and a ring that kept it for
11577
+ * 60 s pinned the slab and every short-lived packet beside it — the clip
11578
+ * rings of ~70 brokers were the largest holder of the hub broker's
11579
+ * 550–980 MB of ArrayBuffers. The byte budget charges what an entry pins.
11308
11580
  */
11309
11581
  var EncodedRingBuffer = class {
11310
11582
  packets = [];
@@ -11336,13 +11608,25 @@ var EncodedRingBuffer = class {
11336
11608
  getDuration() {
11337
11609
  return this.maxDurationMs / 1e3;
11338
11610
  }
11339
- /** Push a new packet into the buffer and evict expired ones. */
11611
+ /**
11612
+ * Push a new packet into the buffer and evict expired ones.
11613
+ *
11614
+ * A pooled payload is copied into its own allocation on the way in; one
11615
+ * that already owns its ArrayBuffer is stored as-is. The consumers fanned
11616
+ * out from the same packet keep the original, so the copy is the ring's
11617
+ * alone and is freed with it.
11618
+ */
11340
11619
  push(packet) {
11341
- this.packets.push(packet);
11342
- this.bytes += packet.data.length;
11620
+ const data = detachFromPool(packet.data);
11621
+ const stored = data === packet.data ? packet : {
11622
+ ...packet,
11623
+ data
11624
+ };
11625
+ this.packets.push(stored);
11626
+ this.bytes += backingStoreBytes(data);
11343
11627
  this.evict();
11344
11628
  }
11345
- /** What the retained window currently costs, in bytes. */
11629
+ /** What the retained window currently costs, in bytes of backing store. */
11346
11630
  getBufferedBytes() {
11347
11631
  return this.bytes;
11348
11632
  }
@@ -11420,14 +11704,14 @@ var EncodedRingBuffer = class {
11420
11704
  let removeCount = 0;
11421
11705
  while (removeCount < this.packets.length && (this.packets[removeCount].pts < cutoff || this.packets[removeCount].pts > newest)) removeCount++;
11422
11706
  if (removeCount > 0) {
11423
- for (let i = 0; i < removeCount; i++) this.bytes -= this.packets[i].data.length;
11707
+ for (let i = 0; i < removeCount; i++) this.bytes -= backingStoreBytes(this.packets[i].data);
11424
11708
  this.packets.splice(0, removeCount);
11425
11709
  }
11426
11710
  if (this.maxBytes > 0 && this.bytes > this.maxBytes) {
11427
11711
  this.clampedByBytes = true;
11428
11712
  let dropped = 0;
11429
11713
  while (dropped < this.packets.length && this.bytes > this.maxBytes) {
11430
- this.bytes -= this.packets[dropped].data.length;
11714
+ this.bytes -= backingStoreBytes(this.packets[dropped].data);
11431
11715
  dropped++;
11432
11716
  }
11433
11717
  if (dropped > 0) this.packets.splice(0, dropped);
@@ -13091,7 +13375,7 @@ var StreamBroker = class StreamBroker {
13091
13375
  this.rtspRestreamer.destroy();
13092
13376
  this.preBuffer.clear();
13093
13377
  this.clipBuffer?.clear();
13094
- this.pendingParamNals = [];
13378
+ this.annexBAssembler.reset();
13095
13379
  this.encodedCallbacks.clear();
13096
13380
  this.audioChunkPlane.dispose();
13097
13381
  }
@@ -13554,7 +13838,7 @@ var StreamBroker = class StreamBroker {
13554
13838
  this.reconcilePrestreamWindow();
13555
13839
  this.preBuffer.clear();
13556
13840
  this.clipBuffer?.clear();
13557
- this.pendingParamNals = [];
13841
+ this.annexBAssembler.reset();
13558
13842
  this.rtspRestreamer.resetStreamState();
13559
13843
  this.totalBytes = 0;
13560
13844
  this.bytesInWindow = 0;
@@ -13863,8 +14147,9 @@ var StreamBroker = class StreamBroker {
13863
14147
  captureRtpForPreBuffer(rtpData) {
13864
14148
  const marker = rtpData.length > 1 && (rtpData[1] & 128) !== 0;
13865
14149
  if (this.rtpRingPrevMarker || this.rtpRing.length === 0) this.rtpRingCurAuStart = this.rtpRing.length;
13866
- this.rtpRing.push(rtpData);
13867
- this.rtpRingBytes += rtpData.length;
14150
+ const owned = detachFromPool(rtpData);
14151
+ this.rtpRing.push(owned);
14152
+ this.rtpRingBytes += backingStoreBytes(owned);
13868
14153
  if (this._lastNalParamSet || this._lastNalKeyframe) {
13869
14154
  const auStart = this.rtpRingCurAuStart;
13870
14155
  const lastMark = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1];
@@ -14042,18 +14327,18 @@ var StreamBroker = class StreamBroker {
14042
14327
  this.reportPreRollClamp(budget, retentionMs);
14043
14328
  const historyEnd = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1].index;
14044
14329
  let retained = 0;
14045
- for (let i = cut; i < historyEnd; i++) retained += this.rtpRing[i].length;
14330
+ for (let i = cut; i < historyEnd; i++) retained += backingStoreBytes(this.rtpRing[i]);
14046
14331
  let markIdx = this.rtpRingKeyframeMarks.findIndex((m) => m.index === cut);
14047
14332
  while (retained > budget.bytes && markIdx >= 0 && markIdx < this.rtpRingKeyframeMarks.length - 1) {
14048
14333
  const nextCut = this.rtpRingKeyframeMarks[markIdx + 1].index;
14049
- for (let i = cut; i < nextCut; i++) retained -= this.rtpRing[i].length;
14334
+ for (let i = cut; i < nextCut; i++) retained -= backingStoreBytes(this.rtpRing[i]);
14050
14335
  cut = nextCut;
14051
14336
  markIdx++;
14052
14337
  }
14053
14338
  }
14054
14339
  if (cut <= 0) return;
14055
14340
  const dropped = this.rtpRing.splice(0, cut);
14056
- for (const b of dropped) this.rtpRingBytes -= b.length;
14341
+ for (const b of dropped) this.rtpRingBytes -= backingStoreBytes(b);
14057
14342
  this.rtpRingKeyframeMarks = this.rtpRingKeyframeMarks.filter((mark) => mark.index >= cut).map((mark) => ({
14058
14343
  index: mark.index - cut,
14059
14344
  ts: mark.ts
@@ -14393,11 +14678,15 @@ var StreamBroker = class StreamBroker {
14393
14678
  if (clamped === this.clipRetentionSec) return;
14394
14679
  const previous = this.clipRetentionSec;
14395
14680
  this.clipRetentionSec = clamped;
14396
- this.logger?.info(clamped === 0 ? "Clip retention off" : "Clip retention set", { meta: {
14397
- seconds: clamped,
14398
- requested: seconds,
14399
- previousSeconds: previous
14400
- } });
14681
+ this.logger?.info(clamped === 0 ? "Clip retention off" : "Clip retention set", {
14682
+ tags: { deviceId: this.numericDeviceId },
14683
+ meta: {
14684
+ seconds: clamped,
14685
+ requested: seconds,
14686
+ previousSeconds: previous,
14687
+ capBytes: StreamBroker.CLIP_RING_MAX_BYTES
14688
+ }
14689
+ });
14401
14690
  if (clamped === 0) {
14402
14691
  this.clipBuffer = null;
14403
14692
  return;
@@ -14630,8 +14919,9 @@ var StreamBroker = class StreamBroker {
14630
14919
  } });
14631
14920
  this._lastNalKeyframe = false;
14632
14921
  this._lastNalParamSet = false;
14633
- this.rtpDepacketizer?.processPacket(rtpData);
14634
- this.rtspRestreamer.pushRtpPassthrough(rtpData, this._lastNalKeyframe, this._lastNalParamSet);
14922
+ const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
14923
+ const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
14924
+ this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
14635
14925
  this.captureRtpForPreBuffer(rtpData);
14636
14926
  if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
14637
14927
  cb(rtpData);
@@ -14827,51 +15117,44 @@ var StreamBroker = class StreamBroker {
14827
15117
  * RTSP restreamer's pushRtpPassthrough.
14828
15118
  */
14829
15119
  /**
14830
- * Pending parameter-set NALs (SPS/PPS for H.264, VPS/SPS/PPS for H.265).
14831
- * Cameras emit these as separate RTP packets before the IDR/IRAP frame.
14832
- * We buffer them here and prepend to the next VCL NAL so downstream
14833
- * consumers (WebRTC, prebuffer, restream) receive complete access units
14834
- * with a single `EncodedPacket` that contains SPS+PPS+IDR together.
15120
+ * Groups the depacketizer's NALs into access units — ONE `EncodedPacket`
15121
+ * per picture, parameter sets welded onto the keyframe picture they
15122
+ * precede. See `access-unit-assembler.ts` for the boundary rule and the
15123
+ * per-NAL emit it replaced.
14835
15124
  */
14836
- pendingParamNals = [];
15125
+ annexBAssembler = new AccessUnitAssembler((accessUnit) => this.emitAccessUnit(accessUnit));
15126
+ /** Access-unit membership of the source RTP packets, for the restreamer's flags. */
15127
+ rtpAuTracker = new RtpAccessUnitTracker();
14837
15128
  /**
14838
15129
  * handleDepacketizedNal receives individual NALs from the RTP depacketizer
14839
- * and emits them as EncodedPackets.
14840
- *
14841
- * Parameter-set NALs (SPS/PPS type 7/8 for H.264, VPS/SPS/PPS type 32/33/34
14842
- * for H.265) are buffered and prepended to the next VCL NAL (slice/IDR).
14843
- * This guarantees every keyframe EncodedPacket is a self-contained access
14844
- * unit that a decoder can initialise from — critical for WebRTC (Chrome
14845
- * can't decode an IDR without preceding SPS/PPS).
15130
+ * and hands them to the access-unit assembler, which emits one
15131
+ * `EncodedPacket` per PICTURE (`emitAccessUnit`) — never per NAL. A
15132
+ * multi-slice IDR, its parameter sets and its prefix SEI are one keyframe
15133
+ * packet; three P slices are one P packet.
14846
15134
  *
14847
- * The RTSP restreamer handles its own RTP passthrough separately and is
14848
- * not affected by this merging.
15135
+ * The RTSP restreamer forwards source RTP separately (`onVideoRtp`) and is
15136
+ * not fed from here.
14849
15137
  */
14850
15138
  handleDepacketizedNal(depacketized) {
14851
- const { nal, keyframe, parameterSet, timestamp } = depacketized;
15139
+ if (depacketized.keyframe) this._lastNalKeyframe = true;
15140
+ if (depacketized.parameterSet) this._lastNalParamSet = true;
15141
+ this.annexBAssembler.push(depacketized);
15142
+ }
15143
+ /**
15144
+ * One assembled access unit → one `EncodedPacket` for every Annex-B
15145
+ * consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
15146
+ * self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
15147
+ * initialises from and what a clip or a pre-roll window may start at.
15148
+ */
15149
+ emitAccessUnit(accessUnit) {
14852
15150
  const codec = this.detectedCodec ?? "h264";
14853
- if (keyframe) this._lastNalKeyframe = true;
14854
- if (parameterSet) this._lastNalParamSet = true;
14855
- const isH264 = codec === "h264";
14856
- const nalType = isH264 ? nal[0] & 31 : (nal[0] & 126) >> 1;
14857
- if (isH264 ? nalType === 7 || nalType === 8 : nalType === 32 || nalType === 33 || nalType === 34) {
14858
- this.pendingParamNals.push(depacketized.annexB);
14859
- return;
14860
- }
14861
- const hadParamSets = this.pendingParamNals.length > 0;
14862
- let annexBData;
14863
- if (hadParamSets) {
14864
- this.pendingParamNals.push(depacketized.annexB);
14865
- annexBData = Buffer.concat(this.pendingParamNals);
14866
- this.pendingParamNals = [];
14867
- } else annexBData = depacketized.annexB;
14868
- const pts = Math.floor(timestamp / 90);
15151
+ const pts = Math.floor(accessUnit.timestamp / 90);
14869
15152
  const packet = {
14870
15153
  type: "video",
14871
- data: annexBData,
15154
+ data: accessUnit.annexB,
14872
15155
  pts,
14873
15156
  dts: pts,
14874
- keyframe: keyframe || hadParamSets,
15157
+ keyframe: accessUnit.keyframe,
14875
15158
  codec
14876
15159
  };
14877
15160
  this.pushEncodedPacket(packet);
@@ -15275,6 +15558,9 @@ var StreamBroker = class StreamBroker {
15275
15558
  this.rtpDepacketizer.reset();
15276
15559
  this.rtpDepacketizer = null;
15277
15560
  }
15561
+ this.annexBAssembler.flush();
15562
+ this.annexBAssembler.reset();
15563
+ this.rtpAuTracker.reset();
15278
15564
  if (this.pushRtpFlagDepacketizer) {
15279
15565
  this.pushRtpFlagDepacketizer.reset();
15280
15566
  this.pushRtpFlagDepacketizer = null;