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