@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
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-BX9VTm12.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-C8bbmV3c.mjs";
2
+ import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-BshC0NZR.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-Dqmrb990.mjs";
4
4
  import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-BhnT__dA.mjs";
7
+ import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-COoPqx93.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os$1 from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -3081,6 +3081,68 @@ var RtpPacketizer = class {
3081
3081
  }
3082
3082
  };
3083
3083
  //#endregion
3084
+ //#region src/stream-broker/stream-broker/owned-buffer.ts
3085
+ /**
3086
+ * Buffers that enter a LONG-LIVED ring own their backing store.
3087
+ *
3088
+ * Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
3089
+ * below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
3090
+ * That is the right choice for the ingest path — a start code, a 4-byte
3091
+ * interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
3092
+ * within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
3093
+ * for 60 s in a clip ring pins its whole slab, and with it every short-lived
3094
+ * packet that happened to be allocated beside it. On the hub that held
3095
+ * 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
3096
+ * pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
3097
+ *
3098
+ * The ring byte caps counted payload bytes, so they never saw it. Both rings
3099
+ * now pass every entry through {@link detachFromPool} on the way in and
3100
+ * account it with {@link backingStoreBytes}, so the cap measures what the
3101
+ * entry costs. Short-lived traffic stays pooled.
3102
+ */
3103
+ /** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
3104
+ function ownsBackingStore(buf) {
3105
+ return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
3106
+ }
3107
+ /**
3108
+ * What retaining `buf` really costs: its own length when it owns its
3109
+ * ArrayBuffer, the whole shared slab when it is a view of one.
3110
+ */
3111
+ function backingStoreBytes(buf) {
3112
+ return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
3113
+ }
3114
+ /**
3115
+ * `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
3116
+ * buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
3117
+ * runtime detail), else a copy that does. One copy, never two: a buffer that is
3118
+ * already unpooled is returned as-is.
3119
+ */
3120
+ function detachFromPool(buf) {
3121
+ if (ownsBackingStore(buf)) return buf;
3122
+ const owned = Buffer.allocUnsafeSlow(buf.length);
3123
+ buf.copy(owned);
3124
+ return owned;
3125
+ }
3126
+ /**
3127
+ * A copy of `src[start, end)` that OWNS its backing store — the shape a reader
3128
+ * hands out when the packet it cuts from its socket buffer is going to be
3129
+ * retained anyway.
3130
+ *
3131
+ * Every source video packet is: `captureRtpForPreBuffer` keeps it for the life
3132
+ * of a GOP, and the restreamer's keyframe cache keeps the keyframe ones. With
3133
+ * the readers cutting packets as `Buffer.from(subarray)` — a pooled view — the
3134
+ * ring then had to copy each one a second time to own it: two copies of every
3135
+ * packet on the ingest path (measured 15.1 + 14.8 MB/s on the hub's load).
3136
+ * Cutting the packet owned up front makes the ring's and the cache's
3137
+ * `detachFromPool` an identity, so the packet is copied once and shared by the
3138
+ * reader's fan-out, the ring, the cache and every session's write queue.
3139
+ */
3140
+ function copyOwned(src, start, end) {
3141
+ const owned = Buffer.allocUnsafeSlow(end - start);
3142
+ src.copy(owned, 0, start, end);
3143
+ return owned;
3144
+ }
3145
+ //#endregion
3084
3146
  //#region src/stream-broker/rtsp/sdp-builder.ts
3085
3147
  function buildSdp(options) {
3086
3148
  const { codec, parameterSets, streamName } = options;
@@ -3751,7 +3813,7 @@ var RtspRestreamer = class {
3751
3813
  if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
3752
3814
  if (this.collectingKeyframeRtp) {
3753
3815
  this.lastKeyframeRtpPackets.push({
3754
- data: Buffer.from(rtpData),
3816
+ data: detachFromPool(rtpData),
3755
3817
  marker: rtpPacket.marker
3756
3818
  });
3757
3819
  this.lastKeyframeRtpBytes += rtpData.length;
@@ -4353,6 +4415,19 @@ var RtspSession = class {
4353
4415
  lastRtpAt = 0;
4354
4416
  /** Total video RTP bytes written to the socket. */
4355
4417
  bytesSent = 0;
4418
+ /**
4419
+ * The socket is corked for the access unit in progress — see
4420
+ * {@link writeInterleaved}. Cleared by the packet that ends the picture (RTP
4421
+ * marker, or any audio packet) or by the next-tick safety net, whichever
4422
+ * comes first.
4423
+ */
4424
+ accessUnitCorked = false;
4425
+ /** Flush the access unit in progress. Idempotent: the marker and the safety net both call it. */
4426
+ uncorkAccessUnit = () => {
4427
+ if (!this.accessUnitCorked) return;
4428
+ this.accessUnitCorked = false;
4429
+ this.socket.uncork();
4430
+ };
4356
4431
  /** Client `User-Agent` (from the first request that carries one). Lets the
4357
4432
  * broker panel identify a consumer by purpose — e.g. the recorder sets
4358
4433
  * `-user_agent recorder` on its RTSP pull — instead of a bare address. */
@@ -4600,7 +4675,7 @@ var RtspSession = class {
4600
4675
  return "dropped-backlog";
4601
4676
  }
4602
4677
  }
4603
- this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
4678
+ this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
4604
4679
  this.missedKeyframes = 0;
4605
4680
  return "written";
4606
4681
  }
@@ -4609,7 +4684,7 @@ var RtspSession = class {
4609
4684
  if (!this.playing || this.closed || this.muted) return "not-playing";
4610
4685
  if (this.tracks.length < 2) return "not-playing";
4611
4686
  if (this.socket.writableLength > 2097152) return "dropped-backlog";
4612
- this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
4687
+ this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
4613
4688
  return "written";
4614
4689
  }
4615
4690
  /**
@@ -4631,14 +4706,13 @@ var RtspSession = class {
4631
4706
  * ## Why sharing the payload is safe, and what would make it unsafe
4632
4707
  *
4633
4708
  * 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
4709
+ * `copyOwned(readBuffer, …)` in the readers' `onData`, for video and audio
4710
+ * alike — never a view of the reader's socket buffer, which advances on the
4711
+ * next read. Nothing in the broker writes into a captured packet: every
4712
+ * mutation in this path targets a freshly allocated buffer. (The same object
4713
+ * is what the broker's `rtpRing` and the restreamer's keyframe cache retain —
4714
+ * it owns its backing store, so `detachFromPool` keeps it as is — and that is
4715
+ * fine: a pending write holds its own reference, and the rule below is about
4642
4716
  * mutation, not retention.)
4643
4717
  *
4644
4718
  * The rule to keep: **`socket.write` forbids MUTATION before the write
@@ -4648,18 +4722,36 @@ var RtspSession = class {
4648
4722
  * queued. Four bytes from the Buffer pool is a pointer bump; the copy this
4649
4723
  * replaces was ~1.4 KB.
4650
4724
  *
4651
- * `cork`/`uncork` keeps the pair a single `writev` syscall, so removing the
4652
- * copy does not buy an extra write syscall per packet.
4725
+ * ## One cork per ACCESS UNIT, not per packet
4726
+ *
4727
+ * The pair used to be corked and uncorked on its own: one `writev` per RTP
4728
+ * packet per session, so a 4K keyframe fragmented into a hundred packets and
4729
+ * fanned out to three sessions was three hundred syscalls for one picture no
4730
+ * decoder can use before it is whole. The socket is corked on the first write
4731
+ * of an access unit and uncorked by the packet that ENDS it
4732
+ * (`endOfAccessUnit`: the RTP marker on video, every audio packet), so the
4733
+ * picture leaves in one `writev`. A `process.nextTick` safety net uncorks
4734
+ * whatever is still open before the tick yields to I/O — a marker-less camera
4735
+ * or a lost marker degrades to one `writev` per tick, never to a socket held
4736
+ * corked across a read.
4737
+ *
4738
+ * Backpressure sees through the cork: `writableLength` counts corked bytes,
4739
+ * so the `MAX_WRITE_BUFFER` gate, the latch and the missed-keyframe close
4740
+ * (D447) judge exactly what they judged per packet.
4653
4741
  */
4654
- writeInterleaved(channel, payload) {
4742
+ writeInterleaved(channel, payload, endOfAccessUnit) {
4655
4743
  const header = Buffer.allocUnsafe(4);
4656
4744
  header[0] = 36;
4657
4745
  header[1] = channel;
4658
4746
  header.writeUInt16BE(payload.length, 2);
4659
- this.socket.cork();
4747
+ if (!this.accessUnitCorked) {
4748
+ this.accessUnitCorked = true;
4749
+ this.socket.cork();
4750
+ process.nextTick(this.uncorkAccessUnit);
4751
+ }
4660
4752
  this.socket.write(header);
4661
4753
  this.socket.write(payload);
4662
- this.socket.uncork();
4754
+ if (endOfAccessUnit) this.uncorkAccessUnit();
4663
4755
  this.lastRtpAt = Date.now();
4664
4756
  this.bytesSent += 4 + payload.length;
4665
4757
  }
@@ -10083,7 +10175,7 @@ var Rfc4571Reader = class {
10083
10175
  while (this.readBuffer.length >= 2) {
10084
10176
  const length = this.readBuffer.readUInt16BE(0);
10085
10177
  if (this.readBuffer.length < 2 + length) return;
10086
- const rtpPacket = Buffer.from(this.readBuffer.subarray(2, 2 + length));
10178
+ const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
10087
10179
  this.readBuffer = this.readBuffer.subarray(2 + length);
10088
10180
  this.dispatchRtp(rtpPacket);
10089
10181
  }
@@ -10144,6 +10236,93 @@ function totalFragmentBytes(fragments) {
10144
10236
  for (const fragment of fragments) total += fragment.length;
10145
10237
  return total;
10146
10238
  }
10239
+ /** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
10240
+ function classifyH264Payload(payload) {
10241
+ const nalType = payload[0] & 31;
10242
+ if (nalType === H264_NAL_STAP_A) {
10243
+ let keyframe = false;
10244
+ let parameterSet = false;
10245
+ let offset = 1;
10246
+ while (offset + 2 <= payload.length) {
10247
+ const nalSize = payload.readUInt16BE(offset);
10248
+ offset += 2;
10249
+ if (nalSize === 0 || offset + nalSize > payload.length) break;
10250
+ const type = payload[offset] & 31;
10251
+ keyframe ||= type === H264_NAL_IDR;
10252
+ parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
10253
+ offset += nalSize;
10254
+ }
10255
+ return {
10256
+ keyframe,
10257
+ parameterSet
10258
+ };
10259
+ }
10260
+ if (nalType === 28) {
10261
+ if (payload.length < 2) return NO_PAYLOAD_CLASS;
10262
+ return {
10263
+ keyframe: (payload[1] & 31) === H264_NAL_IDR,
10264
+ parameterSet: false
10265
+ };
10266
+ }
10267
+ return {
10268
+ keyframe: nalType === H264_NAL_IDR,
10269
+ parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
10270
+ };
10271
+ }
10272
+ /** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
10273
+ function classifyH265Payload(payload) {
10274
+ if (payload.length < 2) return NO_PAYLOAD_CLASS;
10275
+ const nalType = payload[0] >> 1 & 63;
10276
+ if (nalType === H265_NAL_AP) {
10277
+ let keyframe = false;
10278
+ let parameterSet = false;
10279
+ let offset = 2;
10280
+ while (offset + 2 <= payload.length) {
10281
+ const nalSize = payload.readUInt16BE(offset);
10282
+ offset += 2;
10283
+ if (nalSize === 0 || offset + nalSize > payload.length) break;
10284
+ const type = payload[offset] >> 1 & 63;
10285
+ keyframe ||= isH265IrapType(type);
10286
+ parameterSet ||= isH265ParameterSetType(type);
10287
+ offset += nalSize;
10288
+ }
10289
+ return {
10290
+ keyframe,
10291
+ parameterSet
10292
+ };
10293
+ }
10294
+ if (nalType === 49) {
10295
+ if (payload.length < 3) return NO_PAYLOAD_CLASS;
10296
+ return {
10297
+ keyframe: isH265IrapType(payload[2] & 63),
10298
+ parameterSet: false
10299
+ };
10300
+ }
10301
+ return {
10302
+ keyframe: isH265IrapType(nalType),
10303
+ parameterSet: isH265ParameterSetType(nalType)
10304
+ };
10305
+ }
10306
+ /**
10307
+ * Classify one raw RTP packet (12-byte header + payload) by what it carries —
10308
+ * the per-packet half of {@link RtpDepacketizer.processPacket}, without the
10309
+ * reassembly half. Pure: reads the headers, allocates nothing, keeps no state.
10310
+ * `null` for a packet too short to be RTP with a payload.
10311
+ */
10312
+ function classifyRtpPacket(codec, rtpData) {
10313
+ if (rtpData.length < 12) return null;
10314
+ const marker = (rtpData[1] & 128) !== 0;
10315
+ const timestamp = rtpData.readUInt32BE(4);
10316
+ const payload = rtpData.subarray(12);
10317
+ if (payload.length === 0) return null;
10318
+ const carried = codec === "h264" ? classifyH264Payload(payload) : classifyH265Payload(payload);
10319
+ return {
10320
+ timestamp,
10321
+ marker,
10322
+ keyframe: carried.keyframe,
10323
+ parameterSet: carried.parameterSet
10324
+ };
10325
+ }
10147
10326
  /**
10148
10327
  * RTP → Annex-B NAL depacketizer.
10149
10328
  *
@@ -10188,87 +10367,22 @@ var RtpDepacketizer = class {
10188
10367
  * short to be RTP with a payload.
10189
10368
  */
10190
10369
  processPacket(rtpData) {
10191
- if (rtpData.length < 12) return null;
10192
- const marker = (rtpData[1] & 128) !== 0;
10193
- const timestamp = rtpData.readUInt32BE(4);
10370
+ const carried = this.classifyPacket(rtpData);
10371
+ if (carried === null) return null;
10194
10372
  const payload = rtpData.subarray(12);
10195
- if (payload.length === 0) return null;
10196
- const carried = this.codec === "h264" ? this.classifyH264Payload(payload) : this.classifyH265Payload(payload);
10197
- if (this.codec === "h264") this.processH264(payload, timestamp, marker);
10198
- else this.processH265(payload, timestamp, marker);
10199
- return {
10200
- timestamp,
10201
- marker,
10202
- keyframe: carried.keyframe,
10203
- parameterSet: carried.parameterSet
10204
- };
10373
+ if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
10374
+ else this.processH265(payload, carried.timestamp, carried.marker);
10375
+ return carried;
10205
10376
  }
10206
- /** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
10207
- classifyH264Payload(payload) {
10208
- const nalType = payload[0] & 31;
10209
- if (nalType === H264_NAL_STAP_A) {
10210
- let keyframe = false;
10211
- let parameterSet = false;
10212
- let offset = 1;
10213
- while (offset + 2 <= payload.length) {
10214
- const nalSize = payload.readUInt16BE(offset);
10215
- offset += 2;
10216
- if (nalSize === 0 || offset + nalSize > payload.length) break;
10217
- const type = payload[offset] & 31;
10218
- keyframe ||= type === H264_NAL_IDR;
10219
- parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
10220
- offset += nalSize;
10221
- }
10222
- return {
10223
- keyframe,
10224
- parameterSet
10225
- };
10226
- }
10227
- if (nalType === 28) {
10228
- if (payload.length < 2) return NO_PAYLOAD_CLASS;
10229
- return {
10230
- keyframe: (payload[1] & 31) === H264_NAL_IDR,
10231
- parameterSet: false
10232
- };
10233
- }
10234
- return {
10235
- keyframe: nalType === H264_NAL_IDR,
10236
- parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
10237
- };
10238
- }
10239
- /** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
10240
- classifyH265Payload(payload) {
10241
- if (payload.length < 2) return NO_PAYLOAD_CLASS;
10242
- const nalType = payload[0] >> 1 & 63;
10243
- if (nalType === H265_NAL_AP) {
10244
- let keyframe = false;
10245
- let parameterSet = false;
10246
- let offset = 2;
10247
- while (offset + 2 <= payload.length) {
10248
- const nalSize = payload.readUInt16BE(offset);
10249
- offset += 2;
10250
- if (nalSize === 0 || offset + nalSize > payload.length) break;
10251
- const type = payload[offset] >> 1 & 63;
10252
- keyframe ||= isH265IrapType(type);
10253
- parameterSet ||= isH265ParameterSetType(type);
10254
- offset += nalSize;
10255
- }
10256
- return {
10257
- keyframe,
10258
- parameterSet
10259
- };
10260
- }
10261
- if (nalType === 49) {
10262
- if (payload.length < 3) return NO_PAYLOAD_CLASS;
10263
- return {
10264
- keyframe: isH265IrapType(payload[2] & 63),
10265
- parameterSet: false
10266
- };
10267
- }
10268
- return {
10269
- keyframe: isH265IrapType(nalType),
10270
- parameterSet: isH265ParameterSetType(nalType)
10271
- };
10377
+ /**
10378
+ * What this packet CARRIES, and nothing else: no NAL is reassembled, no buffer
10379
+ * allocated, no reassembly state touched. The same verdict `processPacket`
10380
+ * returns, for a broker whose Annex-B plane has no consumer — the source-RTP
10381
+ * ring, the restreamer's keyframe cache and the stats need exactly this and
10382
+ * none of the copies.
10383
+ */
10384
+ classifyPacket(rtpData) {
10385
+ return classifyRtpPacket(this.codec, rtpData);
10272
10386
  }
10273
10387
  processH264(payload, timestamp, marker) {
10274
10388
  const nalType = payload[0] & 31;
@@ -10448,11 +10562,13 @@ var RtpAccessUnitTracker = class {
10448
10562
  const keyframeContent = packet.keyframe || packet.parameterSet;
10449
10563
  const keyframeAuStart = keyframeContent && !this.keyframeAu;
10450
10564
  if (keyframeContent) this.keyframeAu = true;
10565
+ const pictureStart = !packet.parameterSet && !this.pictureSeen;
10451
10566
  if (!packet.parameterSet) this.pictureSeen = true;
10452
10567
  return {
10453
10568
  auStart,
10454
10569
  keyframeAu: this.keyframeAu,
10455
10570
  keyframeAuStart,
10571
+ pictureStart,
10456
10572
  parameterSet: packet.parameterSet
10457
10573
  };
10458
10574
  }
@@ -10778,7 +10894,7 @@ var NativeRtspClient = class {
10778
10894
  const channel = this.readBuffer[1];
10779
10895
  const length = this.readBuffer.readUInt16BE(2);
10780
10896
  if (this.readBuffer.length < 4 + length) return;
10781
- const payload = Buffer.from(this.readBuffer.subarray(4, 4 + length));
10897
+ const payload = copyOwned(this.readBuffer, 4, 4 + length);
10782
10898
  this.readBuffer = this.readBuffer.subarray(4 + length);
10783
10899
  this.handleInterleavedPacket(channel, payload);
10784
10900
  } else {
@@ -11514,49 +11630,6 @@ var AudioCodecSession = class {
11514
11630
  }
11515
11631
  };
11516
11632
  //#endregion
11517
- //#region src/stream-broker/stream-broker/owned-buffer.ts
11518
- /**
11519
- * Buffers that enter a LONG-LIVED ring own their backing store.
11520
- *
11521
- * Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
11522
- * below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
11523
- * That is the right choice for the ingest path — a start code, a 4-byte
11524
- * interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
11525
- * within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
11526
- * for 60 s in a clip ring pins its whole slab, and with it every short-lived
11527
- * packet that happened to be allocated beside it. On the hub that held
11528
- * 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
11529
- * pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
11530
- *
11531
- * The ring byte caps counted payload bytes, so they never saw it. Both rings
11532
- * now pass every entry through {@link detachFromPool} on the way in and
11533
- * account it with {@link backingStoreBytes}, so the cap measures what the
11534
- * entry costs. Short-lived traffic stays pooled.
11535
- */
11536
- /** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
11537
- function ownsBackingStore(buf) {
11538
- return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
11539
- }
11540
- /**
11541
- * What retaining `buf` really costs: its own length when it owns its
11542
- * ArrayBuffer, the whole shared slab when it is a view of one.
11543
- */
11544
- function backingStoreBytes(buf) {
11545
- return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
11546
- }
11547
- /**
11548
- * `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
11549
- * buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
11550
- * runtime detail), else a copy that does. One copy, never two: a buffer that is
11551
- * already unpooled is returned as-is.
11552
- */
11553
- function detachFromPool(buf) {
11554
- if (ownsBackingStore(buf)) return buf;
11555
- const owned = Buffer.allocUnsafeSlow(buf.length);
11556
- buf.copy(owned);
11557
- return owned;
11558
- }
11559
- //#endregion
11560
11633
  //#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
11561
11634
  /**
11562
11635
  * Time-based ring buffer for encoded video packets.
@@ -12496,9 +12569,6 @@ var StreamBroker = class StreamBroker {
12496
12569
  */
12497
12570
  ffmpegBinaryPathSource = () => null;
12498
12571
  rtpDepacketizer = null;
12499
- /** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
12500
- * (sets keyframe/param-set flags without re-emitting AnnexB packets). */
12501
- pushRtpFlagDepacketizer = null;
12502
12572
  audioRtpDecoder = null;
12503
12573
  audioCodecSession = null;
12504
12574
  /**
@@ -12866,10 +12936,24 @@ var StreamBroker = class StreamBroker {
12866
12936
  * log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
12867
12937
  * back but never video is observable from the structured logs. */
12868
12938
  videoRtpSeen = 0;
12869
- /** Tracking flags set synchronously by the RTP depacketizer callback. */
12939
+ /**
12940
+ * What the source RTP packet being handled CARRIES (keyframe / parameter-set
12941
+ * content, read from its payload headers by `classifyRtpPacket`) — the flags
12942
+ * `captureRtpForPreBuffer` reads. Per packet, not per completed NAL: every
12943
+ * fragment of a fragmented IDR carries them, and the ring marks one keyframe
12944
+ * AU for the lot.
12945
+ */
12870
12946
  _lastNalKeyframe = false;
12871
12947
  _lastNalParamSet = false;
12872
12948
  /**
12949
+ * Whether the Annex-B plane (NAL reassembly + access-unit assembly →
12950
+ * `EncodedPacket`) is being BUILT for this stream. False while nothing
12951
+ * consumes it: the source-RTP path then classifies each packet and copies
12952
+ * nothing. Armed at the next keyframe access unit once a consumer attaches
12953
+ * (`needsAnnexBPlane`), so the first packet it emits is decodable.
12954
+ */
12955
+ annexBPlaneActive = false;
12956
+ /**
12873
12957
  * Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
12874
12958
  * begin at the most recent keyframe access unit — i.e. the current GOP so
12875
12959
  * far. A late-joining WebRTC viewer on the repacketizer path replays this
@@ -13945,26 +14029,7 @@ var StreamBroker = class StreamBroker {
13945
14029
  return;
13946
14030
  }
13947
14031
  }
13948
- if (packet.type === "video") {
13949
- this.packetCount++;
13950
- this.totalBytes += packet.data.length;
13951
- this.bytesInWindow += packet.data.length;
13952
- this.packetsInWindow++;
13953
- this.lastPacketAt = Date.now();
13954
- const now = Date.now();
13955
- const windowMs = now - this.windowStartMs;
13956
- if (windowMs >= 2e3) {
13957
- this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
13958
- this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
13959
- this.bytesInWindow = 0;
13960
- this.packetsInWindow = 0;
13961
- this.windowStartMs = now;
13962
- }
13963
- if (packet.keyframe) {
13964
- if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
13965
- this.lastKeyframeMs = now;
13966
- }
13967
- }
14032
+ if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
13968
14033
  if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
13969
14034
  if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
13970
14035
  this.fanOutEncoded(packet);
@@ -14155,8 +14220,8 @@ var StreamBroker = class StreamBroker {
14155
14220
  index: auStart,
14156
14221
  ts: auPacket.readUInt32BE(4)
14157
14222
  });
14223
+ this.trimRtpRingToPreRollWindow();
14158
14224
  }
14159
- this.trimRtpRingToPreRollWindow();
14160
14225
  this.rtpRingHasKeyframe = true;
14161
14226
  }
14162
14227
  this.rtpRingPrevMarker = marker;
@@ -14425,10 +14490,10 @@ var StreamBroker = class StreamBroker {
14425
14490
  *
14426
14491
  * `push-rtp` providers deliver AnnexB-equivalents separately via
14427
14492
  * `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
14428
- * does NOT re-emit EncodedPackets; it only derives the per-packet
14429
- * keyframe/param-set flags the pre-buffer capture needs (via a
14430
- * flag-only depacketizer that never touches the AnnexB plane), keeping
14431
- * the AnnexB consumers (pipe, restreamer) single-fed by the provider.
14493
+ * does NOT re-emit EncodedPackets; it only classifies each packet for the
14494
+ * keyframe/param-set flags the pre-buffer capture needs
14495
+ * (`classifyRtpPacket`, no reassembly), keeping the AnnexB consumers
14496
+ * (pipe, restreamer) single-fed by the provider.
14432
14497
  *
14433
14498
  * The pre-buffer is captured even with zero subscribers attached — a
14434
14499
  * viewer that connects mid-GOP must be able to replay the GOP that
@@ -14438,9 +14503,9 @@ var StreamBroker = class StreamBroker {
14438
14503
  pushVideoRtp(rtpData) {
14439
14504
  if (this.stopping) return;
14440
14505
  this.kickRtpActivityWatchdog();
14441
- this._lastNalKeyframe = false;
14442
- this._lastNalParamSet = false;
14443
- this.ensurePushRtpFlagDepacketizer().processPacket(rtpData);
14506
+ const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
14507
+ this._lastNalKeyframe = carried?.keyframe ?? false;
14508
+ this._lastNalParamSet = carried?.parameterSet ?? false;
14444
14509
  this.captureRtpForPreBuffer(rtpData);
14445
14510
  if (this.rtpVideoCallbacks.size === 0) return;
14446
14511
  for (const [cb, entry] of this.rtpVideoCallbacks) try {
@@ -14450,27 +14515,6 @@ var StreamBroker = class StreamBroker {
14450
14515
  this.logger?.warn("rtp video subscriber threw", { meta: { error: errMsg(err) } });
14451
14516
  }
14452
14517
  }
14453
- /**
14454
- * Lazily build a flag-only RTP depacketizer for the `push-rtp` source
14455
- * path. Unlike the native reader's depacketizer (which emits AnnexB
14456
- * EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
14457
- * `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
14458
- * AnnexB plane through `pushEncodedPacket`, so re-emitting here would
14459
- * double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
14460
- * so the codec comes from the source declaration (`detectedCodec`, seeded
14461
- * in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
14462
- * starts with clean FU reassembly state.
14463
- */
14464
- ensurePushRtpFlagDepacketizer() {
14465
- if (!this.pushRtpFlagDepacketizer) {
14466
- const codec = this.detectedCodec === "h265" ? "h265" : "h264";
14467
- this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
14468
- if (keyframe) this._lastNalKeyframe = true;
14469
- if (parameterSet) this._lastNalParamSet = true;
14470
- });
14471
- }
14472
- return this.pushRtpFlagDepacketizer;
14473
- }
14474
14518
  /** Toggle the per-device streaming debug flag. */
14475
14519
  setStreamingDebug(enabled) {
14476
14520
  this.streamingDebug = enabled;
@@ -14913,10 +14957,17 @@ var StreamBroker = class StreamBroker {
14913
14957
  seq: this.videoRtpSeen,
14914
14958
  bytes: rtpData.length
14915
14959
  } });
14916
- this._lastNalKeyframe = false;
14917
- this._lastNalParamSet = false;
14918
- const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
14960
+ const wantPlane = this.needsAnnexBPlane();
14961
+ if (!wantPlane && this.annexBPlaneActive) this.idleAnnexBPlane();
14962
+ const carried = this.annexBPlaneActive ? this.rtpDepacketizer?.processPacket(rtpData) ?? null : this.rtpDepacketizer?.classifyPacket(rtpData) ?? null;
14963
+ this._lastNalKeyframe = carried?.keyframe ?? false;
14964
+ this._lastNalParamSet = carried?.parameterSet ?? false;
14919
14965
  const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
14966
+ if (wantPlane && !this.annexBPlaneActive && auFlags?.keyframeAuStart === true) {
14967
+ this.armAnnexBPlane();
14968
+ this.rtpDepacketizer?.processPacket(rtpData);
14969
+ }
14970
+ if (auFlags) this.recordVideoStats(rtpData.length, auFlags.pictureStart, auFlags.keyframeAu);
14920
14971
  this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
14921
14972
  this.captureRtpForPreBuffer(rtpData);
14922
14973
  if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
@@ -15132,11 +15183,83 @@ var StreamBroker = class StreamBroker {
15132
15183
  * not fed from here.
15133
15184
  */
15134
15185
  handleDepacketizedNal(depacketized) {
15135
- if (depacketized.keyframe) this._lastNalKeyframe = true;
15136
- if (depacketized.parameterSet) this._lastNalParamSet = true;
15137
15186
  this.annexBAssembler.push(depacketized);
15138
15187
  }
15139
15188
  /**
15189
+ * Does anything consume the Annex-B plane right now? An encoded subscriber
15190
+ * (WebRTC Annex-B, fragments, a decode session), a pipe client, a clip ring
15191
+ * (it retains access units continuously, so it needs them built even with
15192
+ * nobody attached), or a restreamer still in Annex-B mode (no camera SDP yet).
15193
+ * The RTSP restream and the WebRTC RTP path are served from the source RTP
15194
+ * and never count. Cheap — four field reads — because it runs per packet.
15195
+ */
15196
+ needsAnnexBPlane() {
15197
+ return this.encodedCallbacks.size > 0 || this.clipBuffer !== null || this.pipeServer.getClientCount() > 0 || !this.rtspRestreamer.rtpPassthroughMode;
15198
+ }
15199
+ /** The consumers that want the plane, for the arm/idle lines. */
15200
+ annexBPlaneConsumers() {
15201
+ return {
15202
+ encodedSubscribers: this.encodedCallbacks.size,
15203
+ clipRing: this.clipBuffer !== null,
15204
+ pipeClients: this.pipeServer.getClientCount(),
15205
+ restreamerAnnexB: !this.rtspRestreamer.rtpPassthroughMode
15206
+ };
15207
+ }
15208
+ /**
15209
+ * Start building access units from the keyframe AU the current packet opens.
15210
+ * The depacketizer and the assembler are clean here (reset when the plane
15211
+ * went idle, or never used), so nothing stale precedes that keyframe.
15212
+ */
15213
+ armAnnexBPlane() {
15214
+ this.annexBPlaneActive = true;
15215
+ this.logger?.info("annex-b plane armed at a keyframe access unit", {
15216
+ tags: { deviceId: this.numericDeviceId },
15217
+ meta: this.annexBPlaneConsumers()
15218
+ });
15219
+ }
15220
+ /**
15221
+ * Stop building access units: the last consumer left. The picture in
15222
+ * progress is dropped, not flushed — there is nobody to hand it to, and a
15223
+ * partial picture must never reach the next consumer's clean start. Logged,
15224
+ * once per transition: a plane that is idle on purpose must not read as a
15225
+ * stream that stopped.
15226
+ */
15227
+ idleAnnexBPlane() {
15228
+ this.annexBPlaneActive = false;
15229
+ this.rtpDepacketizer?.reset();
15230
+ this.annexBAssembler.reset();
15231
+ this.logger?.info("annex-b plane idle — no consumer, NAL reassembly skipped", { tags: { deviceId: this.numericDeviceId } });
15232
+ }
15233
+ /**
15234
+ * One place for the input stats, fed from whichever path owns the pictures:
15235
+ * the source RTP packets while the native reader is up (`onVideoRtp`), the
15236
+ * `EncodedPacket`s otherwise (`pushEncodedPacket`). `bytes` accumulate per
15237
+ * call; `inputFps`, `packetCount` and the IDR interval advance once per
15238
+ * picture — the window is closed on a picture start so a window of N frames
15239
+ * reads N, exactly as it did when every call was one access unit.
15240
+ */
15241
+ recordVideoStats(bytes, pictureStart, keyframe) {
15242
+ this.totalBytes += bytes;
15243
+ this.bytesInWindow += bytes;
15244
+ if (!pictureStart) return;
15245
+ this.packetCount++;
15246
+ this.packetsInWindow++;
15247
+ const now = Date.now();
15248
+ this.lastPacketAt = now;
15249
+ const windowMs = now - this.windowStartMs;
15250
+ if (windowMs >= 2e3) {
15251
+ this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
15252
+ this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
15253
+ this.bytesInWindow = 0;
15254
+ this.packetsInWindow = 0;
15255
+ this.windowStartMs = now;
15256
+ }
15257
+ if (keyframe) {
15258
+ if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
15259
+ this.lastKeyframeMs = now;
15260
+ }
15261
+ }
15262
+ /**
15140
15263
  * One assembled access unit → one `EncodedPacket` for every Annex-B
15141
15264
  * consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
15142
15265
  * self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
@@ -15557,10 +15680,7 @@ var StreamBroker = class StreamBroker {
15557
15680
  this.annexBAssembler.flush();
15558
15681
  this.annexBAssembler.reset();
15559
15682
  this.rtpAuTracker.reset();
15560
- if (this.pushRtpFlagDepacketizer) {
15561
- this.pushRtpFlagDepacketizer.reset();
15562
- this.pushRtpFlagDepacketizer = null;
15563
- }
15683
+ this.annexBPlaneActive = false;
15564
15684
  this.resetRtpPreBuffer();
15565
15685
  if (this.audioRtpDecoder) {
15566
15686
  this.audioRtpDecoder.flush();