@camstack/addon-pipeline 1.2.140 → 1.2.142
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/recorder/index.js +280 -60
- package/dist/recorder/index.mjs +280 -60
- package/dist/stream-broker/index.js +222 -83
- package/dist/stream-broker/index.mjs +222 -83
- package/package.json +1 -1
|
@@ -171,6 +171,25 @@ var CH_RTP = declareLogChannel({
|
|
|
171
171
|
defaultLevel: "info",
|
|
172
172
|
perDevice: false
|
|
173
173
|
});
|
|
174
|
+
/**
|
|
175
|
+
* Every channel this addon declares, as a VALUE.
|
|
176
|
+
*
|
|
177
|
+
* `addon.ts` used to reach the declarations through a bare
|
|
178
|
+
* `import './log-channels.js'` — an import with no binding, kept alive only by
|
|
179
|
+
* a comment saying "never drop this as unused". On 2026-08-29 the shipped
|
|
180
|
+
* bundle contained this module TWICE: inlined into `stream-broker/index.mjs`
|
|
181
|
+
* and again in a shared chunk that same entry statically imports. Loading the
|
|
182
|
+
* addon ran `declareLogChannel` twice, the duplicate was refused — correctly,
|
|
183
|
+
* that guard is doing its job — and the whole stream-broker failed to load.
|
|
184
|
+
* The hub had no broker and no live streams until it was rolled back.
|
|
185
|
+
*
|
|
186
|
+
* The duplication has NOT been reproduced since, from a clean dist or a dirty
|
|
187
|
+
* one, so this is not offered as a proven fix: it removes the freedom the
|
|
188
|
+
* bundler took. A value binding must resolve to one module instance; a
|
|
189
|
+
* side-effect import need not. `scripts/check-log-channel-single-declaration.ts`
|
|
190
|
+
* is the part that actually holds, because it reads the artefact.
|
|
191
|
+
*/
|
|
192
|
+
var STREAM_BROKER_LOG_CHANNELS = [CH_WEBRTC, CH_RTP];
|
|
174
193
|
//#endregion
|
|
175
194
|
//#region src/stream-broker/stream-broker/probed-summary.ts
|
|
176
195
|
/**
|
|
@@ -1818,15 +1837,32 @@ var DEFAULT_RECORDING_PRE_ROLL_MS = 1e4;
|
|
|
1818
1837
|
*/
|
|
1819
1838
|
var PRE_ROLL_UNKNOWN_BITRATE_BYTES = 8 * 1024 * 1024;
|
|
1820
1839
|
/**
|
|
1821
|
-
*
|
|
1840
|
+
* Hard ceiling on the WHOLE source-RTP ring (history + the current GOP).
|
|
1841
|
+
*
|
|
1842
|
+
* Lives here, next to the pre-roll budget, because the two numbers are one
|
|
1843
|
+
* number: a history budget above the ring cap is not a wider window, it is a
|
|
1844
|
+
* ring that overflows and calls `resetRtpPreBuffer`, which drops the join
|
|
1845
|
+
* bootstrap of EVERY consumer — WebRTC viewers included — for as long as the
|
|
1846
|
+
* condition holds. Keeping them in separate files is how they came to disagree.
|
|
1847
|
+
* Read by `StreamBroker.captureRtpForPreBuffer`.
|
|
1848
|
+
*/
|
|
1849
|
+
var RTP_RING_MAX_BYTES = 24 * 1024 * 1024;
|
|
1850
|
+
/**
|
|
1851
|
+
* Absolute per-stream ceiling on retained pre-roll HISTORY.
|
|
1822
1852
|
*
|
|
1823
1853
|
* The bitrate-derived budget is what the window COSTS; this is the most any one
|
|
1824
|
-
* stream may spend regardless.
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1854
|
+
* stream may spend regardless. Past it the operator has configured a window this
|
|
1855
|
+
* node cannot afford on that camera, and the broker says so rather than
|
|
1856
|
+
* delivering silently less (D192).
|
|
1857
|
+
*
|
|
1858
|
+
* **DERIVED, not chosen.** It was a free-standing `32 * 1024 * 1024` — 8 MiB
|
|
1859
|
+
* ABOVE the ring it has to fit inside. A ≳13 Mbps stream at a 10 s window was
|
|
1860
|
+
* therefore authorised to overflow its own ring and cycle `resetRtpPreBuffer`:
|
|
1861
|
+
* the pre-roll knob, turned up, would have taken every viewer's join bootstrap
|
|
1862
|
+
* with it. Deriving it from {@link RTP_RING_MAX_BYTES} minus the current-GOP
|
|
1863
|
+
* reserve makes the inversion unrepresentable rather than merely fixed.
|
|
1828
1864
|
*/
|
|
1829
|
-
var PRE_ROLL_ABSOLUTE_MAX_BYTES =
|
|
1865
|
+
var PRE_ROLL_ABSOLUTE_MAX_BYTES = RTP_RING_MAX_BYTES - 8 * 1024 * 1024;
|
|
1830
1866
|
/**
|
|
1831
1867
|
* Headroom over the OBSERVED mean bitrate.
|
|
1832
1868
|
*
|
|
@@ -1856,7 +1892,7 @@ function preRollBudgetBytes(windowMs, observedKbps) {
|
|
|
1856
1892
|
clamped: false
|
|
1857
1893
|
};
|
|
1858
1894
|
const raw = Math.round(windowMs * observedKbps * PRE_ROLL_BITRATE_HEADROOM / 8);
|
|
1859
|
-
if (raw >
|
|
1895
|
+
if (raw > 16777216) return {
|
|
1860
1896
|
bytes: PRE_ROLL_ABSOLUTE_MAX_BYTES,
|
|
1861
1897
|
clamped: true
|
|
1862
1898
|
};
|
|
@@ -2865,6 +2901,12 @@ var RtspRestreamer = class {
|
|
|
2865
2901
|
*/
|
|
2866
2902
|
servePendingSessionsRtp() {
|
|
2867
2903
|
if (this.pendingKeyframe.size === 0) return;
|
|
2904
|
+
const ready = [];
|
|
2905
|
+
for (const sessionId of this.pendingKeyframe) {
|
|
2906
|
+
if (this.activeReplays.has(sessionId)) continue;
|
|
2907
|
+
if (this.sessions.get(sessionId)?.isPlaying()) ready.push(sessionId);
|
|
2908
|
+
}
|
|
2909
|
+
if (ready.length === 0) return;
|
|
2868
2910
|
const ringPackets = this.rtpPreBufferSupplier?.() ?? [];
|
|
2869
2911
|
const fromRing = ringPackets.length > 0;
|
|
2870
2912
|
const primePackets = fromRing ? ringPackets.map((data) => ({
|
|
@@ -2874,12 +2916,6 @@ var RtspRestreamer = class {
|
|
|
2874
2916
|
if (primePackets.length === 0) return;
|
|
2875
2917
|
const firstTs = primePackets[0].data.readUInt32BE(4);
|
|
2876
2918
|
const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
|
|
2877
|
-
const ready = [];
|
|
2878
|
-
for (const sessionId of this.pendingKeyframe) {
|
|
2879
|
-
if (this.activeReplays.has(sessionId)) continue;
|
|
2880
|
-
if (this.sessions.get(sessionId)?.isPlaying()) ready.push(sessionId);
|
|
2881
|
-
}
|
|
2882
|
-
if (ready.length === 0) return;
|
|
2883
2919
|
const preRollPlans = /* @__PURE__ */ new Map();
|
|
2884
2920
|
const bound = this.currentPrimeBound();
|
|
2885
2921
|
/**
|
|
@@ -3506,15 +3542,7 @@ var RtspSession = class {
|
|
|
3506
3542
|
return "closed-backlog";
|
|
3507
3543
|
}
|
|
3508
3544
|
}
|
|
3509
|
-
|
|
3510
|
-
const frame = Buffer.allocUnsafe(4 + packet.data.length);
|
|
3511
|
-
frame[0] = 36;
|
|
3512
|
-
frame[1] = channel;
|
|
3513
|
-
frame.writeUInt16BE(packet.data.length, 2);
|
|
3514
|
-
packet.data.copy(frame, 4);
|
|
3515
|
-
this.socket.write(frame);
|
|
3516
|
-
this.lastRtpAt = Date.now();
|
|
3517
|
-
this.bytesSent += frame.length;
|
|
3545
|
+
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
|
|
3518
3546
|
return "written";
|
|
3519
3547
|
}
|
|
3520
3548
|
/** Send an audio RTP packet via TCP interleaved framing on the audio track channel. */
|
|
@@ -3525,17 +3553,60 @@ var RtspSession = class {
|
|
|
3525
3553
|
this.close();
|
|
3526
3554
|
return "closed-backlog";
|
|
3527
3555
|
}
|
|
3528
|
-
|
|
3529
|
-
const frame = Buffer.allocUnsafe(4 + packet.data.length);
|
|
3530
|
-
frame[0] = 36;
|
|
3531
|
-
frame[1] = channel;
|
|
3532
|
-
frame.writeUInt16BE(packet.data.length, 2);
|
|
3533
|
-
packet.data.copy(frame, 4);
|
|
3534
|
-
this.socket.write(frame);
|
|
3535
|
-
this.lastRtpAt = Date.now();
|
|
3536
|
-
this.bytesSent += frame.length;
|
|
3556
|
+
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
|
|
3537
3557
|
return "written";
|
|
3538
3558
|
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Write ONE interleaved frame as a 4-byte header plus the caller's payload,
|
|
3561
|
+
* **shared by reference** — the payload is never copied.
|
|
3562
|
+
*
|
|
3563
|
+
* ## Why this is the whole point
|
|
3564
|
+
*
|
|
3565
|
+
* This used to be `Buffer.allocUnsafe(4 + payload.length)` + `payload.copy()`:
|
|
3566
|
+
* a FULL copy of every packet, once per connected session, to prepend four
|
|
3567
|
+
* constant bytes. At ~3 000 pps ingest and 2–3 sessions per broker (detection
|
|
3568
|
+
* dial, recording dial, egress legs) that is ~6 000–9 000 full-packet
|
|
3569
|
+
* allocations per second, and `stream-broker` was measured at 95.5 % of a core
|
|
3570
|
+
* with **68 % of it in GC**. Scrypted's prebuffer — same fundamental ring
|
|
3571
|
+
* design — writes the header and the shared payload as two writes and does
|
|
3572
|
+
* exactly one allocation per packet, total, for every consumer. This closes
|
|
3573
|
+
* that gap. N connected clients now cause ONE payload allocation (the ingest
|
|
3574
|
+
* copy), not N.
|
|
3575
|
+
*
|
|
3576
|
+
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
3577
|
+
*
|
|
3578
|
+
* The payload buffer handed here is ALREADY retained, by reference, in the
|
|
3579
|
+
* broker's `rtpRing` for the life of a GOP (`captureRtpForPreBuffer` pushes
|
|
3580
|
+
* the same object). A pending `socket.write` cannot outlive that, so the
|
|
3581
|
+
* lifetime requirement this adds is strictly weaker than one the broker
|
|
3582
|
+
* already meets. Its provenance is a private allocation per packet —
|
|
3583
|
+
* `Buffer.from(readBuffer.subarray(...))` in `rtsp-client.ts`'s `onData`, for
|
|
3584
|
+
* video and audio alike — never a view of the reader's socket buffer, which
|
|
3585
|
+
* advances on the next read. Nothing in the broker writes into a captured
|
|
3586
|
+
* packet: every mutation in this path targets a freshly allocated buffer.
|
|
3587
|
+
*
|
|
3588
|
+
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
3589
|
+
* completes, not retention.** So the header, which IS built per call, must
|
|
3590
|
+
* stay a fresh allocation — a reused 4-byte scratch buffer would be rewritten
|
|
3591
|
+
* under an in-flight write and corrupt the frame length of a packet already
|
|
3592
|
+
* queued. Four bytes from the Buffer pool is a pointer bump; the copy this
|
|
3593
|
+
* replaces was ~1.4 KB.
|
|
3594
|
+
*
|
|
3595
|
+
* `cork`/`uncork` keeps the pair a single `writev` syscall, so removing the
|
|
3596
|
+
* copy does not buy an extra write syscall per packet.
|
|
3597
|
+
*/
|
|
3598
|
+
writeInterleaved(channel, payload) {
|
|
3599
|
+
const header = Buffer.allocUnsafe(4);
|
|
3600
|
+
header[0] = 36;
|
|
3601
|
+
header[1] = channel;
|
|
3602
|
+
header.writeUInt16BE(payload.length, 2);
|
|
3603
|
+
this.socket.cork();
|
|
3604
|
+
this.socket.write(header);
|
|
3605
|
+
this.socket.write(payload);
|
|
3606
|
+
this.socket.uncork();
|
|
3607
|
+
this.lastRtpAt = Date.now();
|
|
3608
|
+
this.bytesSent += 4 + payload.length;
|
|
3609
|
+
}
|
|
3539
3610
|
close() {
|
|
3540
3611
|
if (this.closed) return;
|
|
3541
3612
|
this.closed = true;
|
|
@@ -8547,8 +8618,6 @@ var Rfc4571Reader = class {
|
|
|
8547
8618
|
}
|
|
8548
8619
|
}
|
|
8549
8620
|
};
|
|
8550
|
-
//#endregion
|
|
8551
|
-
//#region src/stream-broker/rtsp/rtp-depacketizer.ts
|
|
8552
8621
|
var H264_NAL_SPS = 7;
|
|
8553
8622
|
var H264_NAL_PPS = 8;
|
|
8554
8623
|
var H264_NAL_IDR = 5;
|
|
@@ -8560,6 +8629,29 @@ var H265_NAL_IRAP_MIN = 16;
|
|
|
8560
8629
|
var H265_NAL_IRAP_MAX = 21;
|
|
8561
8630
|
var H265_NAL_AP = 48;
|
|
8562
8631
|
/**
|
|
8632
|
+
* Allocate a NAL buffer of `byteLength` payload bytes with the 4-byte Annex-B
|
|
8633
|
+
* start code already written into reserved leading headroom.
|
|
8634
|
+
*
|
|
8635
|
+
* The caller fills from {@link ANNEXB_START_CODE_LENGTH} onwards. This is the
|
|
8636
|
+
* single place a start code is produced on the ingest path — the broker used to
|
|
8637
|
+
* build one with `Buffer.from([0, 0, 0, 1])` per NAL and then concat it onto a
|
|
8638
|
+
* NAL this function had already allocated, which copied every NAL twice.
|
|
8639
|
+
*/
|
|
8640
|
+
function allocAnnexBNal(byteLength) {
|
|
8641
|
+
const buf = Buffer.allocUnsafe(4 + byteLength);
|
|
8642
|
+
buf[0] = 0;
|
|
8643
|
+
buf[1] = 0;
|
|
8644
|
+
buf[2] = 0;
|
|
8645
|
+
buf[3] = 1;
|
|
8646
|
+
return buf;
|
|
8647
|
+
}
|
|
8648
|
+
/** Total byte length of the reassembly fragments, without allocating a closure. */
|
|
8649
|
+
function totalFragmentBytes(fragments) {
|
|
8650
|
+
let total = 0;
|
|
8651
|
+
for (const fragment of fragments) total += fragment.length;
|
|
8652
|
+
return total;
|
|
8653
|
+
}
|
|
8654
|
+
/**
|
|
8563
8655
|
* RTP → Annex-B NAL depacketizer.
|
|
8564
8656
|
*
|
|
8565
8657
|
* Takes raw RTP packets (as received from TCP interleaved RTSP) and
|
|
@@ -8611,7 +8703,7 @@ var RtpDepacketizer = class {
|
|
|
8611
8703
|
}
|
|
8612
8704
|
processH264(payload, timestamp, marker) {
|
|
8613
8705
|
const nalType = payload[0] & 31;
|
|
8614
|
-
if (nalType >= 1 && nalType <= 23) this.emitNal(payload, timestamp, marker);
|
|
8706
|
+
if (nalType >= 1 && nalType <= 23) this.emitNal(this.copyIntoAnnexB(payload), timestamp, marker);
|
|
8615
8707
|
else if (nalType === H264_NAL_STAP_A) this.processH264StapA(payload, timestamp, marker);
|
|
8616
8708
|
else if (nalType === 28) this.processH264FuA(payload, timestamp, marker);
|
|
8617
8709
|
}
|
|
@@ -8622,7 +8714,8 @@ var RtpDepacketizer = class {
|
|
|
8622
8714
|
const nalSize = payload.readUInt16BE(offset);
|
|
8623
8715
|
offset += 2;
|
|
8624
8716
|
if (offset + nalSize > payload.length) break;
|
|
8625
|
-
const nal =
|
|
8717
|
+
const nal = allocAnnexBNal(nalSize);
|
|
8718
|
+
payload.copy(nal, 4, offset, offset + nalSize);
|
|
8626
8719
|
offset += nalSize;
|
|
8627
8720
|
this.emitNal(nal, timestamp, marker && offset >= payload.length);
|
|
8628
8721
|
}
|
|
@@ -8642,10 +8735,9 @@ var RtpDepacketizer = class {
|
|
|
8642
8735
|
} else if (this.fuBuffer.length > 0 && this.fuTimestamp === timestamp) {
|
|
8643
8736
|
this.fuBuffer.push(payload.subarray(2));
|
|
8644
8737
|
if (isEnd) {
|
|
8645
|
-
const
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
let offset = 1;
|
|
8738
|
+
const nal = allocAnnexBNal(1 + totalFragmentBytes(this.fuBuffer));
|
|
8739
|
+
nal[4] = this.fuFirstByte;
|
|
8740
|
+
let offset = 5;
|
|
8649
8741
|
for (const fragment of this.fuBuffer) {
|
|
8650
8742
|
fragment.copy(nal, offset);
|
|
8651
8743
|
offset += fragment.length;
|
|
@@ -8658,7 +8750,7 @@ var RtpDepacketizer = class {
|
|
|
8658
8750
|
processH265(payload, timestamp, marker) {
|
|
8659
8751
|
if (payload.length < 2) return;
|
|
8660
8752
|
const nalType = payload[0] >> 1 & 63;
|
|
8661
|
-
if (nalType < 48) this.emitNal(payload, timestamp, marker);
|
|
8753
|
+
if (nalType < 48) this.emitNal(this.copyIntoAnnexB(payload), timestamp, marker);
|
|
8662
8754
|
else if (nalType === H265_NAL_AP) this.processH265Ap(payload, timestamp, marker);
|
|
8663
8755
|
else if (nalType === 49) this.processH265Fu(payload, timestamp, marker);
|
|
8664
8756
|
}
|
|
@@ -8669,7 +8761,8 @@ var RtpDepacketizer = class {
|
|
|
8669
8761
|
const nalSize = payload.readUInt16BE(offset);
|
|
8670
8762
|
offset += 2;
|
|
8671
8763
|
if (offset + nalSize > payload.length) break;
|
|
8672
|
-
const nal =
|
|
8764
|
+
const nal = allocAnnexBNal(nalSize);
|
|
8765
|
+
payload.copy(nal, 4, offset, offset + nalSize);
|
|
8673
8766
|
offset += nalSize;
|
|
8674
8767
|
this.emitNal(nal, timestamp, marker && offset >= payload.length);
|
|
8675
8768
|
}
|
|
@@ -8691,10 +8784,9 @@ var RtpDepacketizer = class {
|
|
|
8691
8784
|
} else if (this.fuBuffer.length > 0 && this.fuTimestamp === timestamp) {
|
|
8692
8785
|
this.fuBuffer.push(payload.subarray(3));
|
|
8693
8786
|
if (isEnd && this.fuH265Header) {
|
|
8694
|
-
const
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
let offset = 2;
|
|
8787
|
+
const nal = allocAnnexBNal(2 + totalFragmentBytes(this.fuBuffer));
|
|
8788
|
+
this.fuH265Header.copy(nal, 4);
|
|
8789
|
+
let offset = 6;
|
|
8698
8790
|
for (const fragment of this.fuBuffer) {
|
|
8699
8791
|
fragment.copy(nal, offset);
|
|
8700
8792
|
offset += fragment.length;
|
|
@@ -8708,11 +8800,24 @@ var RtpDepacketizer = class {
|
|
|
8708
8800
|
this.fuH265Header = null;
|
|
8709
8801
|
}
|
|
8710
8802
|
}
|
|
8711
|
-
|
|
8803
|
+
/** Copy a payload view into a fresh Annex-B buffer (start code + payload). */
|
|
8804
|
+
copyIntoAnnexB(payload) {
|
|
8805
|
+
const buf = allocAnnexBNal(payload.length);
|
|
8806
|
+
payload.copy(buf, 4);
|
|
8807
|
+
return buf;
|
|
8808
|
+
}
|
|
8809
|
+
/**
|
|
8810
|
+
* @param annexB the NAL WITH its 4-byte start code already in front — every
|
|
8811
|
+
* caller builds it that way via {@link allocAnnexBNal}, so `nal` below is a
|
|
8812
|
+
* free view rather than a second allocation.
|
|
8813
|
+
*/
|
|
8814
|
+
emitNal(annexB, timestamp, marker) {
|
|
8815
|
+
const nal = annexB.subarray(4);
|
|
8712
8816
|
const keyframe = this.codec === "h264" ? this.isH264Keyframe(nal) : this.isH265Keyframe(nal);
|
|
8713
8817
|
const parameterSet = this.codec === "h264" ? this.isH264ParameterSet(nal) : this.isH265ParameterSet(nal);
|
|
8714
8818
|
this.onNal({
|
|
8715
8819
|
nal,
|
|
8820
|
+
annexB,
|
|
8716
8821
|
keyframe,
|
|
8717
8822
|
parameterSet,
|
|
8718
8823
|
timestamp,
|
|
@@ -11051,6 +11156,8 @@ var StreamBroker = class StreamBroker {
|
|
|
11051
11156
|
* consumer reads from. `null` until a consumer subscribes.
|
|
11052
11157
|
*/
|
|
11053
11158
|
frameHandlePlane = null;
|
|
11159
|
+
/** One-shot latch for the "seed ring not filled" line — see `pushEncodedPacket`. */
|
|
11160
|
+
seedRingSkipLogged = false;
|
|
11054
11161
|
decoderApi;
|
|
11055
11162
|
audioCodecApi;
|
|
11056
11163
|
logger;
|
|
@@ -11254,7 +11361,20 @@ var StreamBroker = class StreamBroker {
|
|
|
11254
11361
|
* packet; push / RTMP (provider-driven) sources sit out because they have
|
|
11255
11362
|
* no native reader to tear down.
|
|
11256
11363
|
*/
|
|
11257
|
-
|
|
11364
|
+
rtpActivitySweep;
|
|
11365
|
+
/**
|
|
11366
|
+
* Wall-clock ms of the most recent source-RTP packet, or `0` while the
|
|
11367
|
+
* watchdog is disarmed.
|
|
11368
|
+
*
|
|
11369
|
+
* This is the whole of the per-packet work. The watchdog used to re-arm a
|
|
11370
|
+
* `setTimeout` on EVERY packet (`clearTimeout` + `setTimeout`), and the
|
|
11371
|
+
* comment here called that "negligible". It was not: at 30 cameras and
|
|
11372
|
+
* 80-100 packets/s each, that is ~3 000 `Timeout` objects allocated,
|
|
11373
|
+
* inserted into and removed from libuv's timer heap every second, in the
|
|
11374
|
+
* process that already spends 68% of a full core in GC. Writing a number
|
|
11375
|
+
* allocates nothing; one sweep timer per broker replaces ~100 arms/s.
|
|
11376
|
+
*/
|
|
11377
|
+
lastRtpActivityAt = 0;
|
|
11258
11378
|
/** Number of times the activity watchdog fired in this broker's lifetime
|
|
11259
11379
|
* (incremented on expiry, NOT on arm/cancel). Surfaced via the test
|
|
11260
11380
|
* seam so unit tests can assert recovery behavior without inspecting
|
|
@@ -11266,6 +11386,16 @@ var StreamBroker = class StreamBroker {
|
|
|
11266
11386
|
* real stall is detected before Chrome's ~15s ICE-disconnect timer
|
|
11267
11387
|
* has any chance to fire. */
|
|
11268
11388
|
static RTP_ACTIVITY_TIMEOUT_MS = 8e3;
|
|
11389
|
+
/**
|
|
11390
|
+
* How often the single sweep timer checks {@link lastRtpActivityAt}.
|
|
11391
|
+
*
|
|
11392
|
+
* The stall is still declared at exactly `RTP_ACTIVITY_TIMEOUT_MS` of
|
|
11393
|
+
* silence; this only bounds how late that verdict can be observed (one
|
|
11394
|
+
* sweep, so ≤ 9s worst case rather than exactly 8s). That slack is far
|
|
11395
|
+
* inside Chrome's ~15s ICE-disconnect timer, which is the deadline the
|
|
11396
|
+
* timeout was chosen against.
|
|
11397
|
+
*/
|
|
11398
|
+
static RTP_ACTIVITY_SWEEP_MS = 1e3;
|
|
11269
11399
|
manualStop = false;
|
|
11270
11400
|
stopping = false;
|
|
11271
11401
|
pipeServer;
|
|
@@ -11426,10 +11556,6 @@ var StreamBroker = class StreamBroker {
|
|
|
11426
11556
|
joinBurstBound = DEFAULT_PRIME_BURST_BOUND;
|
|
11427
11557
|
/** Per-message throttle state for {@link reportThrottled}. */
|
|
11428
11558
|
throttledReports = /* @__PURE__ */ new Map();
|
|
11429
|
-
/** Memory ceiling for the RTP ring. A GOP that exceeds this (pathological
|
|
11430
|
-
* long-GOP high-bitrate stream) drops the bootstrap rather than balloon
|
|
11431
|
-
* memory — late joiners fall back to waiting for the next keyframe. */
|
|
11432
|
-
static RTP_RING_MAX_BYTES = 24 * 1024 * 1024;
|
|
11433
11559
|
/** Last budget printed by {@link reportPreRollClamp}, so the line fires on a
|
|
11434
11560
|
* TRANSITION rather than on every keyframe. */
|
|
11435
11561
|
preRollClampLogged = false;
|
|
@@ -12197,9 +12323,18 @@ var StreamBroker = class StreamBroker {
|
|
|
12197
12323
|
}
|
|
12198
12324
|
}
|
|
12199
12325
|
if (packet.type === "video" && this.firstKeyframeReceived) {
|
|
12200
|
-
const
|
|
12201
|
-
|
|
12202
|
-
|
|
12326
|
+
const plane = this.frameHandlePlane;
|
|
12327
|
+
if (plane) {
|
|
12328
|
+
const decoderPacket = this.decoderPacketWithParamSets(packet);
|
|
12329
|
+
this.decoderSeedBuffer.push(decoderPacket);
|
|
12330
|
+
plane.pushPacket(decoderPacket);
|
|
12331
|
+
} else if (!this.seedRingSkipLogged) {
|
|
12332
|
+
this.seedRingSkipLogged = true;
|
|
12333
|
+
this.logger?.info("decoder seed ring not filled — no frame-handle consumer on this broker", {
|
|
12334
|
+
tags: { deviceId: this.numericDeviceId },
|
|
12335
|
+
meta: { seedSec: StreamBroker.DECODER_SEED_BUFFER_SEC }
|
|
12336
|
+
});
|
|
12337
|
+
}
|
|
12203
12338
|
}
|
|
12204
12339
|
}
|
|
12205
12340
|
/**
|
|
@@ -12423,11 +12558,11 @@ var StreamBroker = class StreamBroker {
|
|
|
12423
12558
|
this.rtpRingHasKeyframe = true;
|
|
12424
12559
|
}
|
|
12425
12560
|
this.rtpRingPrevMarker = marker;
|
|
12426
|
-
if (this.rtpRingBytes >
|
|
12561
|
+
if (this.rtpRingBytes > 25165824) {
|
|
12427
12562
|
this.reportThrottled("rtp pre-buffer ring exceeded cap — join bootstrap dropped", {
|
|
12428
12563
|
bytes: this.rtpRingBytes,
|
|
12429
12564
|
packets: this.rtpRing.length,
|
|
12430
|
-
capBytes:
|
|
12565
|
+
capBytes: RTP_RING_MAX_BYTES
|
|
12431
12566
|
});
|
|
12432
12567
|
this.resetRtpPreBuffer();
|
|
12433
12568
|
}
|
|
@@ -12790,7 +12925,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12790
12925
|
*/
|
|
12791
12926
|
getRtpActivityWatchdogStatus() {
|
|
12792
12927
|
return {
|
|
12793
|
-
armed: this.
|
|
12928
|
+
armed: this.rtpActivitySweep !== void 0,
|
|
12794
12929
|
firedCount: this.rtpActivityWatchdogFiredCount
|
|
12795
12930
|
};
|
|
12796
12931
|
}
|
|
@@ -13489,28 +13624,16 @@ var StreamBroker = class StreamBroker {
|
|
|
13489
13624
|
const isH264 = codec === "h264";
|
|
13490
13625
|
const nalType = isH264 ? nal[0] & 31 : (nal[0] & 126) >> 1;
|
|
13491
13626
|
if (isH264 ? nalType === 7 || nalType === 8 : nalType === 32 || nalType === 33 || nalType === 34) {
|
|
13492
|
-
|
|
13493
|
-
0,
|
|
13494
|
-
0,
|
|
13495
|
-
0,
|
|
13496
|
-
1
|
|
13497
|
-
]);
|
|
13498
|
-
this.pendingParamNals.push(Buffer.concat([startCode, nal]));
|
|
13627
|
+
this.pendingParamNals.push(depacketized.annexB);
|
|
13499
13628
|
return;
|
|
13500
13629
|
}
|
|
13501
|
-
const startCode = Buffer.from([
|
|
13502
|
-
0,
|
|
13503
|
-
0,
|
|
13504
|
-
0,
|
|
13505
|
-
1
|
|
13506
|
-
]);
|
|
13507
13630
|
const hadParamSets = this.pendingParamNals.length > 0;
|
|
13508
13631
|
let annexBData;
|
|
13509
13632
|
if (hadParamSets) {
|
|
13510
|
-
this.pendingParamNals.push(
|
|
13633
|
+
this.pendingParamNals.push(depacketized.annexB);
|
|
13511
13634
|
annexBData = Buffer.concat(this.pendingParamNals);
|
|
13512
13635
|
this.pendingParamNals = [];
|
|
13513
|
-
} else annexBData =
|
|
13636
|
+
} else annexBData = depacketized.annexB;
|
|
13514
13637
|
const pts = Math.floor(timestamp / 90);
|
|
13515
13638
|
const packet = {
|
|
13516
13639
|
type: "video",
|
|
@@ -13653,22 +13776,38 @@ var StreamBroker = class StreamBroker {
|
|
|
13653
13776
|
* sources stay covered by the same arm/cancel lifecycle (the expiry
|
|
13654
13777
|
* path is reader-aware — it does nothing when no reader is alive).
|
|
13655
13778
|
*
|
|
13656
|
-
*
|
|
13657
|
-
*
|
|
13658
|
-
*
|
|
13779
|
+
* ALLOCATION-FREE per packet: it stamps {@link lastRtpActivityAt} and
|
|
13780
|
+
* arms ONE {@link StreamBroker.RTP_ACTIVITY_SWEEP_MS} interval the first
|
|
13781
|
+
* time. Do not turn this back into a per-packet `clearTimeout` +
|
|
13782
|
+
* `setTimeout` — that is a `Timeout` allocation and two libuv timer-heap
|
|
13783
|
+
* operations on the highest-frequency path in the busiest process.
|
|
13659
13784
|
*/
|
|
13660
13785
|
kickRtpActivityWatchdog() {
|
|
13661
13786
|
if (this.manualStop || this.stopping || this._suspended) return;
|
|
13662
|
-
|
|
13663
|
-
this.
|
|
13664
|
-
|
|
13665
|
-
|
|
13787
|
+
this.lastRtpActivityAt = Date.now();
|
|
13788
|
+
if (this.rtpActivitySweep !== void 0) return;
|
|
13789
|
+
this.rtpActivitySweep = setInterval(() => {
|
|
13790
|
+
this.sweepRtpActivity();
|
|
13791
|
+
}, StreamBroker.RTP_ACTIVITY_SWEEP_MS);
|
|
13792
|
+
this.rtpActivitySweep.unref?.();
|
|
13793
|
+
}
|
|
13794
|
+
/**
|
|
13795
|
+
* One sweep: declare the stall only once the full
|
|
13796
|
+
* {@link StreamBroker.RTP_ACTIVITY_TIMEOUT_MS} of silence has actually
|
|
13797
|
+
* elapsed since the last packet.
|
|
13798
|
+
*/
|
|
13799
|
+
sweepRtpActivity() {
|
|
13800
|
+
if (this.lastRtpActivityAt === 0) return;
|
|
13801
|
+
if (Date.now() - this.lastRtpActivityAt < StreamBroker.RTP_ACTIVITY_TIMEOUT_MS) return;
|
|
13802
|
+
this.cancelRtpActivityWatchdog();
|
|
13803
|
+
this.onRtpActivityWatchdogExpired();
|
|
13666
13804
|
}
|
|
13667
13805
|
/** Cancel the rolling RTP activity watchdog. Idempotent. */
|
|
13668
13806
|
cancelRtpActivityWatchdog() {
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
this.
|
|
13807
|
+
this.lastRtpActivityAt = 0;
|
|
13808
|
+
if (this.rtpActivitySweep === void 0) return;
|
|
13809
|
+
clearInterval(this.rtpActivitySweep);
|
|
13810
|
+
this.rtpActivitySweep = void 0;
|
|
13672
13811
|
}
|
|
13673
13812
|
/**
|
|
13674
13813
|
* Activity watchdog expiry: source RTP went silent for longer than
|
|
@@ -13683,7 +13822,6 @@ var StreamBroker = class StreamBroker {
|
|
|
13683
13822
|
* their own `PUSH_STALL_TIMEOUT_MS` path.
|
|
13684
13823
|
*/
|
|
13685
13824
|
onRtpActivityWatchdogExpired() {
|
|
13686
|
-
this.rtpActivityWatchdog = void 0;
|
|
13687
13825
|
this.rtpActivityWatchdogFiredCount++;
|
|
13688
13826
|
if (this.manualStop || this.stopping || this._suspended) return;
|
|
13689
13827
|
const meta = {
|
|
@@ -29295,6 +29433,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
29295
29433
|
}
|
|
29296
29434
|
}
|
|
29297
29435
|
async onInitialize() {
|
|
29436
|
+
this.ctx.logger.info("log channels declared", { meta: { channels: STREAM_BROKER_LOG_CHANNELS.map((c) => c.descriptor.name) } });
|
|
29298
29437
|
this.logChannels = createLogChannelsProvider(this.ctx.logger.child("log-channels"));
|
|
29299
29438
|
const logChannelsRegistration = {
|
|
29300
29439
|
capability: logChannelsCapability,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.142",
|
|
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",
|