@camstack/addon-pipeline 1.2.221 → 1.2.223

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 (28) hide show
  1. package/dist/audio-analyzer/index.js +55 -16
  2. package/dist/audio-analyzer/index.mjs +55 -16
  3. package/dist/{default-detection-model-B1NT1A38.js → default-detection-model-B7kn9xHR.js} +1 -1
  4. package/dist/{default-detection-model-DkVFgubU.mjs → default-detection-model-BSX8cVHY.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-CEFC3PkP.mjs → dist-BNzfbpF6.mjs} +31 -2
  8. package/dist/{dist-ATDfcxgL.js → dist-DZsjVFey.js} +31 -2
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-C5jnTgpv.js → node-BhQ8dDQC.js} +1 -1
  12. package/dist/{node-DR3fHqUq.mjs → node-uYIx2aZF.mjs} +1 -1
  13. package/dist/pipeline-runner/index.js +139 -20
  14. package/dist/pipeline-runner/index.mjs +139 -20
  15. package/dist/{process-memory-DQbCPnMA.js → process-memory-BozKb5-g.js} +1 -1
  16. package/dist/{process-memory-DqYj5IAb.mjs → process-memory-CkoLpOV-.mjs} +1 -1
  17. package/dist/recorder/index.js +90 -27
  18. package/dist/recorder/index.mjs +90 -27
  19. package/dist/{segment-demux-js-o9KBAz9p.js → segment-demux-js-Cvszh55p.js} +1 -1
  20. package/dist/{segment-demux-js-CddDwD69.mjs → segment-demux-js-Dh1dn_AT.mjs} +1 -1
  21. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BZG3prqU.mjs} +3 -3
  22. package/dist/stream-broker/demux-worker-child.js +1 -1
  23. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  24. package/dist/stream-broker/{hostInit-C-VdcJS2.mjs → hostInit-Cyo0CFo3.mjs} +3 -3
  25. package/dist/stream-broker/index.js +96 -54
  26. package/dist/stream-broker/index.mjs +96 -54
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/package.json +1 -1
@@ -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-ATDfcxgL.js");
7
- const require_node = require("../node-C5jnTgpv.js");
6
+ const require_dist = require("../dist-DZsjVFey.js");
7
+ const require_node = require("../node-BhQ8dDQC.js");
8
8
  const require_restream_intent = require("../restream-intent-B1Difcnq.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-o9KBAz9p.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-Cvszh55p.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");
@@ -9409,42 +9409,14 @@ function extractQuoted(header, key) {
9409
9409
  const unquotedRegex = new RegExp(`${key}=([^,\\s]+)`, "i");
9410
9410
  return header.match(unquotedRegex)?.[1];
9411
9411
  }
9412
- //#endregion
9413
- //#region src/stream-broker/stream-broker/audio-chunk-plane.ts
9414
- /**
9415
- * `AudioChunkPlane` — the broker's serialisable decoded audio-chunk surface
9416
- * (Phase 5 / D9).
9417
- *
9418
- * This is the poll-based replacement for `StreamBroker.onDecodedAudioChunk`'s
9419
- * live-object callback path. A live callback cannot cross a process boundary;
9420
- * once the `pipeline` group is dissolved (Task 8) the audio consumer
9421
- * (`pipeline-orchestrator`) runs in a different process from the broker, so
9422
- * audio delivery must go over tRPC like the video frame plane.
9423
- *
9424
- * ## Why no shared memory
9425
- *
9426
- * Unlike video frames (~6 MB at 1080p RGB), a decoded audio chunk is tiny — a
9427
- * ~500ms `AudioCodecSession` window is a few KB of Float32 PCM. Tiny payloads
9428
- * serialise over tRPC cheaply, so audio ships its bytes INLINE on the RPC
9429
- * wire; there is no shm ring.
9430
- *
9431
- * ## FIFO / no-drop queue policy
9432
- *
9433
- * Audio wants FIFO arrival order with no latest-wins drop: a missing chunk is
9434
- * an audible gap and breaks the audio analyzer's fixed-length analysis
9435
- * window. Each subscription therefore owns a generously-sized FIFO queue
9436
- * (`AUDIO_QUEUE_CAPACITY`). Chunks arrive roughly every
9437
- * `AudioCodecSession`-window (~500ms), so the queue buffers tens of seconds
9438
- * of audio — a healthy poll loop never fills it. The queue only drops its
9439
- * OLDEST chunk if a truly stalled consumer lets it overflow (a bounded safety
9440
- * valve, not the steady-state behaviour).
9441
- */
9442
9412
  /**
9443
- * Per-subscription FIFO capacity. A chunk is a few KB and arrives ~every
9444
- * 500ms, so 64 buffers ~30s of audio between two `pullAudioChunks` polls
9445
- * without unbounded growth.
9413
+ * Re-state a standing overflow at most this often (D465). The line fires when a
9414
+ * subscription STARTS overwriting, and again every 30 s while it still is — a
9415
+ * per-chunk warn is ~25 writes/s per camera of synchronous winston on the
9416
+ * broker's event loop, which is the cost the `pullPcm` warn coalescing exists
9417
+ * to avoid.
9446
9418
  */
9447
- var AUDIO_QUEUE_CAPACITY = 64;
9419
+ var OVERFLOW_RELOG_MS = 3e4;
9448
9420
  /**
9449
9421
  * How long a subscription may go without a `pull` before the plane releases it
9450
9422
  * (D461).
@@ -9515,10 +9487,13 @@ var AudioChunkPlane = class {
9515
9487
  id: subscriptionId,
9516
9488
  tag: input?.tag ?? "unknown",
9517
9489
  subscribedAt: Date.now(),
9518
- queue: new require_dist.RingBuffer(AUDIO_QUEUE_CAPACITY),
9490
+ queue: new require_dist.RingBuffer(64),
9519
9491
  accept: new Set(input?.accept ?? []),
9520
9492
  servedKey: null,
9521
9493
  chunksDelivered: 0,
9494
+ chunksDropped: 0,
9495
+ overflowing: false,
9496
+ lastOverflowLogAt: 0,
9522
9497
  lastPulledAt: Date.now()
9523
9498
  };
9524
9499
  this.subscriptions.set(subscriptionId, subscription);
@@ -9570,6 +9545,7 @@ var AudioChunkPlane = class {
9570
9545
  leaseMs: AUDIO_SUBSCRIPTION_IDLE_MS,
9571
9546
  queued: subscription.queue.size,
9572
9547
  chunksDelivered: subscription.chunksDelivered,
9548
+ chunksDropped: subscription.chunksDropped,
9573
9549
  hint: "the consumer process died without detaching — its subscription was holding this broker in demand"
9574
9550
  } });
9575
9551
  }
@@ -9605,13 +9581,47 @@ var AudioChunkPlane = class {
9605
9581
  for (const subscription of this.subscriptions.values()) {
9606
9582
  if (sourceFormat === "f32le" || subscription.accept.has(sourceFormat)) {
9607
9583
  this.noteServed(subscription, sourceFormat, sourceFormat);
9608
- subscription.queue.push(chunk);
9584
+ this.enqueue(subscription, chunk);
9609
9585
  continue;
9610
9586
  }
9611
9587
  expanded ??= expandChunk(chunk, sourceFormat);
9612
9588
  this.noteServed(subscription, sourceFormat, "f32le");
9613
- subscription.queue.push(expanded);
9589
+ this.enqueue(subscription, expanded);
9590
+ }
9591
+ }
9592
+ /**
9593
+ * Push one chunk into a subscription's FIFO and NAME the loss when the FIFO
9594
+ * had no room (D465).
9595
+ *
9596
+ * `RingBuffer.push` overwrites its oldest entry. Every docblock on this path
9597
+ * called that "a bounded safety valve, not the steady-state behaviour" and
9598
+ * nothing ever said when the valve opened, so a consumer polling too slowly
9599
+ * for its producer lost audio permanently and in silence — the exact shape
9600
+ * CLAUDE.md forbids. The realistic cause is a cadence chosen for the
9601
+ * BOUNDARY rather than for the producer's chunk rate: 500 ms of a ~42 ms AAC
9602
+ * chunk is ~12 chunks against a batch that used to drain 8.
9603
+ */
9604
+ enqueue(subscription, chunk) {
9605
+ const wasFull = subscription.queue.size >= 64;
9606
+ subscription.queue.push(chunk);
9607
+ if (!wasFull) {
9608
+ subscription.overflowing = false;
9609
+ return;
9614
9610
  }
9611
+ subscription.chunksDropped += 1;
9612
+ const now = Date.now();
9613
+ const isNewEpisode = !subscription.overflowing;
9614
+ subscription.overflowing = true;
9615
+ if (!isNewEpisode && now - subscription.lastOverflowLogAt < OVERFLOW_RELOG_MS) return;
9616
+ subscription.lastOverflowLogAt = now;
9617
+ this.logger?.warn("audio-chunk subscription OVERFLOW — oldest audio overwritten", { meta: {
9618
+ subscriptionId: subscription.id,
9619
+ tag: subscription.tag,
9620
+ capacity: 64,
9621
+ chunksDropped: subscription.chunksDropped,
9622
+ chunksDelivered: subscription.chunksDelivered,
9623
+ hint: "the consumer is pulling slower than this camera produces — its poll cadence or drain batch is wrong for the producer, not the boundary"
9624
+ } });
9615
9625
  }
9616
9626
  /**
9617
9627
  * Run the idle-lease sweep on a cadence rather than on every chunk. A quarter
@@ -9669,10 +9679,6 @@ var AudioChunkPlane = class {
9669
9679
  this.subscriptions.clear();
9670
9680
  }
9671
9681
  };
9672
- //#endregion
9673
- //#region src/stream-broker/stream-broker/audio-codec-session.ts
9674
- var POLL_INTERVAL_MS$1 = 40;
9675
- var MAX_PCM_PER_POLL = 16;
9676
9682
  var RTP_HEADER_LENGTH = 12;
9677
9683
  var WARN_THROTTLE_MS$1 = 5e3;
9678
9684
  var SESSION_NOT_FOUND_RE$1 = /decode session\b.*\bnot found\b/;
@@ -9684,19 +9690,23 @@ var AudioCodecSession = class {
9684
9690
  cfg;
9685
9691
  emit;
9686
9692
  logger;
9693
+ realtimeDemand;
9687
9694
  sessionId = null;
9688
9695
  sessionNodeId = null;
9689
9696
  creating = null;
9690
9697
  pollTimer = null;
9698
+ /** The cadence the loop last armed, so a change can be NAMED once (D465). */
9699
+ pollIntervalMs = null;
9691
9700
  closed = false;
9692
9701
  packetsIn = 0;
9693
9702
  packetsDropped = 0;
9694
9703
  warnState = /* @__PURE__ */ new Map();
9695
- constructor(api, cfg, emit, logger) {
9704
+ constructor(api, cfg, emit, logger, realtimeDemand) {
9696
9705
  this.api = api;
9697
9706
  this.cfg = cfg;
9698
9707
  this.emit = emit;
9699
9708
  this.logger = logger;
9709
+ this.realtimeDemand = realtimeDemand;
9700
9710
  }
9701
9711
  processPacket(rtpData) {
9702
9712
  if (this.closed) return;
@@ -9764,7 +9774,7 @@ var AudioCodecSession = class {
9764
9774
  if (this.creating) await this.creating;
9765
9775
  if (this.closed) return;
9766
9776
  if (this.pollTimer) {
9767
- clearInterval(this.pollTimer);
9777
+ clearTimeout(this.pollTimer);
9768
9778
  this.pollTimer = null;
9769
9779
  }
9770
9780
  if (!this.sessionId) return;
@@ -9786,7 +9796,7 @@ var AudioCodecSession = class {
9786
9796
  if (this.closed) return;
9787
9797
  this.closed = true;
9788
9798
  if (this.pollTimer) {
9789
- clearInterval(this.pollTimer);
9799
+ clearTimeout(this.pollTimer);
9790
9800
  this.pollTimer = null;
9791
9801
  }
9792
9802
  if (this.sessionId) {
@@ -9852,11 +9862,43 @@ var AudioCodecSession = class {
9852
9862
  })();
9853
9863
  return this.creating;
9854
9864
  }
9865
+ /**
9866
+ * The cadence this tick should use. Re-read every tick rather than latched at
9867
+ * arm time: a WebRTC session attaching mid-stream must be served at
9868
+ * {@link REALTIME_POLL_INTERVAL_MS}, and it costs at most one idle interval
9869
+ * (400 ms) to notice — against an ICE/DTLS handshake measured in seconds.
9870
+ */
9871
+ currentPollIntervalMs() {
9872
+ if (!this.realtimeDemand) return 40;
9873
+ return this.realtimeDemand() ? 40 : 400;
9874
+ }
9855
9875
  startPollLoop() {
9856
9876
  if (this.pollTimer || this.closed) return;
9857
- this.pollTimer = setInterval(() => {
9858
- this.pollOnce();
9859
- }, POLL_INTERVAL_MS$1);
9877
+ this.scheduleNextPoll();
9878
+ }
9879
+ /**
9880
+ * Self-scheduling tick. `setInterval` cannot change period, and the period is
9881
+ * now a property of who is listening (D465), so the loop re-arms itself.
9882
+ */
9883
+ scheduleNextPoll() {
9884
+ if (this.closed || !this.sessionId) return;
9885
+ const intervalMs = this.currentPollIntervalMs();
9886
+ if (intervalMs !== this.pollIntervalMs) {
9887
+ const previous = this.pollIntervalMs;
9888
+ this.pollIntervalMs = intervalMs;
9889
+ this.logger?.info("audio-codec: poll cadence", { meta: {
9890
+ intervalMs,
9891
+ previousMs: previous,
9892
+ reason: intervalMs === 40 ? "realtime-consumer" : "plane-only",
9893
+ maxPcmPerPoll: 32
9894
+ } });
9895
+ }
9896
+ this.pollTimer = setTimeout(() => {
9897
+ this.pollTimer = null;
9898
+ this.pollOnce().finally(() => {
9899
+ this.scheduleNextPoll();
9900
+ });
9901
+ }, intervalMs);
9860
9902
  if (typeof this.pollTimer.unref === "function") this.pollTimer.unref();
9861
9903
  }
9862
9904
  async pollOnce() {
@@ -9867,7 +9909,7 @@ var AudioCodecSession = class {
9867
9909
  const pcms = await this.api.pullPcm({
9868
9910
  sessionId: sid,
9869
9911
  ...nodeId ? { nodeId } : {},
9870
- maxCount: MAX_PCM_PER_POLL
9912
+ maxCount: 32
9871
9913
  });
9872
9914
  for (const pcm of pcms) this.appendPcm(pcm);
9873
9915
  } catch (err) {
@@ -9911,7 +9953,7 @@ var AudioCodecSession = class {
9911
9953
  this.sessionId = null;
9912
9954
  this.sessionNodeId = null;
9913
9955
  if (this.pollTimer) {
9914
- clearInterval(this.pollTimer);
9956
+ clearTimeout(this.pollTimer);
9915
9957
  this.pollTimer = null;
9916
9958
  }
9917
9959
  this.warnThrottled("audio-codec: session reaped upstream — will re-create on next packet", `staleSessionId=${staleSid}`);
@@ -12619,7 +12661,7 @@ var StreamBroker = class StreamBroker {
12619
12661
  this.logger?.debug("audio-codec: prior session close failed", { meta: { error: require_dist.errMsg(err) } });
12620
12662
  });
12621
12663
  }
12622
- this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, cfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"));
12664
+ this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, cfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"), () => this.encodedCallbacks.size > 0);
12623
12665
  this.logger?.info("push-audio: decode session armed", { meta: {
12624
12666
  codec: cfg.codec,
12625
12667
  sourceSampleRate: cfg.sourceSampleRate,
@@ -13825,7 +13867,7 @@ var StreamBroker = class StreamBroker {
13825
13867
  this.logger?.debug("audio-codec: prior RTSP session close failed", { meta: { error: require_dist.errMsg(err) } });
13826
13868
  });
13827
13869
  }
13828
- this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, capCfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"));
13870
+ this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, capCfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"), () => this.encodedCallbacks.size > 0);
13829
13871
  this.logger?.info("audio-codec: decode session armed", { meta: {
13830
13872
  codec: capCfg.codec,
13831
13873
  sourceSampleRate: capCfg.sourceSampleRate,
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $t as makeProfileBrokerId, B as declareLogChannel, Bt as buildFfmpegArgs$1, Et as resolveEgressDecodeHwAccel, F as batteryCapability, Gt as CAM_PROFILE_ORDER, Ht as isSoftwareDecode, I as cameraStreamsCapability, It as webrtcSessionCapability, J as egressTransportFromRequest, Kt as DeviceFeature, L as createHwAccelCache, Lt as errMsg, Nt as streamBrokerCapability, Qt as isEvent, R as createLogChannelsProvider, Rt as AUDIO_PRESETS, T as RATE_CONTROL_TIGHT, U as deriveBatteryPresence, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Xt as expandAudioChunkToF32le, Yt as createEvent, _n as union, at as loadContributionCapability, cn as boolean, ct as maskUrlCredentials, dn as literal, en as makeSourceBrokerId, fn as number, gn as string, h as EncodeProfileSchema, hn as record, i as BatteryStatusSchema, j as addonWidgetsSourceCapability, k as RingBuffer, ln as discriminatedUnion, on as _enum, ot as logChannelsCapability, pn as object, q as egressTranscodeSharingKey, qt as DeviceType, rn as parseProfileBrokerId, sn as array, tn as nodePin, v as G711_SCALE_CORRECTION_DB, vn as EventCategory, w as RATE_CONTROL_RELAXED } from "../dist-CEFC3PkP.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-DR3fHqUq.mjs";
2
+ import { $t as makeProfileBrokerId, B as declareLogChannel, Bt as buildFfmpegArgs$1, Et as resolveEgressDecodeHwAccel, F as batteryCapability, Gt as CAM_PROFILE_ORDER, Ht as isSoftwareDecode, I as cameraStreamsCapability, It as webrtcSessionCapability, J as egressTransportFromRequest, Kt as DeviceFeature, L as createHwAccelCache, Lt as errMsg, Nt as streamBrokerCapability, Qt as isEvent, R as createLogChannelsProvider, Rt as AUDIO_PRESETS, T as RATE_CONTROL_TIGHT, U as deriveBatteryPresence, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Xt as expandAudioChunkToF32le, Yt as createEvent, _n as union, at as loadContributionCapability, cn as boolean, ct as maskUrlCredentials, dn as literal, en as makeSourceBrokerId, fn as number, gn as string, h as EncodeProfileSchema, hn as record, i as BatteryStatusSchema, j as addonWidgetsSourceCapability, k as RingBuffer, ln as discriminatedUnion, on as _enum, ot as logChannelsCapability, pn as object, q as egressTranscodeSharingKey, qt as DeviceType, rn as parseProfileBrokerId, sn as array, tn as nodePin, v as G711_SCALE_CORRECTION_DB, vn as EventCategory, w as RATE_CONTROL_RELAXED } from "../dist-BNzfbpF6.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-uYIx2aZF.mjs";
4
4
  import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-DWe2kInY.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 { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-CddDwD69.mjs";
7
+ import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-Dh1dn_AT.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -9405,42 +9405,14 @@ function extractQuoted(header, key) {
9405
9405
  const unquotedRegex = new RegExp(`${key}=([^,\\s]+)`, "i");
9406
9406
  return header.match(unquotedRegex)?.[1];
9407
9407
  }
9408
- //#endregion
9409
- //#region src/stream-broker/stream-broker/audio-chunk-plane.ts
9410
- /**
9411
- * `AudioChunkPlane` — the broker's serialisable decoded audio-chunk surface
9412
- * (Phase 5 / D9).
9413
- *
9414
- * This is the poll-based replacement for `StreamBroker.onDecodedAudioChunk`'s
9415
- * live-object callback path. A live callback cannot cross a process boundary;
9416
- * once the `pipeline` group is dissolved (Task 8) the audio consumer
9417
- * (`pipeline-orchestrator`) runs in a different process from the broker, so
9418
- * audio delivery must go over tRPC like the video frame plane.
9419
- *
9420
- * ## Why no shared memory
9421
- *
9422
- * Unlike video frames (~6 MB at 1080p RGB), a decoded audio chunk is tiny — a
9423
- * ~500ms `AudioCodecSession` window is a few KB of Float32 PCM. Tiny payloads
9424
- * serialise over tRPC cheaply, so audio ships its bytes INLINE on the RPC
9425
- * wire; there is no shm ring.
9426
- *
9427
- * ## FIFO / no-drop queue policy
9428
- *
9429
- * Audio wants FIFO arrival order with no latest-wins drop: a missing chunk is
9430
- * an audible gap and breaks the audio analyzer's fixed-length analysis
9431
- * window. Each subscription therefore owns a generously-sized FIFO queue
9432
- * (`AUDIO_QUEUE_CAPACITY`). Chunks arrive roughly every
9433
- * `AudioCodecSession`-window (~500ms), so the queue buffers tens of seconds
9434
- * of audio — a healthy poll loop never fills it. The queue only drops its
9435
- * OLDEST chunk if a truly stalled consumer lets it overflow (a bounded safety
9436
- * valve, not the steady-state behaviour).
9437
- */
9438
9408
  /**
9439
- * Per-subscription FIFO capacity. A chunk is a few KB and arrives ~every
9440
- * 500ms, so 64 buffers ~30s of audio between two `pullAudioChunks` polls
9441
- * without unbounded growth.
9409
+ * Re-state a standing overflow at most this often (D465). The line fires when a
9410
+ * subscription STARTS overwriting, and again every 30 s while it still is — a
9411
+ * per-chunk warn is ~25 writes/s per camera of synchronous winston on the
9412
+ * broker's event loop, which is the cost the `pullPcm` warn coalescing exists
9413
+ * to avoid.
9442
9414
  */
9443
- var AUDIO_QUEUE_CAPACITY = 64;
9415
+ var OVERFLOW_RELOG_MS = 3e4;
9444
9416
  /**
9445
9417
  * How long a subscription may go without a `pull` before the plane releases it
9446
9418
  * (D461).
@@ -9511,10 +9483,13 @@ var AudioChunkPlane = class {
9511
9483
  id: subscriptionId,
9512
9484
  tag: input?.tag ?? "unknown",
9513
9485
  subscribedAt: Date.now(),
9514
- queue: new RingBuffer(AUDIO_QUEUE_CAPACITY),
9486
+ queue: new RingBuffer(64),
9515
9487
  accept: new Set(input?.accept ?? []),
9516
9488
  servedKey: null,
9517
9489
  chunksDelivered: 0,
9490
+ chunksDropped: 0,
9491
+ overflowing: false,
9492
+ lastOverflowLogAt: 0,
9518
9493
  lastPulledAt: Date.now()
9519
9494
  };
9520
9495
  this.subscriptions.set(subscriptionId, subscription);
@@ -9566,6 +9541,7 @@ var AudioChunkPlane = class {
9566
9541
  leaseMs: AUDIO_SUBSCRIPTION_IDLE_MS,
9567
9542
  queued: subscription.queue.size,
9568
9543
  chunksDelivered: subscription.chunksDelivered,
9544
+ chunksDropped: subscription.chunksDropped,
9569
9545
  hint: "the consumer process died without detaching — its subscription was holding this broker in demand"
9570
9546
  } });
9571
9547
  }
@@ -9601,13 +9577,47 @@ var AudioChunkPlane = class {
9601
9577
  for (const subscription of this.subscriptions.values()) {
9602
9578
  if (sourceFormat === "f32le" || subscription.accept.has(sourceFormat)) {
9603
9579
  this.noteServed(subscription, sourceFormat, sourceFormat);
9604
- subscription.queue.push(chunk);
9580
+ this.enqueue(subscription, chunk);
9605
9581
  continue;
9606
9582
  }
9607
9583
  expanded ??= expandChunk(chunk, sourceFormat);
9608
9584
  this.noteServed(subscription, sourceFormat, "f32le");
9609
- subscription.queue.push(expanded);
9585
+ this.enqueue(subscription, expanded);
9586
+ }
9587
+ }
9588
+ /**
9589
+ * Push one chunk into a subscription's FIFO and NAME the loss when the FIFO
9590
+ * had no room (D465).
9591
+ *
9592
+ * `RingBuffer.push` overwrites its oldest entry. Every docblock on this path
9593
+ * called that "a bounded safety valve, not the steady-state behaviour" and
9594
+ * nothing ever said when the valve opened, so a consumer polling too slowly
9595
+ * for its producer lost audio permanently and in silence — the exact shape
9596
+ * CLAUDE.md forbids. The realistic cause is a cadence chosen for the
9597
+ * BOUNDARY rather than for the producer's chunk rate: 500 ms of a ~42 ms AAC
9598
+ * chunk is ~12 chunks against a batch that used to drain 8.
9599
+ */
9600
+ enqueue(subscription, chunk) {
9601
+ const wasFull = subscription.queue.size >= 64;
9602
+ subscription.queue.push(chunk);
9603
+ if (!wasFull) {
9604
+ subscription.overflowing = false;
9605
+ return;
9610
9606
  }
9607
+ subscription.chunksDropped += 1;
9608
+ const now = Date.now();
9609
+ const isNewEpisode = !subscription.overflowing;
9610
+ subscription.overflowing = true;
9611
+ if (!isNewEpisode && now - subscription.lastOverflowLogAt < OVERFLOW_RELOG_MS) return;
9612
+ subscription.lastOverflowLogAt = now;
9613
+ this.logger?.warn("audio-chunk subscription OVERFLOW — oldest audio overwritten", { meta: {
9614
+ subscriptionId: subscription.id,
9615
+ tag: subscription.tag,
9616
+ capacity: 64,
9617
+ chunksDropped: subscription.chunksDropped,
9618
+ chunksDelivered: subscription.chunksDelivered,
9619
+ hint: "the consumer is pulling slower than this camera produces — its poll cadence or drain batch is wrong for the producer, not the boundary"
9620
+ } });
9611
9621
  }
9612
9622
  /**
9613
9623
  * Run the idle-lease sweep on a cadence rather than on every chunk. A quarter
@@ -9665,10 +9675,6 @@ var AudioChunkPlane = class {
9665
9675
  this.subscriptions.clear();
9666
9676
  }
9667
9677
  };
9668
- //#endregion
9669
- //#region src/stream-broker/stream-broker/audio-codec-session.ts
9670
- var POLL_INTERVAL_MS$1 = 40;
9671
- var MAX_PCM_PER_POLL = 16;
9672
9678
  var RTP_HEADER_LENGTH = 12;
9673
9679
  var WARN_THROTTLE_MS$1 = 5e3;
9674
9680
  var SESSION_NOT_FOUND_RE$1 = /decode session\b.*\bnot found\b/;
@@ -9680,19 +9686,23 @@ var AudioCodecSession = class {
9680
9686
  cfg;
9681
9687
  emit;
9682
9688
  logger;
9689
+ realtimeDemand;
9683
9690
  sessionId = null;
9684
9691
  sessionNodeId = null;
9685
9692
  creating = null;
9686
9693
  pollTimer = null;
9694
+ /** The cadence the loop last armed, so a change can be NAMED once (D465). */
9695
+ pollIntervalMs = null;
9687
9696
  closed = false;
9688
9697
  packetsIn = 0;
9689
9698
  packetsDropped = 0;
9690
9699
  warnState = /* @__PURE__ */ new Map();
9691
- constructor(api, cfg, emit, logger) {
9700
+ constructor(api, cfg, emit, logger, realtimeDemand) {
9692
9701
  this.api = api;
9693
9702
  this.cfg = cfg;
9694
9703
  this.emit = emit;
9695
9704
  this.logger = logger;
9705
+ this.realtimeDemand = realtimeDemand;
9696
9706
  }
9697
9707
  processPacket(rtpData) {
9698
9708
  if (this.closed) return;
@@ -9760,7 +9770,7 @@ var AudioCodecSession = class {
9760
9770
  if (this.creating) await this.creating;
9761
9771
  if (this.closed) return;
9762
9772
  if (this.pollTimer) {
9763
- clearInterval(this.pollTimer);
9773
+ clearTimeout(this.pollTimer);
9764
9774
  this.pollTimer = null;
9765
9775
  }
9766
9776
  if (!this.sessionId) return;
@@ -9782,7 +9792,7 @@ var AudioCodecSession = class {
9782
9792
  if (this.closed) return;
9783
9793
  this.closed = true;
9784
9794
  if (this.pollTimer) {
9785
- clearInterval(this.pollTimer);
9795
+ clearTimeout(this.pollTimer);
9786
9796
  this.pollTimer = null;
9787
9797
  }
9788
9798
  if (this.sessionId) {
@@ -9848,11 +9858,43 @@ var AudioCodecSession = class {
9848
9858
  })();
9849
9859
  return this.creating;
9850
9860
  }
9861
+ /**
9862
+ * The cadence this tick should use. Re-read every tick rather than latched at
9863
+ * arm time: a WebRTC session attaching mid-stream must be served at
9864
+ * {@link REALTIME_POLL_INTERVAL_MS}, and it costs at most one idle interval
9865
+ * (400 ms) to notice — against an ICE/DTLS handshake measured in seconds.
9866
+ */
9867
+ currentPollIntervalMs() {
9868
+ if (!this.realtimeDemand) return 40;
9869
+ return this.realtimeDemand() ? 40 : 400;
9870
+ }
9851
9871
  startPollLoop() {
9852
9872
  if (this.pollTimer || this.closed) return;
9853
- this.pollTimer = setInterval(() => {
9854
- this.pollOnce();
9855
- }, POLL_INTERVAL_MS$1);
9873
+ this.scheduleNextPoll();
9874
+ }
9875
+ /**
9876
+ * Self-scheduling tick. `setInterval` cannot change period, and the period is
9877
+ * now a property of who is listening (D465), so the loop re-arms itself.
9878
+ */
9879
+ scheduleNextPoll() {
9880
+ if (this.closed || !this.sessionId) return;
9881
+ const intervalMs = this.currentPollIntervalMs();
9882
+ if (intervalMs !== this.pollIntervalMs) {
9883
+ const previous = this.pollIntervalMs;
9884
+ this.pollIntervalMs = intervalMs;
9885
+ this.logger?.info("audio-codec: poll cadence", { meta: {
9886
+ intervalMs,
9887
+ previousMs: previous,
9888
+ reason: intervalMs === 40 ? "realtime-consumer" : "plane-only",
9889
+ maxPcmPerPoll: 32
9890
+ } });
9891
+ }
9892
+ this.pollTimer = setTimeout(() => {
9893
+ this.pollTimer = null;
9894
+ this.pollOnce().finally(() => {
9895
+ this.scheduleNextPoll();
9896
+ });
9897
+ }, intervalMs);
9856
9898
  if (typeof this.pollTimer.unref === "function") this.pollTimer.unref();
9857
9899
  }
9858
9900
  async pollOnce() {
@@ -9863,7 +9905,7 @@ var AudioCodecSession = class {
9863
9905
  const pcms = await this.api.pullPcm({
9864
9906
  sessionId: sid,
9865
9907
  ...nodeId ? { nodeId } : {},
9866
- maxCount: MAX_PCM_PER_POLL
9908
+ maxCount: 32
9867
9909
  });
9868
9910
  for (const pcm of pcms) this.appendPcm(pcm);
9869
9911
  } catch (err) {
@@ -9907,7 +9949,7 @@ var AudioCodecSession = class {
9907
9949
  this.sessionId = null;
9908
9950
  this.sessionNodeId = null;
9909
9951
  if (this.pollTimer) {
9910
- clearInterval(this.pollTimer);
9952
+ clearTimeout(this.pollTimer);
9911
9953
  this.pollTimer = null;
9912
9954
  }
9913
9955
  this.warnThrottled("audio-codec: session reaped upstream — will re-create on next packet", `staleSessionId=${staleSid}`);
@@ -12615,7 +12657,7 @@ var StreamBroker = class StreamBroker {
12615
12657
  this.logger?.debug("audio-codec: prior session close failed", { meta: { error: errMsg(err) } });
12616
12658
  });
12617
12659
  }
12618
- this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, cfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"));
12660
+ this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, cfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"), () => this.encodedCallbacks.size > 0);
12619
12661
  this.logger?.info("push-audio: decode session armed", { meta: {
12620
12662
  codec: cfg.codec,
12621
12663
  sourceSampleRate: cfg.sourceSampleRate,
@@ -13821,7 +13863,7 @@ var StreamBroker = class StreamBroker {
13821
13863
  this.logger?.debug("audio-codec: prior RTSP session close failed", { meta: { error: errMsg(err) } });
13822
13864
  });
13823
13865
  }
13824
- this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, capCfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"));
13866
+ this.audioCodecSession = new AudioCodecSession(this.audioCodecApi, capCfg, this.buildAudioCodecEmitCallback(), this.logger?.child("audio-codec"), () => this.encodedCallbacks.size > 0);
13825
13867
  this.logger?.info("audio-codec: decode session armed", { meta: {
13826
13868
  codec: capCfg.codec,
13827
13869
  sourceSampleRate: capCfg.sourceSampleRate,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BZG3prqU.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.221",
3
+ "version": "1.2.223",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",