@camstack/addon-pipeline 1.2.222 → 1.2.224
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.
- package/dist/audio-analyzer/index.js +53 -14
- package/dist/audio-analyzer/index.mjs +53 -14
- package/dist/pipeline-runner/index.js +61 -45
- package/dist/pipeline-runner/index.mjs +61 -45
- package/dist/recorder/index.js +88 -25
- package/dist/recorder/index.mjs +88 -25
- package/dist/stream-broker/index.js +93 -51
- package/dist/stream-broker/index.mjs +93 -51
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
9440
|
-
*
|
|
9441
|
-
*
|
|
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
|
|
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(
|
|
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
|
-
|
|
9580
|
+
this.enqueue(subscription, chunk);
|
|
9605
9581
|
continue;
|
|
9606
9582
|
}
|
|
9607
9583
|
expanded ??= expandChunk(chunk, sourceFormat);
|
|
9608
9584
|
this.noteServed(subscription, sourceFormat, "f32le");
|
|
9609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
9854
|
-
|
|
9855
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.224",
|
|
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",
|