@camstack/addon-pipeline 1.2.103 → 1.2.104
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/{addon-utils-CLc6yHCN.js → addon-utils-DRbRzrDy.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +844 -110
- package/dist/detection-pipeline/index.mjs +842 -108
- package/dist/{dist-Ccmt3fGJ.mjs → dist-D9sltFoR.mjs} +230 -6
- package/dist/{dist-C11WuNUP.js → dist-gLAVhbvO.js} +247 -5
- package/dist/{event-loop-stall-monitor-OJrOMeuu.mjs → event-loop-stall-monitor-BS_lPX6H.mjs} +82 -88
- package/dist/{event-loop-stall-monitor-Cq_NeC4o.js → event-loop-stall-monitor-BUhYY39J.js} +82 -88
- package/dist/{lazy-sharp-RxUs6on_.js → lazy-sharp-OiAUva0g.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +8 -6
- package/dist/pipeline-runner/index.mjs +6 -4
- package/dist/{process-memory-DOjQ3MgC.js → process-memory-DY4RHcTj.js} +1 -1
- package/dist/{process-memory-D0zDmXLI.mjs → process-memory-Dw7-9jCI.mjs} +1 -1
- package/dist/recorder/index.js +660 -130
- package/dist/recorder/index.mjs +659 -129
- package/dist/remote-restream-BeHi78PZ.mjs +25 -0
- package/dist/remote-restream-CO36Sr30.js +36 -0
- package/dist/restream-intent-B4BXZra7.mjs +72 -0
- package/dist/{remote-restream-BYbAsgUf.js → restream-intent-Cv9x3jmu.js} +30 -30
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BdgcF1lL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZaqZ17-x.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BnhMnnKV.mjs +26 -0
- package/dist/stream-broker/{hostInit-Da9wVA2r.mjs → hostInit-aZY81RnZ.mjs} +3 -3
- package/dist/stream-broker/index.js +598 -82
- package/dist/stream-broker/index.mjs +597 -81
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DDpliBIW.mjs → worker-protocol-Bz7-sMZC.mjs} +1 -1
- package/dist/{worker-protocol-BePduZVV.js → worker-protocol-D_0q_4Le.js} +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +193 -1
- package/python/postprocessors/__init__.py +4 -0
- package/python/postprocessors/ctc.py +52 -0
- package/python/postprocessors/plate_slots.py +180 -0
- package/python/postprocessors/test_ctc.py +39 -0
- package/python/postprocessors/test_plate_slots.py +217 -0
- package/python/postprocessors/test_yolonas.py +83 -0
- package/python/postprocessors/yolonas.py +67 -0
- package/python/test_inference_pool_backpressure.py +6 -2
- package/python/test_inference_pool_coreml_cache.py +12 -4
- package/python/test_inference_pool_device_selection.py +12 -4
- package/python/test_inference_pool_layout.py +16 -5
- package/python/test_inference_pool_memstats.py +6 -2
- package/python/test_inference_pool_ov_ppp.py +12 -9
- package/python/test_inference_pool_preprocess.py +57 -0
- package/python/test_inference_pool_static_batch.py +426 -0
- package/dist/remote-restream-Ci7RXNGb.mjs +0 -66
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DY31bUCj.mjs +0 -26
|
@@ -2,8 +2,9 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const
|
|
5
|
+
const require_dist = require("../dist-gLAVhbvO.js");
|
|
6
|
+
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
7
|
+
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
7
8
|
const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
|
|
8
9
|
let node_crypto = require("node:crypto");
|
|
9
10
|
node_crypto = require_dist.__toESM(node_crypto, 1);
|
|
@@ -1433,6 +1434,35 @@ var TS_COPYABLE_AUDIO_CODECS = new Set(["aac"]);
|
|
|
1433
1434
|
* neither gopFrames nor fps. Matches BASE_LIVE_EGRESS_PROFILE's intent.
|
|
1434
1435
|
*/
|
|
1435
1436
|
var DERIVED_DEFAULT_GOP_FRAMES = 25;
|
|
1437
|
+
/**
|
|
1438
|
+
* No B-frames, ever, unless the profile asks for them by name.
|
|
1439
|
+
*
|
|
1440
|
+
* A derived stream's carrier is MPEG-TS → {@link MpegTsDemux} → the broker →
|
|
1441
|
+
* RTP (an RTSP restream to the recorder, a WebRTC leg to a viewer). Every hop
|
|
1442
|
+
* has to carry the reorder delay and RTP has no field for it at all — so
|
|
1443
|
+
* reordering costs latency on the whole chain and buys compression nobody
|
|
1444
|
+
* asked for at 640x360.
|
|
1445
|
+
*/
|
|
1446
|
+
var DERIVED_DEFAULT_BF = 0;
|
|
1447
|
+
/**
|
|
1448
|
+
* The encode preset and tune the carrier pins when the profile names neither.
|
|
1449
|
+
*
|
|
1450
|
+
* `veryfast` + `zerolatency` is `BASE_LIVE_EGRESS_PROFILE`'s pairing, and it is
|
|
1451
|
+
* the pairing that removes x264's lookahead: `preset medium` carries
|
|
1452
|
+
* `rc-lookahead=40`, which at 25 fps is 1.6 SECONDS the encoder holds before it
|
|
1453
|
+
* emits anything. See this module's `withCarrierDefaults` for the measurement.
|
|
1454
|
+
*/
|
|
1455
|
+
var DERIVED_DEFAULT_PRESET = "veryfast";
|
|
1456
|
+
var DERIVED_DEFAULT_TUNE = "zerolatency";
|
|
1457
|
+
/**
|
|
1458
|
+
* The bit density a derived tier is capped at, expressed as the shared
|
|
1459
|
+
* live-egress profile it is scaled from: `BASE_LIVE_EGRESS_PROFILE` spends
|
|
1460
|
+
* 2500 kbps on 1280x720 at 25 fps. Scaling that per pixel-per-second gives
|
|
1461
|
+
* 625 kbps at 640x360@25 — and a 10 s segment of 625 kbps is ~780 KB, half of
|
|
1462
|
+
* the scrub's 1.5 MB whole-segment ceiling (D163).
|
|
1463
|
+
*/
|
|
1464
|
+
var DERIVED_BITRATE_REFERENCE_KBPS = 2500;
|
|
1465
|
+
var DERIVED_BITRATE_REFERENCE_PIXELS_PER_SECOND = 1280 * 720 * 25;
|
|
1436
1466
|
var DERIVED_AAC_RECODE = {
|
|
1437
1467
|
codec: "aac",
|
|
1438
1468
|
channels: 1
|
|
@@ -1486,12 +1516,66 @@ function resolveDerivedAudio(profileAudio, sourceAudioCodec) {
|
|
|
1486
1516
|
* own — the MPEG-TS carrier, not the operator, decides whether the source
|
|
1487
1517
|
* track can be copied.
|
|
1488
1518
|
*/
|
|
1519
|
+
/**
|
|
1520
|
+
* The rate ceiling for a geometry, or `undefined` when the profile named no
|
|
1521
|
+
* geometry to scale by (there is then nothing to derive a number from, and a
|
|
1522
|
+
* guessed cap on an unknown resolution is worse than none).
|
|
1523
|
+
*/
|
|
1524
|
+
function derivedBitrateCeilingKbps(video) {
|
|
1525
|
+
const { width, height } = video;
|
|
1526
|
+
if (width === void 0 || height === void 0) return void 0;
|
|
1527
|
+
const pixelsPerSecond = width * height * (video.fps ?? DERIVED_DEFAULT_GOP_FRAMES);
|
|
1528
|
+
return Math.round(DERIVED_BITRATE_REFERENCE_KBPS * pixelsPerSecond / DERIVED_BITRATE_REFERENCE_PIXELS_PER_SECOND);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Fill the encode knobs the CARRIER owns, leaving every one the profile names
|
|
1532
|
+
* untouched. Same rule as the audio decision above: an operator states intent,
|
|
1533
|
+
* the carrier states what its own hops can survive.
|
|
1534
|
+
*
|
|
1535
|
+
* - **GOP.** x264's own default is keyint=250 — TEN seconds at the fleet's
|
|
1536
|
+
* 25 fps. Measured live (camera dispensa, 2026-08-19): 1 I-frame in 250 on a
|
|
1537
|
+
* recorded low segment, and a WebRTC viewer joining mid-GOP decoded
|
|
1538
|
+
* reference-less P-frames for up to 10 s — the smear artifacts on every live
|
|
1539
|
+
* open of a derived tier, grid tiles included.
|
|
1540
|
+
*
|
|
1541
|
+
* - **B-frames, preset, tune.** Measured live (camera 1438, 2026-08-20) by
|
|
1542
|
+
* reading the camera's burnt-in OSD out of RECORDED segments and comparing
|
|
1543
|
+
* it against the wall clock the recorder named the file with. Same camera,
|
|
1544
|
+
* so the same OSD clock; mid-roll samples only, because the first segment of
|
|
1545
|
+
* a triggered roll carries pre-roll and is older than its own name:
|
|
1546
|
+
*
|
|
1547
|
+
* native `high` (-c:v copy): -1s -1s -1s -2s
|
|
1548
|
+
* derived `low` (transcode): -3s -3s -3s -3s
|
|
1549
|
+
*
|
|
1550
|
+
* ~2 s, and it is the encoder holding frames back: `preset medium` carries
|
|
1551
|
+
* `rc-lookahead=40` (1.6 s at 25 fps) on top of `bframes=3` with b-adapt and
|
|
1552
|
+
* b-pyramid. The recorder stamps a segment with the wall clock at WRITE
|
|
1553
|
+
* time, so all of it becomes a MISLABEL: seeking a derived tier to an
|
|
1554
|
+
* event's timestamp lands ~2 s past the event while the native tier of the
|
|
1555
|
+
* same camera lands on it.
|
|
1556
|
+
*
|
|
1557
|
+
* - **Bitrate.** A CEILING (`-maxrate`, no `-b:v` — see `buildRateControlArgs`,
|
|
1558
|
+
* whose default rate control is `cap`), because a derived tier is RECORDED
|
|
1559
|
+
* and the scrub reads a `low` segment whole only while it is under 1.5 MB
|
|
1560
|
+
* (D163). Uncapped CRF has no relationship to that ceiling: 1438/low
|
|
1561
|
+
* measured 537–941 kbps at 640x360@25 against 142 kbps for camera 615's
|
|
1562
|
+
* NATIVE low at the identical geometry, peaking at 1.19 MB in a 10 s
|
|
1563
|
+
* segment. Quiet scenes still spend less than the cap.
|
|
1564
|
+
*/
|
|
1565
|
+
function withCarrierDefaults(video) {
|
|
1566
|
+
const bitrateKbps = video.bitrateKbps ?? derivedBitrateCeilingKbps(video);
|
|
1567
|
+
return {
|
|
1568
|
+
...video,
|
|
1569
|
+
gopFrames: video.gopFrames ?? video.fps ?? DERIVED_DEFAULT_GOP_FRAMES,
|
|
1570
|
+
bf: video.bf ?? DERIVED_DEFAULT_BF,
|
|
1571
|
+
preset: video.preset ?? DERIVED_DEFAULT_PRESET,
|
|
1572
|
+
tune: video.tune ?? DERIVED_DEFAULT_TUNE,
|
|
1573
|
+
...bitrateKbps !== void 0 ? { bitrateKbps } : {}
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1489
1576
|
function buildDerivedTranscodeArgs(input) {
|
|
1490
1577
|
const audio = resolveDerivedAudio(input.profile.audio, input.sourceAudioCodec);
|
|
1491
|
-
const video = input.profile.video
|
|
1492
|
-
...input.profile.video,
|
|
1493
|
-
gopFrames: input.profile.video.fps ?? DERIVED_DEFAULT_GOP_FRAMES
|
|
1494
|
-
};
|
|
1578
|
+
const video = withCarrierDefaults(input.profile.video);
|
|
1495
1579
|
return {
|
|
1496
1580
|
args: require_dist.buildFfmpegArgs(require_dist.invocationFromEncodeProfile({
|
|
1497
1581
|
profile: {
|
|
@@ -1557,11 +1641,34 @@ var NOTHING = {
|
|
|
1557
1641
|
ageMs: 0,
|
|
1558
1642
|
gopsBack: 0
|
|
1559
1643
|
};
|
|
1560
|
-
/**
|
|
1561
|
-
|
|
1644
|
+
/**
|
|
1645
|
+
* A configured pre-roll in ms, or `0` when the input carries no instruction.
|
|
1646
|
+
*
|
|
1647
|
+
* `maxMs` is the ceiling of the WINDOW being resolved: detection's
|
|
1648
|
+
* ({@link PRE_ROLL_MAX_MS}) bounds a decoder's drain, recording's
|
|
1649
|
+
* ({@link RECORDING_PRE_ROLL_MAX_MS}) bounds the recorder's keep gate. One
|
|
1650
|
+
* clamp for both so a new window cannot arrive with its own rounding rules.
|
|
1651
|
+
*/
|
|
1652
|
+
function clampPreRoll(value, maxMs) {
|
|
1562
1653
|
if (value === void 0) return 0;
|
|
1563
1654
|
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
1564
|
-
return Math.min(
|
|
1655
|
+
return Math.min(maxMs, Math.max(250, value));
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* {@link clampPreRoll} at the WIDEST ceiling any window in this system may
|
|
1659
|
+
* carry.
|
|
1660
|
+
*
|
|
1661
|
+
* Used by the three functions that operate on a window ALREADY resolved by
|
|
1662
|
+
* `resolvePreRollMs` / `resolveRecordingPreRollMs` — the selection, the packet
|
|
1663
|
+
* ceiling and the span bound. Clamping those at the DETECTION ceiling was
|
|
1664
|
+
* correct while there was one window and became a silent 6 s truncation of
|
|
1665
|
+
* every recording burst the moment there were two: the selector would refuse
|
|
1666
|
+
* the keyframe the recorder asked for, and the span bound would withhold the
|
|
1667
|
+
* burst for being as wide as the operator configured — D192's failure, one
|
|
1668
|
+
* layer down.
|
|
1669
|
+
*/
|
|
1670
|
+
function usablePreRoll(value) {
|
|
1671
|
+
return clampPreRoll(value, RECORDING_PRE_ROLL_MAX_MS);
|
|
1565
1672
|
}
|
|
1566
1673
|
/**
|
|
1567
1674
|
* Choose the keyframe access unit a pre-roll burst starts at.
|
|
@@ -1599,6 +1706,104 @@ function selectPreRollStart(marks, preRollMs) {
|
|
|
1599
1706
|
};
|
|
1600
1707
|
}
|
|
1601
1708
|
/**
|
|
1709
|
+
* Ceiling for the RECORDING window.
|
|
1710
|
+
*
|
|
1711
|
+
* Wider than {@link PRE_ROLL_MAX_MS} because the two windows are bounded by
|
|
1712
|
+
* different things. Detection's ceiling is the backlog a decoder must drain
|
|
1713
|
+
* before it reaches the live edge — media the analyser pays for frame by frame.
|
|
1714
|
+
* A recorder drains nothing: ffmpeg copies the burst to disk. What bounds THIS
|
|
1715
|
+
* number is the recorder's retroactive keep window (`preBufferSec`), because
|
|
1716
|
+
* footage the broker hands the writer from before that bound is deleted by the
|
|
1717
|
+
* keep gate the moment it finalizes — the divergence D191/D192 exist to forbid.
|
|
1718
|
+
* `packages/addon-pipeline/src/recorder/addon/__tests__/band-decision.spec.ts`
|
|
1719
|
+
* fails if this is ever raised past the shipped keep window.
|
|
1720
|
+
*/
|
|
1721
|
+
var RECORDING_PRE_ROLL_MAX_MS = 15e3;
|
|
1722
|
+
/**
|
|
1723
|
+
* Shipped RECORDING pre-roll on a mains camera. Operator decision, 2026-08-20.
|
|
1724
|
+
*
|
|
1725
|
+
* Unlike detection's, this one ships ON. The audit of the same day measured the
|
|
1726
|
+
* reason: on camera 1438 the footage began AFTER the motion in 60 % of
|
|
1727
|
+
* episodes, because the writer is spawned at the trigger and ffmpeg's first
|
|
1728
|
+
* frame is at best the live edge. A recording feature whose media starts after
|
|
1729
|
+
* the event is not a tuning question.
|
|
1730
|
+
*/
|
|
1731
|
+
var DEFAULT_RECORDING_PRE_ROLL_MS = 1e4;
|
|
1732
|
+
/**
|
|
1733
|
+
* Budget when the broker has not measured a bitrate for this stream yet — the
|
|
1734
|
+
* historical flat cap, kept verbatim as the honest "we do not know".
|
|
1735
|
+
*
|
|
1736
|
+
* `bitrateKbps` is 0 for the first stats window after every dial and after
|
|
1737
|
+
* every reset, so this is the value in force exactly while the ring is filling
|
|
1738
|
+
* for the first time.
|
|
1739
|
+
*/
|
|
1740
|
+
var PRE_ROLL_UNKNOWN_BITRATE_BYTES = 8 * 1024 * 1024;
|
|
1741
|
+
/**
|
|
1742
|
+
* Absolute per-stream ceiling on retained pre-roll history.
|
|
1743
|
+
*
|
|
1744
|
+
* The bitrate-derived budget is what the window COSTS; this is the most any one
|
|
1745
|
+
* stream may spend regardless. 32 MiB is 4x the flat cap it replaces and covers
|
|
1746
|
+
* 15 s of a 16 Mbps stream with headroom; past it the operator has configured a
|
|
1747
|
+
* window this node cannot afford on that camera, and the broker says so rather
|
|
1748
|
+
* than delivering silently less (D192).
|
|
1749
|
+
*/
|
|
1750
|
+
var PRE_ROLL_ABSOLUTE_MAX_BYTES = 32 * 1024 * 1024;
|
|
1751
|
+
/**
|
|
1752
|
+
* Headroom over the OBSERVED mean bitrate.
|
|
1753
|
+
*
|
|
1754
|
+
* `BrokerStatsSchema.bitrateKbps` is a mean over the stats window; a GOP
|
|
1755
|
+
* carrying an IDR plus a scene change is well above it. Budgeting at exactly
|
|
1756
|
+
* the mean would drop the oldest GOP on every busy second, so the window would
|
|
1757
|
+
* be the one the operator asked for only while nothing moved — which is the
|
|
1758
|
+
* opposite of when it is needed.
|
|
1759
|
+
*/
|
|
1760
|
+
var PRE_ROLL_BITRATE_HEADROOM = 1.5;
|
|
1761
|
+
/**
|
|
1762
|
+
* The byte budget for the history a `windowMs` pre-roll needs on a stream
|
|
1763
|
+
* observed at `observedKbps`.
|
|
1764
|
+
*
|
|
1765
|
+
* Replaces a FLAT 8 MiB cap. That constant was a promise the feature could not
|
|
1766
|
+
* keep in either direction: above ~6.5 Mbps it silently truncated the window
|
|
1767
|
+
* (a 4K main stream asked for 10 s and got 6), and below it, it authorised a
|
|
1768
|
+
* doorbell at 512 kbps to hold 8 MiB — 16x what its own window weighs.
|
|
1769
|
+
*/
|
|
1770
|
+
function preRollBudgetBytes(windowMs, observedKbps) {
|
|
1771
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) return {
|
|
1772
|
+
bytes: 0,
|
|
1773
|
+
clamped: false
|
|
1774
|
+
};
|
|
1775
|
+
if (!Number.isFinite(observedKbps) || observedKbps <= 0) return {
|
|
1776
|
+
bytes: PRE_ROLL_UNKNOWN_BITRATE_BYTES,
|
|
1777
|
+
clamped: false
|
|
1778
|
+
};
|
|
1779
|
+
const raw = Math.round(windowMs * observedKbps * PRE_ROLL_BITRATE_HEADROOM / 8);
|
|
1780
|
+
if (raw > 33554432) return {
|
|
1781
|
+
bytes: PRE_ROLL_ABSOLUTE_MAX_BYTES,
|
|
1782
|
+
clamped: true
|
|
1783
|
+
};
|
|
1784
|
+
return {
|
|
1785
|
+
bytes: raw,
|
|
1786
|
+
clamped: false
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* The RECORDING window a broker retains and serves to a `/recording` dial.
|
|
1791
|
+
*
|
|
1792
|
+
* Precedence: stream override -> battery / terminal relay (0) -> cluster
|
|
1793
|
+
* default -> {@link DEFAULT_RECORDING_PRE_ROLL_MS}. An override wins over the
|
|
1794
|
+
* battery rule on purpose: the rule is a DEFAULT about where retention is
|
|
1795
|
+
* wasted, and an operator who typed a number for that stream has answered the
|
|
1796
|
+
* question the default was guessing at.
|
|
1797
|
+
*/
|
|
1798
|
+
function resolveRecordingPreRollMs(input) {
|
|
1799
|
+
const override = clampPreRoll(input.overrides?.[input.camStreamId], RECORDING_PRE_ROLL_MAX_MS);
|
|
1800
|
+
if (override > 0) return override;
|
|
1801
|
+
if (input.batteryOperated === true) return 0;
|
|
1802
|
+
if (input.terminalRelay === true) return 0;
|
|
1803
|
+
if (input.clusterMs === void 0) return DEFAULT_RECORDING_PRE_ROLL_MS;
|
|
1804
|
+
return clampPreRoll(input.clusterMs, RECORDING_PRE_ROLL_MAX_MS);
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1602
1807
|
* Combine the cluster default and the per-camera-stream override into the one
|
|
1603
1808
|
* pre-roll a broker applies. Precedence: stream override → cluster default →
|
|
1604
1809
|
* OFF. Each layer is validated independently, so a bad override falls back to a
|
|
@@ -1609,10 +1814,10 @@ function selectPreRollStart(marks, preRollMs) {
|
|
|
1609
1814
|
* coincide on every unconfigured stream.
|
|
1610
1815
|
*/
|
|
1611
1816
|
function resolvePreRollMs(input) {
|
|
1612
|
-
const override =
|
|
1817
|
+
const override = clampPreRoll(input.overrides?.[input.camStreamId], PRE_ROLL_MAX_MS);
|
|
1613
1818
|
if (override > 0) return override;
|
|
1614
1819
|
if (input.terminalRelay === true) return 0;
|
|
1615
|
-
return
|
|
1820
|
+
return clampPreRoll(input.clusterMs, PRE_ROLL_MAX_MS);
|
|
1616
1821
|
}
|
|
1617
1822
|
/**
|
|
1618
1823
|
* The packet ceiling for a burst carrying `preRollMs` of pre-roll.
|
|
@@ -2282,8 +2487,13 @@ var RtspRestreamer = class {
|
|
|
2282
2487
|
* A second supplier and not a widening of {@link rtpPreBufferSupplier},
|
|
2283
2488
|
* because the two feed different populations: that one is also the WebRTC
|
|
2284
2489
|
* bootstrap and every ordinary RTSP join, and handing them history is the
|
|
2285
|
-
* D57 failure. Only a session that
|
|
2286
|
-
*
|
|
2490
|
+
* D57 failure. Only a session that DECLARED an intent on its dial path is
|
|
2491
|
+
* served from this one.
|
|
2492
|
+
*
|
|
2493
|
+
* Takes the intent, because the two windows are two numbers (D193): a
|
|
2494
|
+
* detection dial drains its backlog frame by frame and a recorder copies it
|
|
2495
|
+
* to disk, so the operator sets them apart. One supplier keyed by intent, and
|
|
2496
|
+
* not two suppliers, so a third intent cannot arrive with its own wiring.
|
|
2287
2497
|
*
|
|
2288
2498
|
* Pulled per join for the same reason as the bound: a broker outlives the
|
|
2289
2499
|
* setting.
|
|
@@ -2577,36 +2787,74 @@ var RtspRestreamer = class {
|
|
|
2577
2787
|
if (this.sessions.get(sessionId)?.isPlaying()) ready.push(sessionId);
|
|
2578
2788
|
}
|
|
2579
2789
|
if (ready.length === 0) return;
|
|
2580
|
-
const
|
|
2581
|
-
const preRollPackets = fromRing && preRoll && preRoll.preRollMs > 0 && preRoll.packets.length > primePackets.length ? preRoll.packets.map((data) => ({
|
|
2582
|
-
data,
|
|
2583
|
-
marker: (data[1] & 128) !== 0
|
|
2584
|
-
})) : [];
|
|
2585
|
-
const preRollActive = preRollPackets.length > 0;
|
|
2586
|
-
const preRollSpanMs = preRollActive ? (preRollPackets[preRollPackets.length - 1].data.readUInt32BE(4) - preRollPackets[0].data.readUInt32BE(4) | 0) / 90 : 0;
|
|
2790
|
+
const preRollPlans = /* @__PURE__ */ new Map();
|
|
2587
2791
|
const bound = this.currentPrimeBound();
|
|
2588
2792
|
const overSpan = spanMs > bound.maxSpanMs;
|
|
2589
2793
|
const overPackets = primePackets.length > bound.maxPackets;
|
|
2590
|
-
|
|
2591
|
-
|
|
2794
|
+
/**
|
|
2795
|
+
* Resolve (and memoize) the plan for one intent.
|
|
2796
|
+
*
|
|
2797
|
+
* The bound a PRE-ROLL burst is held to is D57's, widened to the pre-roll
|
|
2798
|
+
* the operator configured FOR THAT INTENT, with the packet ceiling scaled
|
|
2799
|
+
* by the same factor so the defensive backstop stays a rate. A window WIDER
|
|
2800
|
+
* than that is still withheld — the selector cannot produce one, but a long
|
|
2801
|
+
* current GOP or a supplier bug can, and D57's remedy (join clean at the
|
|
2802
|
+
* next live keyframe) is the right one for a burst nobody asked for.
|
|
2803
|
+
*/
|
|
2804
|
+
const planFor = (intent) => {
|
|
2805
|
+
const cached = preRollPlans.get(intent);
|
|
2806
|
+
if (cached) return cached;
|
|
2807
|
+
const burst = this.preRollSupplier?.(intent);
|
|
2808
|
+
const window = fromRing && burst && burst.preRollMs > 0 ? burst.packets() : [];
|
|
2809
|
+
const packets = window.length > primePackets.length ? window.map((data) => ({
|
|
2810
|
+
data,
|
|
2811
|
+
marker: (data[1] & 128) !== 0
|
|
2812
|
+
})) : [];
|
|
2813
|
+
const windowSpanMs = packets.length > 0 ? (packets[packets.length - 1].data.readUInt32BE(4) - packets[0].data.readUInt32BE(4) | 0) / 90 : 0;
|
|
2814
|
+
const windowBound = packets.length > 0 ? preRollSpanBound(bound, burst?.preRollMs ?? 0) : bound;
|
|
2815
|
+
const plan = {
|
|
2816
|
+
packets,
|
|
2817
|
+
active: packets.length > 0,
|
|
2818
|
+
configuredMs: burst?.preRollMs ?? 0,
|
|
2819
|
+
spanMs: windowSpanMs,
|
|
2820
|
+
overBound: packets.length > 0 && (windowSpanMs > windowBound.maxSpanMs || packets.length > windowBound.maxPackets)
|
|
2821
|
+
};
|
|
2822
|
+
preRollPlans.set(intent, plan);
|
|
2823
|
+
return plan;
|
|
2824
|
+
};
|
|
2592
2825
|
const served = [];
|
|
2593
|
-
const servedPreRoll = /* @__PURE__ */ new
|
|
2826
|
+
const servedPreRoll = /* @__PURE__ */ new Map();
|
|
2594
2827
|
let withheld = 0;
|
|
2595
2828
|
let detectionIntentServed = 0;
|
|
2596
2829
|
for (const sessionId of ready) {
|
|
2597
2830
|
const session = this.sessions.get(sessionId);
|
|
2598
2831
|
if (!session) continue;
|
|
2599
2832
|
const detectionIntent = session.isDetectionIntent();
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2833
|
+
const preRollIntent = session.preRollIntent();
|
|
2834
|
+
if (preRollIntent !== null) {
|
|
2835
|
+
const plan = planFor(preRollIntent);
|
|
2836
|
+
if (plan.active) {
|
|
2837
|
+
if (plan.overBound) {
|
|
2838
|
+
this.dropsWithheldSpan++;
|
|
2839
|
+
withheld++;
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
servedPreRoll.set(sessionId, preRollIntent);
|
|
2843
|
+
served.push(sessionId);
|
|
2844
|
+
this.logger?.info("restream pre-roll served", {
|
|
2845
|
+
tags: { deviceId: this.deviceId },
|
|
2846
|
+
meta: {
|
|
2847
|
+
streamName: this.streamName,
|
|
2848
|
+
sessionId,
|
|
2849
|
+
intent: preRollIntent,
|
|
2850
|
+
consumer: session.getUserAgent() ?? "unknown",
|
|
2851
|
+
configuredMs: plan.configuredMs,
|
|
2852
|
+
spanMs: Math.round(plan.spanMs),
|
|
2853
|
+
packets: plan.packets.length
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2604
2856
|
continue;
|
|
2605
2857
|
}
|
|
2606
|
-
detectionIntentServed++;
|
|
2607
|
-
servedPreRoll.add(sessionId);
|
|
2608
|
-
served.push(sessionId);
|
|
2609
|
-
continue;
|
|
2610
2858
|
}
|
|
2611
2859
|
if (session.isMuted() && !detectionIntent) {
|
|
2612
2860
|
if (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS) {
|
|
@@ -2625,6 +2873,11 @@ var RtspRestreamer = class {
|
|
|
2625
2873
|
}
|
|
2626
2874
|
if (withheld > 0) this.reportDrops(false);
|
|
2627
2875
|
if (served.length === 0) return;
|
|
2876
|
+
/** Media actually delivered to this intent, or `-1` when none joined. */
|
|
2877
|
+
const servedSpanMs = (intent) => {
|
|
2878
|
+
const plan = preRollPlans.get(intent);
|
|
2879
|
+
return plan === void 0 ? -1 : Math.round(plan.spanMs);
|
|
2880
|
+
};
|
|
2628
2881
|
this.logger?.info("restream prime (lat-probe)", {
|
|
2629
2882
|
tags: { deviceId: this.deviceId },
|
|
2630
2883
|
meta: {
|
|
@@ -2637,9 +2890,10 @@ var RtspRestreamer = class {
|
|
|
2637
2890
|
boundPackets: bound.maxPackets,
|
|
2638
2891
|
withheld,
|
|
2639
2892
|
detectionIntentServed,
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2893
|
+
detectionPreRollMs: this.preRollSupplier?.("detection").preRollMs ?? 0,
|
|
2894
|
+
recordingPreRollMs: this.preRollSupplier?.("recording").preRollMs ?? 0,
|
|
2895
|
+
detectionPreRollSpanMs: servedSpanMs("detection"),
|
|
2896
|
+
recordingPreRollSpanMs: servedSpanMs("recording"),
|
|
2643
2897
|
preRollServed: servedPreRoll.size
|
|
2644
2898
|
}
|
|
2645
2899
|
});
|
|
@@ -2648,14 +2902,16 @@ var RtspRestreamer = class {
|
|
|
2648
2902
|
prime: true,
|
|
2649
2903
|
keyframeAuStart: false
|
|
2650
2904
|
}));
|
|
2651
|
-
const preRollItems =
|
|
2905
|
+
const preRollItems = /* @__PURE__ */ new Map();
|
|
2906
|
+
for (const intent of new Set(servedPreRoll.values())) preRollItems.set(intent, (preRollPlans.get(intent)?.packets ?? []).map((packet) => ({
|
|
2652
2907
|
packet,
|
|
2653
2908
|
prime: true,
|
|
2654
2909
|
keyframeAuStart: false
|
|
2655
|
-
}))
|
|
2910
|
+
})));
|
|
2656
2911
|
for (const sessionId of served) {
|
|
2657
2912
|
this.pendingKeyframe.delete(sessionId);
|
|
2658
|
-
|
|
2913
|
+
const intent = servedPreRoll.get(sessionId);
|
|
2914
|
+
this.startPrimeReplay(sessionId, intent ? preRollItems.get(intent) ?? items : items, intent ?? null);
|
|
2659
2915
|
}
|
|
2660
2916
|
}
|
|
2661
2917
|
/**
|
|
@@ -2668,7 +2924,7 @@ var RtspRestreamer = class {
|
|
|
2668
2924
|
* session that went away — aborts the whole replay rather than punching holes
|
|
2669
2925
|
* in the GOP: the session is left latched and rejoins at the next keyframe AU.
|
|
2670
2926
|
*/
|
|
2671
|
-
startPrimeReplay(sessionId, items) {
|
|
2927
|
+
startPrimeReplay(sessionId, items, preRollIntent) {
|
|
2672
2928
|
const replay = new PacedRtpReplay((item) => {
|
|
2673
2929
|
const session = this.sessions.get(sessionId);
|
|
2674
2930
|
if (!session) {
|
|
@@ -2684,6 +2940,20 @@ var RtspRestreamer = class {
|
|
|
2684
2940
|
return false;
|
|
2685
2941
|
}, (stats) => {
|
|
2686
2942
|
this.activeReplays.delete(sessionId);
|
|
2943
|
+
if (preRollIntent !== null) this.logger?.info("restream pre-roll replay done", {
|
|
2944
|
+
tags: { deviceId: this.deviceId },
|
|
2945
|
+
meta: {
|
|
2946
|
+
streamName: this.streamName,
|
|
2947
|
+
sessionId,
|
|
2948
|
+
intent: preRollIntent,
|
|
2949
|
+
consumer: this.sessions.get(sessionId)?.getUserAgent() ?? "unknown",
|
|
2950
|
+
packets: stats.packets,
|
|
2951
|
+
sent: stats.sent,
|
|
2952
|
+
aborted: stats.aborted,
|
|
2953
|
+
enqueuedLive: stats.enqueuedLive,
|
|
2954
|
+
durationMs: stats.durationMs
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2687
2957
|
this.logger?.debug("restream prime drained", {
|
|
2688
2958
|
tags: { deviceId: this.deviceId },
|
|
2689
2959
|
meta: {
|
|
@@ -3032,6 +3302,17 @@ var RtspSession = class {
|
|
|
3032
3302
|
return this.muted;
|
|
3033
3303
|
}
|
|
3034
3304
|
/**
|
|
3305
|
+
* The `User-Agent` this client put on its first request, or `null`.
|
|
3306
|
+
*
|
|
3307
|
+
* The ONLY identity that crosses from a consumer process into this session.
|
|
3308
|
+
* The restreamer prints it beside the pre-roll window it serves, which is how
|
|
3309
|
+
* a burst the broker measured can be tied to the file the consumer wrote —
|
|
3310
|
+
* `sessionId` is minted here and appears in no consumer's logs.
|
|
3311
|
+
*/
|
|
3312
|
+
getUserAgent() {
|
|
3313
|
+
return this.userAgent;
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3035
3316
|
* True only for a session that dialed the DETECTION path
|
|
3036
3317
|
* (`DETECTION_MUTED_PATH_SUFFIX`). Read by
|
|
3037
3318
|
* `RtspRestreamer.servePendingSessionsRtp` to exempt this session from the
|
|
@@ -3042,6 +3323,28 @@ var RtspSession = class {
|
|
|
3042
3323
|
return this.intent === "detection";
|
|
3043
3324
|
}
|
|
3044
3325
|
/**
|
|
3326
|
+
* Does this session get the PRE-ROLL window instead of the current GOP?
|
|
3327
|
+
*
|
|
3328
|
+
* Both declared intents do, for the same reason: they attach after the edge
|
|
3329
|
+
* they need to see. Separate from {@link isDetectionIntent} because that one
|
|
3330
|
+
* also lifts the live-edge withhold, which is a MUTED-consumer rule and must
|
|
3331
|
+
* not be lifted for anyone else.
|
|
3332
|
+
*/
|
|
3333
|
+
wantsPreRoll() {
|
|
3334
|
+
return this.preRollIntent() !== null;
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* WHICH pre-roll window this session gets, or `null` when it declared no
|
|
3338
|
+
* intent and takes the ordinary current-GOP join burst.
|
|
3339
|
+
*
|
|
3340
|
+
* The two windows are two numbers (D193), so the restreamer needs the intent
|
|
3341
|
+
* and not just the bit: a recorder handed detection's window would carry the
|
|
3342
|
+
* pre-roll of a feature it is not, and on this fleet detection's is 0.
|
|
3343
|
+
*/
|
|
3344
|
+
preRollIntent() {
|
|
3345
|
+
return this.intent === "detection" || this.intent === "recording" ? this.intent : null;
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3045
3348
|
* Diagnostic snapshot consumed by `StreamBroker.listClients` (Phase 10).
|
|
3046
3349
|
* Gives operators the information needed to identify a specific RTSP
|
|
3047
3350
|
* session: where it's coming from, whether it's active, when it
|
|
@@ -3380,7 +3683,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
3380
3683
|
async dispatch(socket, requestBuffer) {
|
|
3381
3684
|
const uriMatch = (requestBuffer.split("\r\n")[0] ?? "").match(/\s(rtsp:\/\/[^\s]+)\s/);
|
|
3382
3685
|
const pathMatch = (uriMatch ? uriMatch[1] : "").match(/rtsp:\/\/[^/]+(\/.*?)(?:\s|$)/);
|
|
3383
|
-
const { lookupPath, muted: isMuted, intent } =
|
|
3686
|
+
const { lookupPath, muted: isMuted, intent } = require_restream_intent.parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
|
|
3384
3687
|
const restreamer = this.restreamers.get(lookupPath);
|
|
3385
3688
|
if (!restreamer) {
|
|
3386
3689
|
socket.write(`RTSP/1.0 404 Not Found\r\nCSeq: 1\r\n\r\n`);
|
|
@@ -11031,6 +11334,8 @@ var StreamBroker = class StreamBroker {
|
|
|
11031
11334
|
* isn't useful. 500 ms is wider than the worst-case fan-out delay.
|
|
11032
11335
|
*/
|
|
11033
11336
|
lastRefreshRequestAt = 0;
|
|
11337
|
+
/** When {@link kickReconnect} last acted, for its own throttle. */
|
|
11338
|
+
lastKickAt = 0;
|
|
11034
11339
|
/** Detected or configured codec — persists across reconnects */
|
|
11035
11340
|
detectedCodec;
|
|
11036
11341
|
/**
|
|
@@ -11168,6 +11473,58 @@ var StreamBroker = class StreamBroker {
|
|
|
11168
11473
|
*/
|
|
11169
11474
|
static FIRST_VIDEO_TIMEOUT_MS = 8e3;
|
|
11170
11475
|
/**
|
|
11476
|
+
* The first-video deadline for a dial that FOLLOWED a lazy materialize.
|
|
11477
|
+
*
|
|
11478
|
+
* 8 s was sized against Chrome's ~15 s ICE-disconnect: detect a stuck stream
|
|
11479
|
+
* before the viewer's browser gives up. That reasoning holds for a source
|
|
11480
|
+
* that is already open and has stalled. It is exactly wrong for a COLD open
|
|
11481
|
+
* behind a lazy publisher.
|
|
11482
|
+
*
|
|
11483
|
+
* Measured on the wyze bridge (devices 3838/3836, 2026-08-20): a successful
|
|
11484
|
+
* open costs **12.4 s** — `WyzeRfc4571Server` waits for the first keyframe
|
|
11485
|
+
* with a 10 s ceiling, on top of ~2 s of DTLS handshake. At 8 s the watchdog
|
|
11486
|
+
* tore down a connection that was two seconds from delivering, and the cost
|
|
11487
|
+
* of that teardown is not "try again quickly": the bridge closes its P2P
|
|
11488
|
+
* server 30 s after the last client, so the next dial repays the whole cold
|
|
11489
|
+
* open. Chrome had given up either way; the only thing the short deadline
|
|
11490
|
+
* changed was whether the NEXT viewer paid for it again too.
|
|
11491
|
+
*
|
|
11492
|
+
* 20 s covers the measured open with margin and still fires well inside the
|
|
11493
|
+
* activity watchdog's own recovery path for a connection that is genuinely
|
|
11494
|
+
* dead.
|
|
11495
|
+
*/
|
|
11496
|
+
static LAZY_MATERIALIZE_FIRST_VIDEO_TIMEOUT_MS = 2e4;
|
|
11497
|
+
/**
|
|
11498
|
+
* How long after a lazy materialize was REQUESTED a dial still counts as
|
|
11499
|
+
* "this one is the cold open we asked for". Past it the source is an
|
|
11500
|
+
* ordinary open and takes the ordinary deadline.
|
|
11501
|
+
*/
|
|
11502
|
+
static MATERIALIZE_RECENT_MS = 3e4;
|
|
11503
|
+
/**
|
|
11504
|
+
* Poll cadence while a lazy publisher is materializing its upstream.
|
|
11505
|
+
*
|
|
11506
|
+
* The exponential backoff is the right shape for a source that is FAILING
|
|
11507
|
+
* and the wrong one for a source that is being built: the publisher answers
|
|
11508
|
+
* in ~12 s, and 1-2-4-8-16 s means the broker learns about it at 16 — up to
|
|
11509
|
+
* 4 s of pure quantisation on every wyze cold open. Waiting at a flat 1 s
|
|
11510
|
+
* costs one cheap re-resolve per second against a bounded deadline.
|
|
11511
|
+
*/
|
|
11512
|
+
static MATERIALIZE_POLL_MS = 1e3;
|
|
11513
|
+
/**
|
|
11514
|
+
* Bound on the flat-poll wait. Past it the materialize is not slow, it has
|
|
11515
|
+
* failed, and the ordinary exponential backoff is the right answer again.
|
|
11516
|
+
* Sized at the measured open (12.4 s) plus room for one retry inside the
|
|
11517
|
+
* publisher.
|
|
11518
|
+
*/
|
|
11519
|
+
static MATERIALIZE_WAIT_MS = 2e4;
|
|
11520
|
+
/**
|
|
11521
|
+
* When the current lazy materialize was first requested, or `0` when none is
|
|
11522
|
+
* outstanding. Drives BOTH the flat poll above and the extended first-video
|
|
11523
|
+
* deadline — one fact, read in two places, so a materialize cannot be "in
|
|
11524
|
+
* progress" for the dial loop and "not happening" for the watchdog.
|
|
11525
|
+
*/
|
|
11526
|
+
materializeRequestedAt = 0;
|
|
11527
|
+
/**
|
|
11171
11528
|
* Rolling source-RTP activity watchdog. Distinct from the first-video
|
|
11172
11529
|
* watchdog: this one ARMS at DIAL time (so a connection that opens the
|
|
11173
11530
|
* loopback TCP but never delivers media is still detected) and RESETS on
|
|
@@ -11339,7 +11696,21 @@ var StreamBroker = class StreamBroker {
|
|
|
11339
11696
|
* then behaves exactly as it did before pre-roll existed. Resolved per stream
|
|
11340
11697
|
* by `resolvePreRollMs` and pushed by the manager.
|
|
11341
11698
|
*/
|
|
11342
|
-
|
|
11699
|
+
detectionPreRollMs = 0;
|
|
11700
|
+
/**
|
|
11701
|
+
* Media time (ms) of history a RECORDING dial (`/recording`) is handed — the
|
|
11702
|
+
* `events`-band recorder's ffmpeg, which is spawned AT the trigger and whose
|
|
11703
|
+
* first frame is otherwise at best the live edge.
|
|
11704
|
+
*
|
|
11705
|
+
* A SECOND number, not a wider first one (D193). The two windows are set by
|
|
11706
|
+
* the operator for different reasons and cost different things: detection's
|
|
11707
|
+
* is a backlog a decoder must drain, recording's is bytes copied to disk.
|
|
11708
|
+
* Raising one to serve the other would change every detection dial on the
|
|
11709
|
+
* fleet to buy a recorder ten seconds — the coupling the 2026-08-20 census
|
|
11710
|
+
* named before anything was turned on. Resolved by `resolveRecordingPreRollMs`
|
|
11711
|
+
* and pushed by the manager.
|
|
11712
|
+
*/
|
|
11713
|
+
recordingPreRollMs = 0;
|
|
11343
11714
|
/**
|
|
11344
11715
|
* Effective join-burst bound for this stream's RTSP restream — the cluster
|
|
11345
11716
|
* default, or this camera stream's override. Applies to the RESTREAM
|
|
@@ -11353,20 +11724,9 @@ var StreamBroker = class StreamBroker {
|
|
|
11353
11724
|
* long-GOP high-bitrate stream) drops the bootstrap rather than balloon
|
|
11354
11725
|
* memory — late joiners fall back to waiting for the next keyframe. */
|
|
11355
11726
|
static RTP_RING_MAX_BYTES = 24 * 1024 * 1024;
|
|
11356
|
-
/**
|
|
11357
|
-
*
|
|
11358
|
-
|
|
11359
|
-
*
|
|
11360
|
-
* Its own budget rather than a share of {@link RTP_RING_MAX_BYTES} because
|
|
11361
|
-
* the two failures are different: reaching the ring cap calls
|
|
11362
|
-
* `resetRtpPreBuffer`, which drops the join bootstrap of EVERY consumer —
|
|
11363
|
-
* WebRTC viewers included — for as long as the condition holds. A pre-roll
|
|
11364
|
-
* that pushed a 4K stream into that state would be a knob for one camera's
|
|
11365
|
-
* detection quietly degrading everybody else's picture. Over this budget the
|
|
11366
|
-
* OLDEST retained GOP is dropped whole (never a tail cut), so the window
|
|
11367
|
-
* stays decodable and simply carries less history than asked for.
|
|
11368
|
-
*/
|
|
11369
|
-
static PRE_ROLL_MAX_BYTES = 8 * 1024 * 1024;
|
|
11727
|
+
/** Last budget printed by {@link reportPreRollClamp}, so the line fires on a
|
|
11728
|
+
* TRANSITION rather than on every keyframe. */
|
|
11729
|
+
preRollClampLogged = false;
|
|
11370
11730
|
/** Stream stats tracking */
|
|
11371
11731
|
totalBytes = 0;
|
|
11372
11732
|
bytesInWindow = 0;
|
|
@@ -11406,10 +11766,13 @@ var StreamBroker = class StreamBroker {
|
|
|
11406
11766
|
if (logger) this.rtspRestreamer.setLogger(logger.child("rtsp"));
|
|
11407
11767
|
this.rtspRestreamer.setRtpPreBufferSupplier(() => this.getRtpPreBuffer());
|
|
11408
11768
|
this.rtspRestreamer.setPrimeBoundSupplier(() => this.joinBurstBound);
|
|
11409
|
-
this.rtspRestreamer.setPreRollSupplier(() =>
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11769
|
+
this.rtspRestreamer.setPreRollSupplier((intent) => {
|
|
11770
|
+
const windowMs = intent === "recording" ? this.recordingPreRollMs : this.detectionPreRollMs;
|
|
11771
|
+
return {
|
|
11772
|
+
packets: () => this.getRtpPreRollBuffer(windowMs),
|
|
11773
|
+
preRollMs: windowMs
|
|
11774
|
+
};
|
|
11775
|
+
});
|
|
11413
11776
|
this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
|
|
11414
11777
|
this.decoderSeedBuffer = new EncodedRingBuffer(StreamBroker.DECODER_SEED_BUFFER_SEC);
|
|
11415
11778
|
this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"));
|
|
@@ -11557,6 +11920,9 @@ var StreamBroker = class StreamBroker {
|
|
|
11557
11920
|
if (this.manualStop || this.stopping || !this.source) return;
|
|
11558
11921
|
if (!this.hasDemand()) return;
|
|
11559
11922
|
if (!this.reconnectTimer) return;
|
|
11923
|
+
const nowMs = Date.now();
|
|
11924
|
+
if (nowMs - this.lastKickAt < INITIAL_RECONNECT_DELAY_MS) return;
|
|
11925
|
+
this.lastKickAt = nowMs;
|
|
11560
11926
|
clearTimeout(this.reconnectTimer);
|
|
11561
11927
|
this.reconnectTimer = void 0;
|
|
11562
11928
|
this.reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
|
|
@@ -12421,16 +12787,61 @@ var StreamBroker = class StreamBroker {
|
|
|
12421
12787
|
* freshly-armed decoder, and a 6 s retention cannot serve a 6 s pre-roll plus
|
|
12422
12788
|
* the GOP that carries it.
|
|
12423
12789
|
*/
|
|
12424
|
-
|
|
12790
|
+
setDetectionPreRollMs(ms) {
|
|
12425
12791
|
const next = Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
12426
|
-
if (next === this.
|
|
12427
|
-
this.
|
|
12792
|
+
if (next === this.detectionPreRollMs) return;
|
|
12793
|
+
this.detectionPreRollMs = next;
|
|
12428
12794
|
const seedSec = Math.max(StreamBroker.DECODER_SEED_BUFFER_SEC, Math.ceil(next / 1e3) + 2);
|
|
12429
12795
|
this.decoderSeedBuffer.setDuration(seedSec);
|
|
12430
12796
|
}
|
|
12431
|
-
/**
|
|
12432
|
-
|
|
12433
|
-
|
|
12797
|
+
/**
|
|
12798
|
+
* Configure how much history a RECORDING dial is handed.
|
|
12799
|
+
*
|
|
12800
|
+
* Touches the RETENTION (through {@link getPreRollRetentionMs}) and nothing
|
|
12801
|
+
* else: no decoder seed, no change to what any other consumer is served.
|
|
12802
|
+
*/
|
|
12803
|
+
setRecordingPreRollMs(ms) {
|
|
12804
|
+
const next = Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
12805
|
+
this.recordingPreRollMs = next;
|
|
12806
|
+
}
|
|
12807
|
+
/** The DETECTION pre-roll in force for this stream (ms). `0` = off. */
|
|
12808
|
+
getDetectionPreRollMs() {
|
|
12809
|
+
return this.detectionPreRollMs;
|
|
12810
|
+
}
|
|
12811
|
+
/** The RECORDING pre-roll in force for this stream (ms). `0` = off. */
|
|
12812
|
+
getRecordingPreRollMs() {
|
|
12813
|
+
return this.recordingPreRollMs;
|
|
12814
|
+
}
|
|
12815
|
+
/**
|
|
12816
|
+
* How far back the ring is kept: the WIDER of the two windows.
|
|
12817
|
+
*
|
|
12818
|
+
* One retention serves both because the bytes are the same bytes — the ring
|
|
12819
|
+
* is cut per intent at read time ({@link getRtpPreRollBuffer}), so a 10 s
|
|
12820
|
+
* recording window does not hand a detection dial 10 s of backlog. Retaining
|
|
12821
|
+
* per intent would be two rings of the same packets.
|
|
12822
|
+
*/
|
|
12823
|
+
getPreRollRetentionMs() {
|
|
12824
|
+
return Math.max(this.detectionPreRollMs, this.recordingPreRollMs);
|
|
12825
|
+
}
|
|
12826
|
+
/**
|
|
12827
|
+
* Byte budget for the EXTRA history pre-roll retains, over and above the
|
|
12828
|
+
* current GOP — derived from the window in force and this stream's OBSERVED
|
|
12829
|
+
* bitrate.
|
|
12830
|
+
*
|
|
12831
|
+
* Its own budget rather than a share of {@link RTP_RING_MAX_BYTES} because
|
|
12832
|
+
* the two failures are different: reaching the ring cap calls
|
|
12833
|
+
* `resetRtpPreBuffer`, which drops the join bootstrap of EVERY consumer —
|
|
12834
|
+
* WebRTC viewers included — for as long as the condition holds. A pre-roll
|
|
12835
|
+
* that pushed a 4K stream into that state would be a knob for one camera's
|
|
12836
|
+
* detection quietly degrading everybody else's picture. Over this budget the
|
|
12837
|
+
* OLDEST retained GOP is dropped whole (never a tail cut), so the window
|
|
12838
|
+
* stays decodable and simply carries less history than asked for.
|
|
12839
|
+
*
|
|
12840
|
+
* It replaced a FLAT 8 MiB constant, which was a promise this feature could
|
|
12841
|
+
* not keep in either direction — see {@link preRollBudgetBytes}.
|
|
12842
|
+
*/
|
|
12843
|
+
getPreRollBudget() {
|
|
12844
|
+
return preRollBudgetBytes(this.getPreRollRetentionMs(), this.bitrateKbps);
|
|
12434
12845
|
}
|
|
12435
12846
|
/**
|
|
12436
12847
|
* The seed replayed into a freshly-armed PUSH-mode decoder session.
|
|
@@ -12444,7 +12855,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12444
12855
|
* both, so a fix to the boundary cannot land on only one plane.
|
|
12445
12856
|
*/
|
|
12446
12857
|
getDecoderSeedPackets() {
|
|
12447
|
-
if (this.
|
|
12858
|
+
if (this.detectionPreRollMs <= 0) return this.decoderSeedBuffer.getPackets();
|
|
12448
12859
|
const all = this.decoderSeedBuffer.getAllPackets();
|
|
12449
12860
|
if (all.length === 0) return [];
|
|
12450
12861
|
const newestPts = all[all.length - 1].pts;
|
|
@@ -12456,7 +12867,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12456
12867
|
ageMs: newestPts - packet.pts
|
|
12457
12868
|
});
|
|
12458
12869
|
}
|
|
12459
|
-
const selection = selectPreRollStart(marks, this.
|
|
12870
|
+
const selection = selectPreRollStart(marks, this.detectionPreRollMs);
|
|
12460
12871
|
return selection.startIndex < 0 ? [] : all.slice(selection.startIndex);
|
|
12461
12872
|
}
|
|
12462
12873
|
/** Retained keyframe AUs as ages in ms of media time behind `newestTs`. */
|
|
@@ -12478,14 +12889,18 @@ var StreamBroker = class StreamBroker {
|
|
|
12478
12889
|
trimRtpRingToPreRollWindow() {
|
|
12479
12890
|
if (this.rtpRingKeyframeMarks.length === 0 || this.rtpRing.length === 0) return;
|
|
12480
12891
|
const newestTs = this.rtpRing[this.rtpRing.length - 1].readUInt32BE(4);
|
|
12481
|
-
|
|
12892
|
+
const marks = this.rtpPreRollMarks(newestTs);
|
|
12893
|
+
const retentionMs = this.getPreRollRetentionMs();
|
|
12894
|
+
let cut = selectPreRollStart(marks, retentionMs).startIndex;
|
|
12482
12895
|
if (cut < 0) return;
|
|
12483
|
-
if (
|
|
12896
|
+
if (retentionMs > 0) {
|
|
12897
|
+
const budget = this.getPreRollBudget();
|
|
12898
|
+
this.reportPreRollClamp(budget, retentionMs);
|
|
12484
12899
|
const historyEnd = this.rtpRingKeyframeMarks[this.rtpRingKeyframeMarks.length - 1].index;
|
|
12485
12900
|
let retained = 0;
|
|
12486
12901
|
for (let i = cut; i < historyEnd; i++) retained += this.rtpRing[i].length;
|
|
12487
12902
|
let markIdx = this.rtpRingKeyframeMarks.findIndex((m) => m.index === cut);
|
|
12488
|
-
while (retained >
|
|
12903
|
+
while (retained > budget.bytes && markIdx >= 0 && markIdx < this.rtpRingKeyframeMarks.length - 1) {
|
|
12489
12904
|
const nextCut = this.rtpRingKeyframeMarks[markIdx + 1].index;
|
|
12490
12905
|
for (let i = cut; i < nextCut; i++) retained -= this.rtpRing[i].length;
|
|
12491
12906
|
cut = nextCut;
|
|
@@ -12522,13 +12937,37 @@ var StreamBroker = class StreamBroker {
|
|
|
12522
12937
|
* when motion fired retained nothing, and the honest outcome there is the
|
|
12523
12938
|
* Stage 0 burst rather than a pretend one.
|
|
12524
12939
|
*/
|
|
12525
|
-
getRtpPreRollBuffer() {
|
|
12940
|
+
getRtpPreRollBuffer(windowMs) {
|
|
12526
12941
|
if (!this.rtpRingHasKeyframe) return [];
|
|
12527
|
-
if (
|
|
12942
|
+
if (windowMs <= 0 || this.rtpRing.length === 0) return this.getRtpPreBuffer();
|
|
12528
12943
|
const newestTs = this.rtpRing[this.rtpRing.length - 1].readUInt32BE(4);
|
|
12529
|
-
const selection = selectPreRollStart(this.rtpPreRollMarks(newestTs),
|
|
12944
|
+
const selection = selectPreRollStart(this.rtpPreRollMarks(newestTs), windowMs);
|
|
12530
12945
|
return selection.startIndex < 0 ? [] : this.rtpRing.slice(selection.startIndex);
|
|
12531
12946
|
}
|
|
12947
|
+
/**
|
|
12948
|
+
* Say it, ONCE per transition, when the absolute ceiling truncates the window
|
|
12949
|
+
* the operator configured.
|
|
12950
|
+
*
|
|
12951
|
+
* A clamp nobody prints is indistinguishable from a camera that was simply
|
|
12952
|
+
* not warm long enough, and the two call for opposite actions (buy the RAM
|
|
12953
|
+
* back vs wait). The flat 8 MiB cap this replaced clamped SILENTLY, which is
|
|
12954
|
+
* how "10 s" could mean 6 s on a 4K stream for as long as it did.
|
|
12955
|
+
*/
|
|
12956
|
+
reportPreRollClamp(budget, retentionMs) {
|
|
12957
|
+
if (budget.clamped === this.preRollClampLogged) return;
|
|
12958
|
+
this.preRollClampLogged = budget.clamped;
|
|
12959
|
+
if (!budget.clamped) return;
|
|
12960
|
+
this.logger?.warn("pre-roll budget clamped — the window will carry less than configured", {
|
|
12961
|
+
tags: { deviceId: this.numericDeviceId },
|
|
12962
|
+
meta: {
|
|
12963
|
+
retentionMs,
|
|
12964
|
+
detectionPreRollMs: this.detectionPreRollMs,
|
|
12965
|
+
recordingPreRollMs: this.recordingPreRollMs,
|
|
12966
|
+
bitrateKbps: this.bitrateKbps,
|
|
12967
|
+
budgetBytes: budget.bytes
|
|
12968
|
+
}
|
|
12969
|
+
});
|
|
12970
|
+
}
|
|
12532
12971
|
getSourceType() {
|
|
12533
12972
|
return this.source?.type ?? null;
|
|
12534
12973
|
}
|
|
@@ -12635,7 +13074,9 @@ var StreamBroker = class StreamBroker {
|
|
|
12635
13074
|
getFirstVideoWatchdogStatus() {
|
|
12636
13075
|
return {
|
|
12637
13076
|
armed: this.firstVideoWatchdog !== void 0,
|
|
12638
|
-
videoRtpSeen: this.videoRtpSeen
|
|
13077
|
+
videoRtpSeen: this.videoRtpSeen,
|
|
13078
|
+
timeoutMs: this.firstVideoTimeoutMs(),
|
|
13079
|
+
materializeRequestedAt: this.materializeRequestedAt
|
|
12639
13080
|
};
|
|
12640
13081
|
}
|
|
12641
13082
|
/**
|
|
@@ -13098,6 +13539,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13098
13539
|
this.nextDialAllowedAt = 0;
|
|
13099
13540
|
this.stopErrorPlaceholder();
|
|
13100
13541
|
this.cancelFirstVideoWatchdog();
|
|
13542
|
+
this.materializeRequestedAt = 0;
|
|
13101
13543
|
this.kickRtpActivityWatchdog();
|
|
13102
13544
|
this.videoRtpSeen++;
|
|
13103
13545
|
if (this.videoRtpSeen === 1 || this.videoRtpSeen === 50) this.logger?.info("video: rtp packet received", { meta: {
|
|
@@ -13424,10 +13866,12 @@ var StreamBroker = class StreamBroker {
|
|
|
13424
13866
|
armFirstVideoWatchdog() {
|
|
13425
13867
|
if (this.firstVideoWatchdog) return;
|
|
13426
13868
|
if (this.manualStop || this.stopping) return;
|
|
13869
|
+
const timeoutMs = this.firstVideoTimeoutMs();
|
|
13427
13870
|
this.firstVideoWatchdog = setTimeout(() => {
|
|
13428
13871
|
this.firstVideoWatchdog = void 0;
|
|
13429
13872
|
if (this.manualStop || this.stopping) return;
|
|
13430
13873
|
if (this.videoRtpSeen > 0) {
|
|
13874
|
+
this.materializeRequestedAt = 0;
|
|
13431
13875
|
this.logger?.info("first-video watchdog: video flowing — standing down", {
|
|
13432
13876
|
tags: { deviceId: this.numericDeviceId },
|
|
13433
13877
|
meta: {
|
|
@@ -13440,7 +13884,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13440
13884
|
this.logger?.warn("first-video watchdog: no video RTP after source connect — forcing reconnect", {
|
|
13441
13885
|
tags: { deviceId: this.numericDeviceId },
|
|
13442
13886
|
meta: {
|
|
13443
|
-
timeoutMs
|
|
13887
|
+
timeoutMs,
|
|
13444
13888
|
...this.sourceMetaForLog()
|
|
13445
13889
|
}
|
|
13446
13890
|
});
|
|
@@ -13451,7 +13895,19 @@ var StreamBroker = class StreamBroker {
|
|
|
13451
13895
|
this.lastRefreshRequestAt = 0;
|
|
13452
13896
|
if (this.source?.type === "rfc4571") this.notifySourceRefresh();
|
|
13453
13897
|
this.scheduleReconnect();
|
|
13454
|
-
},
|
|
13898
|
+
}, timeoutMs);
|
|
13899
|
+
}
|
|
13900
|
+
/**
|
|
13901
|
+
* The first-video deadline for the dial being armed.
|
|
13902
|
+
*
|
|
13903
|
+
* Extended for a dial that FOLLOWED a lazy materialize we asked for — see
|
|
13904
|
+
* {@link LAZY_MATERIALIZE_FIRST_VIDEO_TIMEOUT_MS}. It reads the SAME fact the
|
|
13905
|
+
* dial loop uses (`materializeRequestedAt`), so a cold open cannot be "in
|
|
13906
|
+
* progress" for one and "not happening" for the other.
|
|
13907
|
+
*/
|
|
13908
|
+
firstVideoTimeoutMs() {
|
|
13909
|
+
if (this.materializeRequestedAt === 0) return StreamBroker.FIRST_VIDEO_TIMEOUT_MS;
|
|
13910
|
+
return Date.now() - this.materializeRequestedAt < StreamBroker.MATERIALIZE_RECENT_MS ? StreamBroker.LAZY_MATERIALIZE_FIRST_VIDEO_TIMEOUT_MS : StreamBroker.FIRST_VIDEO_TIMEOUT_MS;
|
|
13455
13911
|
}
|
|
13456
13912
|
/** Cancel the first-video watchdog. Idempotent. */
|
|
13457
13913
|
cancelFirstVideoWatchdog() {
|
|
@@ -13656,12 +14112,29 @@ var StreamBroker = class StreamBroker {
|
|
|
13656
14112
|
startRfc4571Reader(source) {
|
|
13657
14113
|
this.logger?.info("starting RFC 4571 reader");
|
|
13658
14114
|
if (typeof source.url === "string" && source.url.startsWith("lazy:rfc4571:")) {
|
|
13659
|
-
|
|
14115
|
+
const nowMs = Date.now();
|
|
14116
|
+
if (this.materializeRequestedAt === 0) this.materializeRequestedAt = nowMs;
|
|
14117
|
+
const waitedMs = nowMs - this.materializeRequestedAt;
|
|
14118
|
+
const withinDeadline = waitedMs < StreamBroker.MATERIALIZE_WAIT_MS;
|
|
14119
|
+
if (withinDeadline) this.reconnectDelayMs = StreamBroker.MATERIALIZE_POLL_MS;
|
|
14120
|
+
this.logger?.info("rfc4571: lazy URL — requesting publisher to materialize upstream", {
|
|
14121
|
+
tags: { deviceId: this.numericDeviceId },
|
|
14122
|
+
meta: {
|
|
14123
|
+
url: source.url,
|
|
14124
|
+
waitedMs,
|
|
14125
|
+
schedule: withinDeadline ? "materialize-poll" : "backoff",
|
|
14126
|
+
nextDelayMs: this.reconnectDelayMs
|
|
14127
|
+
}
|
|
14128
|
+
});
|
|
13660
14129
|
this._status = "error";
|
|
13661
14130
|
this.notifySourceRefresh();
|
|
13662
14131
|
this.scheduleReconnect();
|
|
13663
14132
|
return;
|
|
13664
14133
|
}
|
|
14134
|
+
if (this.materializeRequestedAt > 0) this.logger?.info("rfc4571: publisher materialized — dialling the real upstream", {
|
|
14135
|
+
tags: { deviceId: this.numericDeviceId },
|
|
14136
|
+
meta: { materializeMs: Date.now() - this.materializeRequestedAt }
|
|
14137
|
+
});
|
|
13665
14138
|
const sdp = typeof source.metadata?.["sdp"] === "string" ? source.metadata["sdp"] : null;
|
|
13666
14139
|
if (!sdp) {
|
|
13667
14140
|
this.logger?.warn("rfc4571: source missing metadata.sdp — requesting publisher refresh");
|
|
@@ -14663,6 +15136,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
14663
15136
|
* bytes on the ingest node, so it is turned on per camera that needs it.
|
|
14664
15137
|
*/
|
|
14665
15138
|
defaultPreRollMs = 0;
|
|
15139
|
+
/**
|
|
15140
|
+
* Cluster-wide default RECORDING pre-roll (ms). `undefined` until the addon
|
|
15141
|
+
* pushes its config — which is a different fact from `0` and must stay so:
|
|
15142
|
+
* `0` is an operator turning the feature OFF, `undefined` is a boot that has
|
|
15143
|
+
* not read the setting yet and takes `DEFAULT_RECORDING_PRE_ROLL_MS`.
|
|
15144
|
+
*/
|
|
15145
|
+
defaultRecordingPreRollMs = void 0;
|
|
14666
15146
|
webrtcServer = null;
|
|
14667
15147
|
/**
|
|
14668
15148
|
* brokerId → the remote-encoded adapter backing a non-owned camera's WebRTC
|
|
@@ -15307,13 +15787,32 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15307
15787
|
* decision); an explicit per-stream override still reaches it.
|
|
15308
15788
|
*/
|
|
15309
15789
|
applyPreRoll(broker, deviceId, camStreamId) {
|
|
15310
|
-
|
|
15790
|
+
const terminalRelay = this.cameraStreams.get(deviceId)?.get(camStreamId)?.kind === "pull-http";
|
|
15791
|
+
broker.setDetectionPreRollMs(resolvePreRollMs({
|
|
15311
15792
|
camStreamId,
|
|
15312
15793
|
clusterMs: this.defaultPreRollMs,
|
|
15313
15794
|
overrides: this.deviceOverrides.get(deviceId)?.preRollMs,
|
|
15314
|
-
terminalRelay
|
|
15795
|
+
terminalRelay
|
|
15796
|
+
}));
|
|
15797
|
+
broker.setRecordingPreRollMs(resolveRecordingPreRollMs({
|
|
15798
|
+
camStreamId,
|
|
15799
|
+
clusterMs: this.defaultRecordingPreRollMs,
|
|
15800
|
+
terminalRelay,
|
|
15801
|
+
batteryOperated: this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated)
|
|
15315
15802
|
}));
|
|
15316
15803
|
}
|
|
15804
|
+
/**
|
|
15805
|
+
* Set the cluster-wide default RECORDING pre-roll and push it into every live
|
|
15806
|
+
* broker — same reasoning as detection's: a broker outlives its setting.
|
|
15807
|
+
*/
|
|
15808
|
+
setDefaultRecordingPreRollMs(ms) {
|
|
15809
|
+
this.defaultRecordingPreRollMs = ms;
|
|
15810
|
+
for (const [brokerId, broker] of this.brokers) {
|
|
15811
|
+
const parsed = parseBrokerId(brokerId);
|
|
15812
|
+
if (!parsed) continue;
|
|
15813
|
+
this.applyPreRoll(broker, parsed.deviceId, parsed.camStreamId);
|
|
15814
|
+
}
|
|
15815
|
+
}
|
|
15317
15816
|
setEventBus(bus) {
|
|
15318
15817
|
this.eventBus = bus;
|
|
15319
15818
|
this.startStreamHealthWatchdog();
|
|
@@ -15901,7 +16400,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15901
16400
|
return { success: true };
|
|
15902
16401
|
}
|
|
15903
16402
|
const existing = this.upsertSourceCameraStream(deviceId, input);
|
|
15904
|
-
|
|
16403
|
+
existing !== void 0 && input.url !== void 0 && (existing.url, input.url);
|
|
15905
16404
|
const wasAlreadyAssigned = (PUSH_KINDS.has(kind) ? findProfileForStream(this.assignments.get(deviceId)?.map ?? {}, camStreamId) : null) !== null && existing !== void 0;
|
|
15906
16405
|
await this.reconcileAutoAssignment(deviceId, "publish", existing === void 0);
|
|
15907
16406
|
if (wasAlreadyAssigned) {
|
|
@@ -15909,7 +16408,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15909
16408
|
if (finalProfile !== null) this.emitCamStreamDemand(deviceId, camStreamId, finalProfile);
|
|
15910
16409
|
}
|
|
15911
16410
|
await this.ensureBroker(deviceId, camStreamId);
|
|
15912
|
-
|
|
16411
|
+
this.brokers.get(brokerIdFor(deviceId, camStreamId))?.kickReconnect();
|
|
15913
16412
|
const justRegisteredBrokerId = brokerIdFor(deviceId, camStreamId);
|
|
15914
16413
|
const pending = this.pendingDerivedStreams.get(justRegisteredBrokerId);
|
|
15915
16414
|
if (pending) {
|
|
@@ -17770,7 +18269,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17770
18269
|
else delete pr[camStreamId];
|
|
17771
18270
|
nextOverride.preRollMs = pr;
|
|
17772
18271
|
const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
|
|
17773
|
-
if (broker) broker.
|
|
18272
|
+
if (broker) broker.setDetectionPreRollMs(resolvePreRollMs({
|
|
17774
18273
|
camStreamId,
|
|
17775
18274
|
clusterMs: this.defaultPreRollMs,
|
|
17776
18275
|
overrides: pr,
|
|
@@ -18423,7 +18922,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18423
18922
|
const cam = this.cameraStreams.get(deviceId)?.get(camStreamId);
|
|
18424
18923
|
if (!cam) return;
|
|
18425
18924
|
const brokerId = brokerIdFor(deviceId, camStreamId);
|
|
18426
|
-
|
|
18925
|
+
const live = this.brokers.get(brokerId);
|
|
18926
|
+
if (live) {
|
|
18927
|
+
this.applyPreRoll(live, deviceId, camStreamId);
|
|
18928
|
+
return;
|
|
18929
|
+
}
|
|
18427
18930
|
const existingEnsure = this.brokerEnsures.get(brokerId);
|
|
18428
18931
|
if (existingEnsure) {
|
|
18429
18932
|
await existingEnsure;
|
|
@@ -35301,6 +35804,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
35301
35804
|
catalogReconcileIntervalSec: 30,
|
|
35302
35805
|
joinBurstMaxSpanMs: DEFAULT_PRIME_BURST_BOUND.maxSpanMs,
|
|
35303
35806
|
preRollMs: 0,
|
|
35807
|
+
recordingPreRollMs: DEFAULT_RECORDING_PRE_ROLL_MS,
|
|
35304
35808
|
binaryPath: "",
|
|
35305
35809
|
hwaccel: "copy"
|
|
35306
35810
|
});
|
|
@@ -35360,6 +35864,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
35360
35864
|
this.brokerManager.setDefaultPreBufferSec(this.config.defaultPreBufferSec);
|
|
35361
35865
|
this.brokerManager.setDefaultJoinBurstMaxSpanMs(this.config.joinBurstMaxSpanMs);
|
|
35362
35866
|
this.brokerManager.setDefaultPreRollMs(this.config.preRollMs);
|
|
35867
|
+
this.brokerManager.setDefaultRecordingPreRollMs(this.config.recordingPreRollMs);
|
|
35363
35868
|
this.brokerManager.setEventBus(this.ctx.eventBus);
|
|
35364
35869
|
const localNode = this.ctx.kernel.localNodeId;
|
|
35365
35870
|
const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
|
|
@@ -35942,6 +36447,17 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
35942
36447
|
default: 0,
|
|
35943
36448
|
unit: "ms"
|
|
35944
36449
|
},
|
|
36450
|
+
{
|
|
36451
|
+
type: "number",
|
|
36452
|
+
key: "recordingPreRollMs",
|
|
36453
|
+
label: "Recording pre-roll",
|
|
36454
|
+
description: "Cluster-wide default for how much video from BEFORE the trigger an event recording begins with. The recorder’s ffmpeg is spawned AT the trigger, so without this its first frame is at best the live edge — measured on one camera, the footage started after the motion in 60 % of episodes. It spends retained video in memory on whichever node ingests the camera and can only reach as far back as that stream has been warm; it never dials a camera on its own. Battery cameras and terminal (MJPEG relay) cameras take 0. Set 0 to turn it off.",
|
|
36455
|
+
min: 0,
|
|
36456
|
+
max: RECORDING_PRE_ROLL_MAX_MS,
|
|
36457
|
+
step: 500,
|
|
36458
|
+
default: DEFAULT_RECORDING_PRE_ROLL_MS,
|
|
36459
|
+
unit: "ms"
|
|
36460
|
+
},
|
|
35945
36461
|
{
|
|
35946
36462
|
type: "number",
|
|
35947
36463
|
key: "catalogReconcileIntervalSec",
|