@camstack/addon-pipeline 1.2.197 → 1.2.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-CM-8U0Bb.js → default-detection-model-B5v8qEI0.js} +1 -1
- package/dist/{default-detection-model-CplzzTmN.mjs → default-detection-model-CASpAESz.mjs} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-CBa1j3On.js → dist-BxP0SvPz.js} +74 -23
- package/dist/{dist-Y4BsF_EC.mjs → dist-oFIHnD88.mjs} +69 -24
- package/dist/{lazy-sharp-CS5WE9mI.js → lazy-sharp-VKOG3WW4.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-B5ouEQYN.mjs → node-B4zETsJ9.mjs} +1 -1
- package/dist/{node-C-B8VuGi.js → node-BIKrNEtS.js} +1 -1
- package/dist/pipeline-runner/index.js +5 -5
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{retire-root-keys-Csb8z2f3.js → prebuffer-DmBYRQWg.mjs} +64 -12
- package/dist/{retire-root-keys-DZvlso2l.mjs → prebuffer-DqdeLSsk.js} +99 -1
- package/dist/{process-memory-CmeKr_MZ.js → process-memory-CMHOmj75.js} +1 -1
- package/dist/{process-memory-DeO2OZ3-.mjs → process-memory-D9xYwm6L.mjs} +1 -1
- package/dist/recorder/index.js +26 -18
- package/dist/recorder/index.mjs +22 -14
- package/dist/{segment-demux-js-DiIsywGz.js → segment-demux-js-DLwL1vmq.js} +1 -1
- package/dist/{segment-demux-js-DXPgq2cf.mjs → segment-demux-js-DZysrgdC.mjs} +1 -1
- 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 +28 -31
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Q-6HzWJ0.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2eTdxvd.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cv5McbTI.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BcjLrxhF.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-C-2Eo_nr.mjs → hostInit-C8tu7NS4.mjs} +3 -3
- package/dist/stream-broker/index.js +452 -541
- package/dist/stream-broker/index.mjs +448 -537
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CXsdriRX.js → worker-protocol-D2mMjUXv.js} +1 -1
- package/dist/{worker-protocol-_7zrToTo.mjs → worker-protocol-DFlZm_wr.mjs} +1 -1
- package/package.json +1 -1
|
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
5
|
+
const require_dist = require("../dist-BxP0SvPz.js");
|
|
6
|
+
const require_node = require("../node-BIKrNEtS.js");
|
|
7
7
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
8
8
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
9
|
-
const
|
|
10
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
9
|
+
const require_prebuffer = require("../prebuffer-DqdeLSsk.js");
|
|
10
|
+
const require_segment_demux_js = require("../segment-demux-js-DLwL1vmq.js");
|
|
11
11
|
let node_os = require("node:os");
|
|
12
12
|
node_os = require_dist.__toESM(node_os);
|
|
13
13
|
let node_crypto = require("node:crypto");
|
|
@@ -370,7 +370,7 @@ var BROKER_PROBE_SNAPSHOTS_SPEC = {
|
|
|
370
370
|
loadLimit: BROKER_ROWS_LIMIT
|
|
371
371
|
};
|
|
372
372
|
function probeSnapshotsStore(store, logger) {
|
|
373
|
-
return new
|
|
373
|
+
return new require_prebuffer.RowMapStore({
|
|
374
374
|
spec: BROKER_PROBE_SNAPSHOTS_SPEC,
|
|
375
375
|
store,
|
|
376
376
|
logger
|
|
@@ -412,7 +412,7 @@ var BROKER_RTSP_TOKENS_SPEC = {
|
|
|
412
412
|
loadLimit: BROKER_ROWS_LIMIT
|
|
413
413
|
};
|
|
414
414
|
function rtspTokensStore(store, logger) {
|
|
415
|
-
return new
|
|
415
|
+
return new require_prebuffer.RowMapStore({
|
|
416
416
|
spec: BROKER_RTSP_TOKENS_SPEC,
|
|
417
417
|
store,
|
|
418
418
|
logger
|
|
@@ -453,7 +453,7 @@ var BROKER_PROFILE_MAP_SPEC = {
|
|
|
453
453
|
loadLimit: BROKER_ROWS_LIMIT
|
|
454
454
|
};
|
|
455
455
|
function profileMapStore(store, logger) {
|
|
456
|
-
return new
|
|
456
|
+
return new require_prebuffer.RowMapStore({
|
|
457
457
|
spec: BROKER_PROFILE_MAP_SPEC,
|
|
458
458
|
store,
|
|
459
459
|
logger
|
|
@@ -1785,247 +1785,66 @@ function rankInitialAssignment(streams) {
|
|
|
1785
1785
|
...byRank
|
|
1786
1786
|
};
|
|
1787
1787
|
}
|
|
1788
|
+
//#endregion
|
|
1789
|
+
//#region src/stream-broker/prebuffer-migration.ts
|
|
1788
1790
|
/**
|
|
1789
|
-
*
|
|
1791
|
+
* MIGRATION of the retired per-stream pre-roll override onto the one prebuffer.
|
|
1790
1792
|
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
* both planes can honour the same maximum.
|
|
1797
|
-
*/
|
|
1798
|
-
var PRE_ROLL_MAX_MS = 6e3;
|
|
1799
|
-
var NOTHING = {
|
|
1800
|
-
startIndex: -1,
|
|
1801
|
-
ageMs: 0,
|
|
1802
|
-
gopsBack: 0
|
|
1803
|
-
};
|
|
1804
|
-
/**
|
|
1805
|
-
* A configured pre-roll in ms, or `0` when the input carries no instruction.
|
|
1793
|
+
* `DeviceOverride.preRollMs` was the per-camera-stream DETECTION pre-roll: how
|
|
1794
|
+
* much history a decode dial could be handed. D381 retires it — there is one
|
|
1795
|
+
* retention now and it is the prebuffer's — but a key that is merely removed
|
|
1796
|
+
* from a non-strict `z.object` is **stripped in silence** on the next save,
|
|
1797
|
+
* which is the failure D380 came within one commit of shipping.
|
|
1806
1798
|
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1799
|
+
* So the key is retired EXPLICITLY: read once, converted into the
|
|
1800
|
+
* configuration that now carries its meaning, and cleared. An operator who had
|
|
1801
|
+
* typed "3 s of history on 617's substream" keeps 3 s of history on 617's
|
|
1802
|
+
* substream; they do not silently get the cluster default, and they do not
|
|
1803
|
+
* silently get nothing.
|
|
1811
1804
|
*/
|
|
1812
|
-
function clampPreRoll(value, maxMs) {
|
|
1813
|
-
if (value === void 0) return 0;
|
|
1814
|
-
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
1815
|
-
return Math.min(maxMs, Math.max(250, value));
|
|
1816
|
-
}
|
|
1817
1805
|
/**
|
|
1818
|
-
*
|
|
1819
|
-
* carry.
|
|
1806
|
+
* Fold a retired `preRollMs` map into the prebuffer overrides.
|
|
1820
1807
|
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
* every recording burst the moment there were two: the selector would refuse
|
|
1826
|
-
* the keyframe the recorder asked for, and the span bound would withhold the
|
|
1827
|
-
* burst for being as wide as the operator configured — D192's failure, one
|
|
1828
|
-
* layer down.
|
|
1829
|
-
*/
|
|
1830
|
-
function usablePreRoll(value) {
|
|
1831
|
-
return clampPreRoll(value, RECORDING_PRE_ROLL_MAX_MS);
|
|
1832
|
-
}
|
|
1833
|
-
/**
|
|
1834
|
-
* Choose the keyframe access unit a pre-roll burst starts at.
|
|
1835
|
-
*
|
|
1836
|
-
* The OLDEST mark that is still inside the window wins; when none is (the
|
|
1837
|
-
* current GOP is itself older than `preRollMs`, i.e. a long-GOP camera with a
|
|
1838
|
-
* short pre-roll) the newest mark is returned, because the alternative to the
|
|
1839
|
-
* current GOP is nothing at all — the blind start Stage 0 exists to prevent.
|
|
1808
|
+
* An EXPLICIT prebuffer override wins and the retired value is dropped: the
|
|
1809
|
+
* operator answered the same question later and more directly, and a migration
|
|
1810
|
+
* that overwrote it would be this subsystem's original sin one more time — two
|
|
1811
|
+
* writers for one number.
|
|
1840
1812
|
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
* Scans instead of assuming order: a caller that appends marks and rebases
|
|
1847
|
-
* indices after a trim is one refactor away from handing this an unsorted
|
|
1848
|
-
* array, and the failure would be a silently wider window.
|
|
1813
|
+
* Rounds UP to whole seconds. The prebuffer is configured in seconds and the
|
|
1814
|
+
* retired knob in milliseconds; rounding down would hand back a window
|
|
1815
|
+
* narrower than the one that was running, which is the direction that loses
|
|
1816
|
+
* media.
|
|
1849
1817
|
*/
|
|
1850
|
-
function
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
if (mark.ageMs < newest.ageMs) newest = mark;
|
|
1857
|
-
if (window > 0 && mark.ageMs <= window && (chosen === null || mark.ageMs > chosen.ageMs)) chosen = mark;
|
|
1858
|
-
}
|
|
1859
|
-
const pick = chosen ?? newest;
|
|
1860
|
-
let gopsBack = 0;
|
|
1861
|
-
for (const mark of marks) if (mark.ageMs < pick.ageMs) gopsBack++;
|
|
1862
|
-
return {
|
|
1863
|
-
startIndex: pick.index,
|
|
1864
|
-
ageMs: pick.ageMs,
|
|
1865
|
-
gopsBack
|
|
1818
|
+
function migrateRetiredPreRollOverride(input) {
|
|
1819
|
+
const retired = input.preRollMs;
|
|
1820
|
+
if (retired === void 0 || Object.keys(retired).length === 0) return {
|
|
1821
|
+
override: input,
|
|
1822
|
+
migrated: [],
|
|
1823
|
+
superseded: []
|
|
1866
1824
|
};
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
* Shipped RECORDING pre-roll on a mains camera. Operator decision, 2026-08-20.
|
|
1884
|
-
*
|
|
1885
|
-
* Unlike detection's, this one ships ON. The audit of the same day measured the
|
|
1886
|
-
* reason: on camera 1438 the footage began AFTER the motion in 60 % of
|
|
1887
|
-
* episodes, because the writer is spawned at the trigger and ffmpeg's first
|
|
1888
|
-
* frame is at best the live edge. A recording feature whose media starts after
|
|
1889
|
-
* the event is not a tuning question.
|
|
1890
|
-
*/
|
|
1891
|
-
var DEFAULT_RECORDING_PRE_ROLL_MS = 1e4;
|
|
1892
|
-
/**
|
|
1893
|
-
* Budget when the broker has not measured a bitrate for this stream yet — the
|
|
1894
|
-
* historical flat cap, kept verbatim as the honest "we do not know".
|
|
1895
|
-
*
|
|
1896
|
-
* `bitrateKbps` is 0 for the first stats window after every dial and after
|
|
1897
|
-
* every reset, so this is the value in force exactly while the ring is filling
|
|
1898
|
-
* for the first time.
|
|
1899
|
-
*/
|
|
1900
|
-
var PRE_ROLL_UNKNOWN_BITRATE_BYTES = 8 * 1024 * 1024;
|
|
1901
|
-
/**
|
|
1902
|
-
* Hard ceiling on the WHOLE source-RTP ring (history + the current GOP).
|
|
1903
|
-
*
|
|
1904
|
-
* Lives here, next to the pre-roll budget, because the two numbers are one
|
|
1905
|
-
* number: a history budget above the ring cap is not a wider window, it is a
|
|
1906
|
-
* ring that overflows and calls `resetRtpPreBuffer`, which drops the join
|
|
1907
|
-
* bootstrap of EVERY consumer — WebRTC viewers included — for as long as the
|
|
1908
|
-
* condition holds. Keeping them in separate files is how they came to disagree.
|
|
1909
|
-
* Read by `StreamBroker.captureRtpForPreBuffer`.
|
|
1910
|
-
*/
|
|
1911
|
-
var RTP_RING_MAX_BYTES = 24 * 1024 * 1024;
|
|
1912
|
-
/**
|
|
1913
|
-
* Absolute per-stream ceiling on retained pre-roll HISTORY.
|
|
1914
|
-
*
|
|
1915
|
-
* The bitrate-derived budget is what the window COSTS; this is the most any one
|
|
1916
|
-
* stream may spend regardless. Past it the operator has configured a window this
|
|
1917
|
-
* node cannot afford on that camera, and the broker says so rather than
|
|
1918
|
-
* delivering silently less (D192).
|
|
1919
|
-
*
|
|
1920
|
-
* **DERIVED, not chosen.** It was a free-standing `32 * 1024 * 1024` — 8 MiB
|
|
1921
|
-
* ABOVE the ring it has to fit inside. A ≳13 Mbps stream at a 10 s window was
|
|
1922
|
-
* therefore authorised to overflow its own ring and cycle `resetRtpPreBuffer`:
|
|
1923
|
-
* the pre-roll knob, turned up, would have taken every viewer's join bootstrap
|
|
1924
|
-
* with it. Deriving it from {@link RTP_RING_MAX_BYTES} minus the current-GOP
|
|
1925
|
-
* reserve makes the inversion unrepresentable rather than merely fixed.
|
|
1926
|
-
*/
|
|
1927
|
-
var PRE_ROLL_ABSOLUTE_MAX_BYTES = RTP_RING_MAX_BYTES - 8 * 1024 * 1024;
|
|
1928
|
-
/**
|
|
1929
|
-
* Headroom over the OBSERVED mean bitrate.
|
|
1930
|
-
*
|
|
1931
|
-
* `BrokerStatsSchema.bitrateKbps` is a mean over the stats window; a GOP
|
|
1932
|
-
* carrying an IDR plus a scene change is well above it. Budgeting at exactly
|
|
1933
|
-
* the mean would drop the oldest GOP on every busy second, so the window would
|
|
1934
|
-
* be the one the operator asked for only while nothing moved — which is the
|
|
1935
|
-
* opposite of when it is needed.
|
|
1936
|
-
*/
|
|
1937
|
-
var PRE_ROLL_BITRATE_HEADROOM = 1.5;
|
|
1938
|
-
/**
|
|
1939
|
-
* The byte budget for the history a `windowMs` pre-roll needs on a stream
|
|
1940
|
-
* observed at `observedKbps`.
|
|
1941
|
-
*
|
|
1942
|
-
* Replaces a FLAT 8 MiB cap. That constant was a promise the feature could not
|
|
1943
|
-
* keep in either direction: above ~6.5 Mbps it silently truncated the window
|
|
1944
|
-
* (a 4K main stream asked for 10 s and got 6), and below it, it authorised a
|
|
1945
|
-
* doorbell at 512 kbps to hold 8 MiB — 16x what its own window weighs.
|
|
1946
|
-
*/
|
|
1947
|
-
function preRollBudgetBytes(windowMs, observedKbps) {
|
|
1948
|
-
if (!Number.isFinite(windowMs) || windowMs <= 0) return {
|
|
1949
|
-
bytes: 0,
|
|
1950
|
-
clamped: false
|
|
1951
|
-
};
|
|
1952
|
-
if (!Number.isFinite(observedKbps) || observedKbps <= 0) return {
|
|
1953
|
-
bytes: PRE_ROLL_UNKNOWN_BITRATE_BYTES,
|
|
1954
|
-
clamped: false
|
|
1955
|
-
};
|
|
1956
|
-
const raw = Math.round(windowMs * observedKbps * PRE_ROLL_BITRATE_HEADROOM / 8);
|
|
1957
|
-
if (raw > 16777216) return {
|
|
1958
|
-
bytes: PRE_ROLL_ABSOLUTE_MAX_BYTES,
|
|
1959
|
-
clamped: true
|
|
1960
|
-
};
|
|
1961
|
-
return {
|
|
1962
|
-
bytes: raw,
|
|
1963
|
-
clamped: false
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
/**
|
|
1967
|
-
* The RECORDING window a broker retains and serves to a `/recording` dial.
|
|
1968
|
-
*
|
|
1969
|
-
* Precedence: stream override -> battery / terminal relay (0) -> cluster
|
|
1970
|
-
* default -> {@link DEFAULT_RECORDING_PRE_ROLL_MS}. An override wins over the
|
|
1971
|
-
* battery rule on purpose: the rule is a DEFAULT about where retention is
|
|
1972
|
-
* wasted, and an operator who typed a number for that stream has answered the
|
|
1973
|
-
* question the default was guessing at.
|
|
1974
|
-
*/
|
|
1975
|
-
function resolveRecordingPreRollMs(input) {
|
|
1976
|
-
const override = clampPreRoll(input.overrides?.[input.camStreamId], RECORDING_PRE_ROLL_MAX_MS);
|
|
1977
|
-
if (override > 0) return override;
|
|
1978
|
-
if (input.batteryOperated === true) return 0;
|
|
1979
|
-
if (input.terminalRelay === true) return 0;
|
|
1980
|
-
if (input.clusterMs === void 0) return DEFAULT_RECORDING_PRE_ROLL_MS;
|
|
1981
|
-
return clampPreRoll(input.clusterMs, RECORDING_PRE_ROLL_MAX_MS);
|
|
1982
|
-
}
|
|
1983
|
-
/**
|
|
1984
|
-
* Combine the cluster default and the per-camera-stream override into the one
|
|
1985
|
-
* pre-roll a broker applies. Precedence: stream override → cluster default →
|
|
1986
|
-
* OFF. Each layer is validated independently, so a bad override falls back to a
|
|
1987
|
-
* good cluster value rather than to nothing.
|
|
1988
|
-
*
|
|
1989
|
-
* `0` means INHERIT, never "off for this stream". It is what a cleared number
|
|
1990
|
-
* input posts, and the shipped cluster default is 0, so inherit and off
|
|
1991
|
-
* coincide on every unconfigured stream.
|
|
1992
|
-
*/
|
|
1993
|
-
function resolvePreRollMs(input) {
|
|
1994
|
-
const override = clampPreRoll(input.overrides?.[input.camStreamId], PRE_ROLL_MAX_MS);
|
|
1995
|
-
if (override > 0) return override;
|
|
1996
|
-
if (input.terminalRelay === true) return 0;
|
|
1997
|
-
return clampPreRoll(input.clusterMs, PRE_ROLL_MAX_MS);
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
|
-
* The packet ceiling for a burst carrying `preRollMs` of pre-roll.
|
|
2001
|
-
*
|
|
2002
|
-
* `PRIME_MAX_PACKETS` is a defensive backstop against RTP timestamps that
|
|
2003
|
-
* stopped describing the stream — "far more packets than this much media time
|
|
2004
|
-
* can contain" — not a tuning knob. Read as an absolute it would silently kill
|
|
2005
|
-
* pre-roll on exactly the cameras that need it (a 4K main stream at 8 Mbps
|
|
2006
|
-
* fills 2 000 MTU-sized packets in under 3 s), so it is applied as the RATE it
|
|
2007
|
-
* always was: the same packets-per-span, over the span actually requested.
|
|
2008
|
-
* Never shrinks the ceiling.
|
|
2009
|
-
*/
|
|
2010
|
-
function preRollPacketCeiling(bound, preRollMs) {
|
|
2011
|
-
const window = usablePreRoll(preRollMs);
|
|
2012
|
-
if (window <= 0 || bound.maxSpanMs <= 0) return bound.maxPackets;
|
|
2013
|
-
const factor = Math.max(1, Math.ceil(window / bound.maxSpanMs));
|
|
2014
|
-
return bound.maxPackets * factor;
|
|
2015
|
-
}
|
|
2016
|
-
/**
|
|
2017
|
-
* The span bound a DETECTION session's burst is held to while pre-roll is on.
|
|
2018
|
-
*
|
|
2019
|
-
* D57's bound is untouched for every other consumer; for this one it becomes
|
|
2020
|
-
* the wider of the two, because withholding a burst for being as wide as the
|
|
2021
|
-
* operator configured would make the knob read as ON and behave as OFF.
|
|
2022
|
-
*/
|
|
2023
|
-
function preRollSpanBound(bound, preRollMs) {
|
|
2024
|
-
const window = usablePreRoll(preRollMs);
|
|
2025
|
-
if (window <= 0) return bound;
|
|
1825
|
+
const preBuffer = { ...input.preBuffer };
|
|
1826
|
+
const migrated = [];
|
|
1827
|
+
const superseded = [];
|
|
1828
|
+
for (const [camStreamId, ms] of Object.entries(retired)) {
|
|
1829
|
+
if (!Number.isFinite(ms) || ms <= 0) continue;
|
|
1830
|
+
if (input.preBuffer?.[camStreamId] !== void 0) {
|
|
1831
|
+
superseded.push(camStreamId);
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
preBuffer[camStreamId] = {
|
|
1835
|
+
enabled: true,
|
|
1836
|
+
seconds: Math.min(15, Math.max(1, Math.ceil(ms / 1e3)))
|
|
1837
|
+
};
|
|
1838
|
+
migrated.push(camStreamId);
|
|
1839
|
+
}
|
|
1840
|
+
const { preRollMs: _retired, ...rest } = input;
|
|
2026
1841
|
return {
|
|
2027
|
-
|
|
2028
|
-
|
|
1842
|
+
override: Object.keys(preBuffer).length > 0 ? {
|
|
1843
|
+
...rest,
|
|
1844
|
+
preBuffer
|
|
1845
|
+
} : rest,
|
|
1846
|
+
migrated,
|
|
1847
|
+
superseded
|
|
2029
1848
|
};
|
|
2030
1849
|
}
|
|
2031
1850
|
//#endregion
|
|
@@ -2201,6 +2020,200 @@ var PacedRtpReplay = class {
|
|
|
2201
2020
|
}
|
|
2202
2021
|
};
|
|
2203
2022
|
//#endregion
|
|
2023
|
+
//#region src/stream-broker/pre-roll.ts
|
|
2024
|
+
/**
|
|
2025
|
+
* PRE-ROLL MECHANICS — how a window of retained history is CUT, BUDGETED and
|
|
2026
|
+
* BOUNDED. Not how wide it is: that is the prebuffer's, and the prebuffer's
|
|
2027
|
+
* alone (`prebuffer.ts`, D381).
|
|
2028
|
+
*
|
|
2029
|
+
* WHAT LIVES HERE. Three mechanisms, none of them a setting:
|
|
2030
|
+
*
|
|
2031
|
+
* - {@link selectPreRollStart} — which retained keyframe access unit a burst
|
|
2032
|
+
* starts at. Keyframe-aligned because D57's "the whole GOP or none of it" is
|
|
2033
|
+
* the reason this selects among marks rather than slicing by time.
|
|
2034
|
+
* - {@link preRollBudgetBytes} — what a window of a given width WEIGHS on a
|
|
2035
|
+
* stream of a measured bitrate, and the ring ceiling it has to fit inside.
|
|
2036
|
+
* - {@link preRollSpanBound} / {@link preRollPacketCeiling} — D57's join bound,
|
|
2037
|
+
* widened for the one session that asked for history, with the packet ceiling
|
|
2038
|
+
* scaled by the same factor so the defensive backstop stays a RATE.
|
|
2039
|
+
*
|
|
2040
|
+
* WHAT USED TO LIVE HERE AND DOES NOT. `resolvePreRollMs`,
|
|
2041
|
+
* `resolveRecordingPreRollMs`, `DEFAULT_PRE_ROLL_MS`,
|
|
2042
|
+
* `DEFAULT_RECORDING_PRE_ROLL_MS` and their two ceilings — a whole
|
|
2043
|
+
* CONFIGURATION layer, two windows deep, that decided how much history each
|
|
2044
|
+
* kind of consumer got. It is gone: the prebuffer decides, once, and a consumer
|
|
2045
|
+
* asks for what it holds. The mechanics below are unchanged, because the
|
|
2046
|
+
* failures they encode (an unpaced burst, a flat byte cap, a silently truncated
|
|
2047
|
+
* window) are about cutting history, not about choosing how much to keep.
|
|
2048
|
+
*
|
|
2049
|
+
* WHAT IT STILL CANNOT DO. Spending history is not creating it. A source that
|
|
2050
|
+
* was idle when motion fired has none, the selection falls back to the current
|
|
2051
|
+
* GOP, and the honest outcome is the ordinary join burst.
|
|
2052
|
+
*/
|
|
2053
|
+
var NOTHING = {
|
|
2054
|
+
startIndex: -1,
|
|
2055
|
+
ageMs: 0,
|
|
2056
|
+
gopsBack: 0
|
|
2057
|
+
};
|
|
2058
|
+
/**
|
|
2059
|
+
* Clamp a window ALREADY resolved by the prebuffer to something this module can
|
|
2060
|
+
* cut.
|
|
2061
|
+
*
|
|
2062
|
+
* One clamp for every mechanism below — the selection, the packet ceiling and
|
|
2063
|
+
* the span bound — so a caller cannot get a wider window out of one of them
|
|
2064
|
+
* than out of the others. {@link PREBUFFER_MAX_MS} is the ceiling because the
|
|
2065
|
+
* prebuffer is the only thing that sets a window now; it was
|
|
2066
|
+
* `RECORDING_PRE_ROLL_MAX_MS`, and before that the DETECTION ceiling, which
|
|
2067
|
+
* silently truncated every recording burst to 6 s the moment there were two
|
|
2068
|
+
* windows — D192's failure, one layer down.
|
|
2069
|
+
*/
|
|
2070
|
+
function usablePreRoll(value) {
|
|
2071
|
+
if (value === void 0) return 0;
|
|
2072
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
2073
|
+
return Math.min(require_prebuffer.PREBUFFER_MAX_MS, value);
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Choose the keyframe access unit a pre-roll burst starts at.
|
|
2077
|
+
*
|
|
2078
|
+
* The OLDEST mark that is still inside the window wins; when none is (the
|
|
2079
|
+
* current GOP is itself older than `preRollMs`, i.e. a long-GOP camera with a
|
|
2080
|
+
* short pre-roll) the newest mark is returned, because the alternative to the
|
|
2081
|
+
* current GOP is nothing at all — the blind start Stage 0 exists to prevent.
|
|
2082
|
+
*
|
|
2083
|
+
* The boundary is INCLUSIVE: a keyframe exactly `preRollMs` old is inside.
|
|
2084
|
+
* Everything delivered after a keyframe is younger than it, so starting at a
|
|
2085
|
+
* mark inside the window is what makes "no frame older than `preRollMs` is
|
|
2086
|
+
* delivered" a property of the slice rather than a per-packet filter.
|
|
2087
|
+
*
|
|
2088
|
+
* Scans instead of assuming order: a caller that appends marks and rebases
|
|
2089
|
+
* indices after a trim is one refactor away from handing this an unsorted
|
|
2090
|
+
* array, and the failure would be a silently wider window.
|
|
2091
|
+
*/
|
|
2092
|
+
function selectPreRollStart(marks, preRollMs) {
|
|
2093
|
+
if (marks.length === 0) return NOTHING;
|
|
2094
|
+
const window = usablePreRoll(preRollMs);
|
|
2095
|
+
let newest = marks[0];
|
|
2096
|
+
let chosen = null;
|
|
2097
|
+
for (const mark of marks) {
|
|
2098
|
+
if (mark.ageMs < newest.ageMs) newest = mark;
|
|
2099
|
+
if (window > 0 && mark.ageMs <= window && (chosen === null || mark.ageMs > chosen.ageMs)) chosen = mark;
|
|
2100
|
+
}
|
|
2101
|
+
const pick = chosen ?? newest;
|
|
2102
|
+
let gopsBack = 0;
|
|
2103
|
+
for (const mark of marks) if (mark.ageMs < pick.ageMs) gopsBack++;
|
|
2104
|
+
return {
|
|
2105
|
+
startIndex: pick.index,
|
|
2106
|
+
ageMs: pick.ageMs,
|
|
2107
|
+
gopsBack
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Budget when the broker has not measured a bitrate for this stream yet — the
|
|
2112
|
+
* historical flat cap, kept verbatim as the honest "we do not know".
|
|
2113
|
+
*
|
|
2114
|
+
* `bitrateKbps` is 0 for the first stats window after every dial and after
|
|
2115
|
+
* every reset, so this is the value in force exactly while the ring is filling
|
|
2116
|
+
* for the first time.
|
|
2117
|
+
*/
|
|
2118
|
+
var PRE_ROLL_UNKNOWN_BITRATE_BYTES = 8 * 1024 * 1024;
|
|
2119
|
+
/**
|
|
2120
|
+
* Hard ceiling on the WHOLE source-RTP ring (history + the current GOP).
|
|
2121
|
+
*
|
|
2122
|
+
* Lives here, next to the pre-roll budget, because the two numbers are one
|
|
2123
|
+
* number: a history budget above the ring cap is not a wider window, it is a
|
|
2124
|
+
* ring that overflows and calls `resetRtpPreBuffer`, which drops the join
|
|
2125
|
+
* bootstrap of EVERY consumer — WebRTC viewers included — for as long as the
|
|
2126
|
+
* condition holds. Keeping them in separate files is how they came to disagree.
|
|
2127
|
+
* Read by `StreamBroker.captureRtpForPreBuffer`.
|
|
2128
|
+
*/
|
|
2129
|
+
var RTP_RING_MAX_BYTES = 24 * 1024 * 1024;
|
|
2130
|
+
/**
|
|
2131
|
+
* Absolute per-stream ceiling on retained pre-roll HISTORY.
|
|
2132
|
+
*
|
|
2133
|
+
* The bitrate-derived budget is what the window COSTS; this is the most any one
|
|
2134
|
+
* stream may spend regardless. Past it the operator has configured a window this
|
|
2135
|
+
* node cannot afford on that camera, and the broker says so rather than
|
|
2136
|
+
* delivering silently less (D192).
|
|
2137
|
+
*
|
|
2138
|
+
* **DERIVED, not chosen.** It was a free-standing `32 * 1024 * 1024` — 8 MiB
|
|
2139
|
+
* ABOVE the ring it has to fit inside. A ≳13 Mbps stream at a 10 s window was
|
|
2140
|
+
* therefore authorised to overflow its own ring and cycle `resetRtpPreBuffer`:
|
|
2141
|
+
* the pre-roll knob, turned up, would have taken every viewer's join bootstrap
|
|
2142
|
+
* with it. Deriving it from {@link RTP_RING_MAX_BYTES} minus the current-GOP
|
|
2143
|
+
* reserve makes the inversion unrepresentable rather than merely fixed.
|
|
2144
|
+
*/
|
|
2145
|
+
var PRE_ROLL_ABSOLUTE_MAX_BYTES = RTP_RING_MAX_BYTES - 8 * 1024 * 1024;
|
|
2146
|
+
/**
|
|
2147
|
+
* Headroom over the OBSERVED mean bitrate.
|
|
2148
|
+
*
|
|
2149
|
+
* `BrokerStatsSchema.bitrateKbps` is a mean over the stats window; a GOP
|
|
2150
|
+
* carrying an IDR plus a scene change is well above it. Budgeting at exactly
|
|
2151
|
+
* the mean would drop the oldest GOP on every busy second, so the window would
|
|
2152
|
+
* be the one the operator asked for only while nothing moved — which is the
|
|
2153
|
+
* opposite of when it is needed.
|
|
2154
|
+
*/
|
|
2155
|
+
var PRE_ROLL_BITRATE_HEADROOM = 1.5;
|
|
2156
|
+
/**
|
|
2157
|
+
* The byte budget for the history a `windowMs` pre-roll needs on a stream
|
|
2158
|
+
* observed at `observedKbps`.
|
|
2159
|
+
*
|
|
2160
|
+
* Replaces a FLAT 8 MiB cap. That constant was a promise the feature could not
|
|
2161
|
+
* keep in either direction: above ~6.5 Mbps it silently truncated the window
|
|
2162
|
+
* (a 4K main stream asked for 10 s and got 6), and below it, it authorised a
|
|
2163
|
+
* doorbell at 512 kbps to hold 8 MiB — 16x what its own window weighs.
|
|
2164
|
+
*/
|
|
2165
|
+
function preRollBudgetBytes(windowMs, observedKbps) {
|
|
2166
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) return {
|
|
2167
|
+
bytes: 0,
|
|
2168
|
+
clamped: false
|
|
2169
|
+
};
|
|
2170
|
+
if (!Number.isFinite(observedKbps) || observedKbps <= 0) return {
|
|
2171
|
+
bytes: PRE_ROLL_UNKNOWN_BITRATE_BYTES,
|
|
2172
|
+
clamped: false
|
|
2173
|
+
};
|
|
2174
|
+
const raw = Math.round(windowMs * observedKbps * PRE_ROLL_BITRATE_HEADROOM / 8);
|
|
2175
|
+
if (raw > 16777216) return {
|
|
2176
|
+
bytes: PRE_ROLL_ABSOLUTE_MAX_BYTES,
|
|
2177
|
+
clamped: true
|
|
2178
|
+
};
|
|
2179
|
+
return {
|
|
2180
|
+
bytes: raw,
|
|
2181
|
+
clamped: false
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* The packet ceiling for a burst carrying `preRollMs` of pre-roll.
|
|
2186
|
+
*
|
|
2187
|
+
* `PRIME_MAX_PACKETS` is a defensive backstop against RTP timestamps that
|
|
2188
|
+
* stopped describing the stream — "far more packets than this much media time
|
|
2189
|
+
* can contain" — not a tuning knob. Read as an absolute it would silently kill
|
|
2190
|
+
* pre-roll on exactly the cameras that need it (a 4K main stream at 8 Mbps
|
|
2191
|
+
* fills 2 000 MTU-sized packets in under 3 s), so it is applied as the RATE it
|
|
2192
|
+
* always was: the same packets-per-span, over the span actually requested.
|
|
2193
|
+
* Never shrinks the ceiling.
|
|
2194
|
+
*/
|
|
2195
|
+
function preRollPacketCeiling(bound, preRollMs) {
|
|
2196
|
+
const window = usablePreRoll(preRollMs);
|
|
2197
|
+
if (window <= 0 || bound.maxSpanMs <= 0) return bound.maxPackets;
|
|
2198
|
+
const factor = Math.max(1, Math.ceil(window / bound.maxSpanMs));
|
|
2199
|
+
return bound.maxPackets * factor;
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* The span bound a DETECTION session's burst is held to while pre-roll is on.
|
|
2203
|
+
*
|
|
2204
|
+
* D57's bound is untouched for every other consumer; for this one it becomes
|
|
2205
|
+
* the wider of the two, because withholding a burst for being as wide as the
|
|
2206
|
+
* operator configured would make the knob read as ON and behave as OFF.
|
|
2207
|
+
*/
|
|
2208
|
+
function preRollSpanBound(bound, preRollMs) {
|
|
2209
|
+
const window = usablePreRoll(preRollMs);
|
|
2210
|
+
if (window <= 0) return bound;
|
|
2211
|
+
return {
|
|
2212
|
+
maxSpanMs: Math.max(bound.maxSpanMs, window),
|
|
2213
|
+
maxPackets: preRollPacketCeiling(bound, window)
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
//#endregion
|
|
2204
2217
|
//#region src/stream-broker/rtsp/annexb-deframer.ts
|
|
2205
2218
|
/**
|
|
2206
2219
|
* Annex-B NAL unit deframer.
|
|
@@ -2978,7 +2991,7 @@ var RtspRestreamer = class {
|
|
|
2978
2991
|
if (primePackets.length === 0) return;
|
|
2979
2992
|
const firstTs = primePackets[0].data.readUInt32BE(4);
|
|
2980
2993
|
const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
|
|
2981
|
-
const
|
|
2994
|
+
const preRollState = { plan: null };
|
|
2982
2995
|
const bound = this.currentPrimeBound();
|
|
2983
2996
|
/**
|
|
2984
2997
|
* Resolve (and memoize) the plan for one intent.
|
|
@@ -2990,10 +3003,9 @@ var RtspRestreamer = class {
|
|
|
2990
3003
|
* current GOP or a supplier bug can, and D57's remedy (join clean at the
|
|
2991
3004
|
* next live keyframe) is the right one for a burst nobody asked for.
|
|
2992
3005
|
*/
|
|
2993
|
-
const planFor = (
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
const burst = this.preRollSupplier?.(intent);
|
|
3006
|
+
const planFor = () => {
|
|
3007
|
+
if (preRollState.plan) return preRollState.plan;
|
|
3008
|
+
const burst = this.preRollSupplier?.();
|
|
2997
3009
|
const window = fromRing && burst && burst.preRollMs > 0 ? burst.packets() : [];
|
|
2998
3010
|
const packets = window.length > primePackets.length ? window.map((data) => ({
|
|
2999
3011
|
data,
|
|
@@ -3008,7 +3020,7 @@ var RtspRestreamer = class {
|
|
|
3008
3020
|
spanMs: windowSpanMs,
|
|
3009
3021
|
overBound: packets.length > 0 && (windowSpanMs > windowBound.maxSpanMs || packets.length > windowBound.maxPackets)
|
|
3010
3022
|
};
|
|
3011
|
-
|
|
3023
|
+
preRollState.plan = plan;
|
|
3012
3024
|
return plan;
|
|
3013
3025
|
};
|
|
3014
3026
|
const served = [];
|
|
@@ -3021,7 +3033,7 @@ var RtspRestreamer = class {
|
|
|
3021
3033
|
const detectionIntent = session.isDetectionIntent();
|
|
3022
3034
|
const preRollIntent = session.preRollIntent();
|
|
3023
3035
|
if (preRollIntent !== null) {
|
|
3024
|
-
const plan = planFor(
|
|
3036
|
+
const plan = planFor();
|
|
3025
3037
|
if (plan.active) {
|
|
3026
3038
|
if (plan.overBound) {
|
|
3027
3039
|
this.dropsWithheldSpan++;
|
|
@@ -3066,11 +3078,10 @@ var RtspRestreamer = class {
|
|
|
3066
3078
|
}
|
|
3067
3079
|
if (withheld > 0) this.reportDrops(false);
|
|
3068
3080
|
if (served.length === 0) return;
|
|
3069
|
-
/** Media actually delivered
|
|
3070
|
-
const servedSpanMs = (
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
};
|
|
3081
|
+
/** Media actually delivered, or `-1` when nobody asked for history at all. */
|
|
3082
|
+
const servedSpanMs = preRollState.plan === null ? -1 : Math.round(preRollState.plan.spanMs);
|
|
3083
|
+
/** Which declared intents joined this burst — `0` is "none", not "off". */
|
|
3084
|
+
const intentsServed = new Set(servedPreRoll.values());
|
|
3074
3085
|
this.logger?.info("restream prime (lat-probe)", {
|
|
3075
3086
|
tags: { deviceId: this.deviceId },
|
|
3076
3087
|
meta: {
|
|
@@ -3083,10 +3094,10 @@ var RtspRestreamer = class {
|
|
|
3083
3094
|
boundPackets: bound.maxPackets,
|
|
3084
3095
|
withheld,
|
|
3085
3096
|
detectionIntentServed,
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3097
|
+
prebufferMs: this.preRollSupplier?.().preRollMs ?? 0,
|
|
3098
|
+
preRollSpanMs: servedSpanMs,
|
|
3099
|
+
recordingIntentServed: intentsServed.has("recording"),
|
|
3100
|
+
detectionPreRollServed: intentsServed.has("detection"),
|
|
3090
3101
|
preRollServed: servedPreRoll.size
|
|
3091
3102
|
}
|
|
3092
3103
|
});
|
|
@@ -3095,16 +3106,15 @@ var RtspRestreamer = class {
|
|
|
3095
3106
|
prime: true,
|
|
3096
3107
|
keyframeAuStart: false
|
|
3097
3108
|
}));
|
|
3098
|
-
const preRollItems =
|
|
3099
|
-
for (const intent of new Set(servedPreRoll.values())) preRollItems.set(intent, (preRollPlans.get(intent)?.packets ?? []).map((packet) => ({
|
|
3109
|
+
const preRollItems = servedPreRoll.size > 0 ? (preRollState.plan?.packets ?? []).map((packet) => ({
|
|
3100
3110
|
packet,
|
|
3101
3111
|
prime: true,
|
|
3102
3112
|
keyframeAuStart: false
|
|
3103
|
-
}))
|
|
3113
|
+
})) : [];
|
|
3104
3114
|
for (const sessionId of served) {
|
|
3105
3115
|
this.pendingKeyframe.delete(sessionId);
|
|
3106
3116
|
const intent = servedPreRoll.get(sessionId);
|
|
3107
|
-
this.startPrimeReplay(sessionId, intent ? preRollItems
|
|
3117
|
+
this.startPrimeReplay(sessionId, intent ? preRollItems : items, intent ?? null);
|
|
3108
3118
|
}
|
|
3109
3119
|
}
|
|
3110
3120
|
/**
|
|
@@ -11533,8 +11543,29 @@ var StreamBroker = class StreamBroker {
|
|
|
11533
11543
|
firstKeyframeReceived = false;
|
|
11534
11544
|
/** Timer for push-mode stall detection. */
|
|
11535
11545
|
pushStallTimer;
|
|
11536
|
-
/**
|
|
11537
|
-
|
|
11546
|
+
/**
|
|
11547
|
+
* Whether the prebuffer holds this stream dialed with zero real subscribers.
|
|
11548
|
+
*
|
|
11549
|
+
* **Starts OFF, and only {@link setPrebuffer} ever writes it.** It used to
|
|
11550
|
+
* start `true`, which is how this broker came to hold a state the settings
|
|
11551
|
+
* screen contradicted: `stream-broker-manager` resolved the same question as
|
|
11552
|
+
* `enabled ?? false` and showed OFF, while an unconfigured broker was already
|
|
11553
|
+
* warm. Neither was "the current configuration" — there were two — and that
|
|
11554
|
+
* is the defect D381 retires. Unconfigured now means off; the manager applies
|
|
11555
|
+
* the resolved prebuffer to every broker it creates, so the honest default
|
|
11556
|
+
* (ON for a mains camera's warm profile) arrives from the one place that
|
|
11557
|
+
* decides it.
|
|
11558
|
+
*/
|
|
11559
|
+
_preBufferEnabled = false;
|
|
11560
|
+
/**
|
|
11561
|
+
* The open prebuffer hold, or `null` when the prebuffer is off.
|
|
11562
|
+
*
|
|
11563
|
+
* A real object rather than a flag plus a synthetic row, so "who is keeping
|
|
11564
|
+
* camera 617 warm?" is answered by something that EXISTS: it has an id, it
|
|
11565
|
+
* has a start time, and `hasDemand()` consults it the way it consults every
|
|
11566
|
+
* other holder.
|
|
11567
|
+
*/
|
|
11568
|
+
prebufferHold = null;
|
|
11538
11569
|
/**
|
|
11539
11570
|
* Seconds of AnnexB video retained SPECIFICALLY so a notification clip can be
|
|
11540
11571
|
* cut around a past instant (`renderPreBufferClip`). 0 = no clip retention.
|
|
@@ -11601,26 +11632,22 @@ var StreamBroker = class StreamBroker {
|
|
|
11601
11632
|
*/
|
|
11602
11633
|
rtpRingKeyframeMarks = [];
|
|
11603
11634
|
/**
|
|
11604
|
-
* Media time (ms) of history
|
|
11605
|
-
*
|
|
11606
|
-
* then behaves exactly as it did before pre-roll existed. Resolved per stream
|
|
11607
|
-
* by `resolvePreRollMs` and pushed by the manager.
|
|
11608
|
-
*/
|
|
11609
|
-
detectionPreRollMs = 0;
|
|
11610
|
-
/**
|
|
11611
|
-
* Media time (ms) of history a RECORDING dial (`/recording`) is handed — the
|
|
11612
|
-
* `events`-band recorder's ffmpeg, which is spawned AT the trigger and whose
|
|
11613
|
-
* first frame is otherwise at best the live edge.
|
|
11635
|
+
* Media time (ms) of history this broker RETAINS and will hand to a consumer
|
|
11636
|
+
* that declared it attaches after the edge it needs to see.
|
|
11614
11637
|
*
|
|
11615
|
-
*
|
|
11616
|
-
*
|
|
11617
|
-
*
|
|
11618
|
-
*
|
|
11619
|
-
*
|
|
11620
|
-
*
|
|
11621
|
-
* and
|
|
11638
|
+
* ONE number (D381), derived from the ONE prebuffer configuration by
|
|
11639
|
+
* `prebufferRetentionMs` and pushed by the manager through
|
|
11640
|
+
* {@link setPrebuffer}. It replaced `detectionPreRollMs` and
|
|
11641
|
+
* `recordingPreRollMs`, which were two configurations of the same retention
|
|
11642
|
+
* that the operator had to keep consistent by hand — and a third,
|
|
11643
|
+
* `preBufferSec`, which sized an Annex-B ring that is never fed for an RTP
|
|
11644
|
+
* source and therefore retained nothing at all on every warm camera in the
|
|
11645
|
+
* fleet.
|
|
11646
|
+
*
|
|
11647
|
+
* A consumer never sets this and never asks for a number wider than it: a
|
|
11648
|
+
* request is capped at read time by {@link capPreRollRequestMs}.
|
|
11622
11649
|
*/
|
|
11623
|
-
|
|
11650
|
+
prebufferMs = 0;
|
|
11624
11651
|
/**
|
|
11625
11652
|
* Effective join-burst bound for this stream's RTSP restream — the cluster
|
|
11626
11653
|
* default, or this camera stream's override. Applies to the RESTREAM
|
|
@@ -11672,11 +11699,10 @@ var StreamBroker = class StreamBroker {
|
|
|
11672
11699
|
if (logger) this.rtspRestreamer.setLogger(logger.child("rtsp"));
|
|
11673
11700
|
this.rtspRestreamer.setRtpPreBufferSupplier(() => this.getRtpPreBuffer());
|
|
11674
11701
|
this.rtspRestreamer.setPrimeBoundSupplier(() => this.joinBurstBound);
|
|
11675
|
-
this.rtspRestreamer.setPreRollSupplier((
|
|
11676
|
-
const windowMs = intent === "recording" ? this.recordingPreRollMs : this.detectionPreRollMs;
|
|
11702
|
+
this.rtspRestreamer.setPreRollSupplier(() => {
|
|
11677
11703
|
return {
|
|
11678
|
-
packets: () => this.getRtpPreRollBuffer(
|
|
11679
|
-
preRollMs:
|
|
11704
|
+
packets: () => this.getRtpPreRollBuffer(),
|
|
11705
|
+
preRollMs: this.prebufferMs
|
|
11680
11706
|
};
|
|
11681
11707
|
});
|
|
11682
11708
|
this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
|
|
@@ -12082,9 +12108,30 @@ var StreamBroker = class StreamBroker {
|
|
|
12082
12108
|
}
|
|
12083
12109
|
});
|
|
12084
12110
|
}
|
|
12085
|
-
/**
|
|
12111
|
+
/**
|
|
12112
|
+
* Open or release the prebuffer HOLD — the session that keeps this stream
|
|
12113
|
+
* dialed with no real consumer attached.
|
|
12114
|
+
*
|
|
12115
|
+
* Opening it is what "prebuffer on" MEANS: there is a holder, it has a name
|
|
12116
|
+
* and a start time, and it appears in `listClients()` beside every other
|
|
12117
|
+
* holder. Re-arming an already-open hold keeps its `since`, so the operator
|
|
12118
|
+
* reads how long this camera has been held warm rather than how long ago the
|
|
12119
|
+
* settings were last re-applied.
|
|
12120
|
+
*/
|
|
12086
12121
|
setPreBufferEnabled(enabled) {
|
|
12122
|
+
if (enabled === this._preBufferEnabled) return;
|
|
12087
12123
|
this._preBufferEnabled = enabled;
|
|
12124
|
+
this.prebufferHold = enabled ? {
|
|
12125
|
+
id: `prebuffer:${this.deviceId}`,
|
|
12126
|
+
since: Date.now()
|
|
12127
|
+
} : null;
|
|
12128
|
+
this.logger?.info(enabled ? "prebuffer hold opened" : "prebuffer hold released", {
|
|
12129
|
+
tags: { deviceId: this.numericDeviceId },
|
|
12130
|
+
meta: {
|
|
12131
|
+
brokerId: this.deviceId,
|
|
12132
|
+
retentionMs: this.prebufferMs
|
|
12133
|
+
}
|
|
12134
|
+
});
|
|
12088
12135
|
this.checkDemand();
|
|
12089
12136
|
}
|
|
12090
12137
|
/** True when at least one consumer needs the stream alive. */
|
|
@@ -12095,7 +12142,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12095
12142
|
if (this.audioChunkPlane.subscriberCount > 0) return true;
|
|
12096
12143
|
if (this.rtspRestreamer.getSessionCount() > 0) return true;
|
|
12097
12144
|
if (this.pipeServer.getClientCount() > 0) return true;
|
|
12098
|
-
if (this.
|
|
12145
|
+
if (this.prebufferHold !== null) return true;
|
|
12099
12146
|
return false;
|
|
12100
12147
|
}
|
|
12101
12148
|
/**
|
|
@@ -12719,48 +12766,36 @@ var StreamBroker = class StreamBroker {
|
|
|
12719
12766
|
this.rtpRingKeyframeMarks = [];
|
|
12720
12767
|
}
|
|
12721
12768
|
/**
|
|
12722
|
-
*
|
|
12769
|
+
* Install THE prebuffer for this stream — the one configuration that decides
|
|
12770
|
+
* whether this broker holds the source warm and how much history it keeps.
|
|
12723
12771
|
*
|
|
12724
|
-
*
|
|
12725
|
-
*
|
|
12726
|
-
*
|
|
12727
|
-
* the
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12772
|
+
* Every ring this broker owns is sized from it, each fed by the source type
|
|
12773
|
+
* it belongs to: the source-RTP ring (RTSP cameras, the history a join burst
|
|
12774
|
+
* is cut from), the Annex-B `preBuffer` (push sources), and the decoder seed.
|
|
12775
|
+
* That is the correction this record exists for — the seconds used to size
|
|
12776
|
+
* ONLY the Annex-B ring, which is never fed for an RTP source, so on every
|
|
12777
|
+
* warm camera in this fleet the configured seconds retained nothing while two
|
|
12778
|
+
* other numbers quietly sized the history that did exist.
|
|
12779
|
+
*/
|
|
12780
|
+
setPrebuffer(config) {
|
|
12781
|
+
const next = require_prebuffer.prebufferRetentionMs(config);
|
|
12782
|
+
this.setPreBufferEnabled(config.enabled);
|
|
12783
|
+
this.setPreBufferDuration(config.seconds);
|
|
12784
|
+
if (next === this.prebufferMs) return;
|
|
12785
|
+
this.prebufferMs = next;
|
|
12733
12786
|
const seedSec = Math.max(StreamBroker.DECODER_SEED_BUFFER_SEC, Math.ceil(next / 1e3) + 2);
|
|
12734
12787
|
this.decoderSeedBuffer.setDuration(seedSec);
|
|
12735
12788
|
}
|
|
12736
12789
|
/**
|
|
12737
|
-
*
|
|
12790
|
+
* How far back the ring is kept — the prebuffer, and nothing else.
|
|
12738
12791
|
*
|
|
12739
|
-
*
|
|
12740
|
-
*
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
const next = Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
12744
|
-
this.recordingPreRollMs = next;
|
|
12745
|
-
}
|
|
12746
|
-
/** The DETECTION pre-roll in force for this stream (ms). `0` = off. */
|
|
12747
|
-
getDetectionPreRollMs() {
|
|
12748
|
-
return this.detectionPreRollMs;
|
|
12749
|
-
}
|
|
12750
|
-
/** The RECORDING pre-roll in force for this stream (ms). `0` = off. */
|
|
12751
|
-
getRecordingPreRollMs() {
|
|
12752
|
-
return this.recordingPreRollMs;
|
|
12753
|
-
}
|
|
12754
|
-
/**
|
|
12755
|
-
* How far back the ring is kept: the WIDER of the two windows.
|
|
12756
|
-
*
|
|
12757
|
-
* One retention serves both because the bytes are the same bytes — the ring
|
|
12758
|
-
* is cut per intent at read time ({@link getRtpPreRollBuffer}), so a 10 s
|
|
12759
|
-
* recording window does not hand a detection dial 10 s of backlog. Retaining
|
|
12760
|
-
* per intent would be two rings of the same packets.
|
|
12792
|
+
* It used to be `max(detectionPreRollMs, recordingPreRollMs)`: a maximum over
|
|
12793
|
+
* CONFIGURATIONS, which is what made "how much history does camera 617 hold"
|
|
12794
|
+
* a question with two answers and no owner. There is one configuration now,
|
|
12795
|
+
* so the retention is it.
|
|
12761
12796
|
*/
|
|
12762
12797
|
getPreRollRetentionMs() {
|
|
12763
|
-
return
|
|
12798
|
+
return this.prebufferMs;
|
|
12764
12799
|
}
|
|
12765
12800
|
/**
|
|
12766
12801
|
* Byte budget for the EXTRA history pre-roll retains, over and above the
|
|
@@ -12794,7 +12829,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12794
12829
|
* both, so a fix to the boundary cannot land on only one plane.
|
|
12795
12830
|
*/
|
|
12796
12831
|
getDecoderSeedPackets() {
|
|
12797
|
-
if (this.
|
|
12832
|
+
if (this.prebufferMs <= 0) return this.decoderSeedBuffer.getPackets();
|
|
12798
12833
|
const all = this.decoderSeedBuffer.getAllPackets();
|
|
12799
12834
|
if (all.length === 0) return [];
|
|
12800
12835
|
const newestPts = all[all.length - 1].pts;
|
|
@@ -12806,7 +12841,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12806
12841
|
ageMs: newestPts - packet.pts
|
|
12807
12842
|
});
|
|
12808
12843
|
}
|
|
12809
|
-
const selection = selectPreRollStart(marks, this.
|
|
12844
|
+
const selection = selectPreRollStart(marks, this.prebufferMs);
|
|
12810
12845
|
return selection.startIndex < 0 ? [] : all.slice(selection.startIndex);
|
|
12811
12846
|
}
|
|
12812
12847
|
/** Retained keyframe AUs as ages in ms of media time behind `newestTs`. */
|
|
@@ -12876,8 +12911,9 @@ var StreamBroker = class StreamBroker {
|
|
|
12876
12911
|
* when motion fired retained nothing, and the honest outcome there is the
|
|
12877
12912
|
* Stage 0 burst rather than a pretend one.
|
|
12878
12913
|
*/
|
|
12879
|
-
getRtpPreRollBuffer(
|
|
12914
|
+
getRtpPreRollBuffer(requestedMs) {
|
|
12880
12915
|
if (!this.rtpRingHasKeyframe) return [];
|
|
12916
|
+
const windowMs = require_prebuffer.capPreRollRequestMs(requestedMs, this.prebufferMs);
|
|
12881
12917
|
if (windowMs <= 0 || this.rtpRing.length === 0) return this.getRtpPreBuffer();
|
|
12882
12918
|
const newestTs = this.rtpRing[this.rtpRing.length - 1].readUInt32BE(4);
|
|
12883
12919
|
const selection = selectPreRollStart(this.rtpPreRollMarks(newestTs), windowMs);
|
|
@@ -12900,8 +12936,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12900
12936
|
tags: { deviceId: this.numericDeviceId },
|
|
12901
12937
|
meta: {
|
|
12902
12938
|
retentionMs,
|
|
12903
|
-
|
|
12904
|
-
recordingPreRollMs: this.recordingPreRollMs,
|
|
12939
|
+
prebufferMs: this.prebufferMs,
|
|
12905
12940
|
bitrateKbps: this.bitrateKbps,
|
|
12906
12941
|
budgetBytes: budget.bytes
|
|
12907
12942
|
}
|
|
@@ -13157,15 +13192,16 @@ var StreamBroker = class StreamBroker {
|
|
|
13157
13192
|
subscribedAt: entry.subscribedAt,
|
|
13158
13193
|
packetsDelivered: entry.packetsDelivered
|
|
13159
13194
|
});
|
|
13160
|
-
|
|
13161
|
-
|
|
13195
|
+
const hold = this.prebufferHold;
|
|
13196
|
+
if (hold) encoded.push({
|
|
13197
|
+
id: hold.id,
|
|
13162
13198
|
channel: "annexb",
|
|
13163
13199
|
attribution: {
|
|
13164
13200
|
kind: "warmup",
|
|
13165
13201
|
userAgent: "prebuffer",
|
|
13166
13202
|
media: "video"
|
|
13167
13203
|
},
|
|
13168
|
-
subscribedAt:
|
|
13204
|
+
subscribedAt: hold.since,
|
|
13169
13205
|
packetsDelivered: 0
|
|
13170
13206
|
});
|
|
13171
13207
|
return {
|
|
@@ -15081,7 +15117,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15081
15117
|
cachedIngestOwnerAt = 0;
|
|
15082
15118
|
static INGEST_OWNER_CACHE_TTL_MS = 6e4;
|
|
15083
15119
|
lastOwnershipReconcile = Promise.resolve();
|
|
15084
|
-
defaultPreBufferSec =
|
|
15120
|
+
defaultPreBufferSec = 5;
|
|
15085
15121
|
/**
|
|
15086
15122
|
* Cluster-wide default join-burst span bound (D57). `undefined` until the
|
|
15087
15123
|
* addon applies its config, at which point every live broker is re-bounded.
|
|
@@ -15090,20 +15126,6 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15090
15126
|
* happened to own its ingest (D52/D56).
|
|
15091
15127
|
*/
|
|
15092
15128
|
defaultJoinBurstMaxSpanMs;
|
|
15093
|
-
/**
|
|
15094
|
-
* Cluster-wide default pre-roll (ms) — motion-start Stage 2. Cluster-scoped
|
|
15095
|
-
* for the same reason as the join-burst bound (a movable ingest role must not
|
|
15096
|
-
* change a camera's behaviour), and 0 by default: pre-roll spends retained
|
|
15097
|
-
* bytes on the ingest node, so it is turned on per camera that needs it.
|
|
15098
|
-
*/
|
|
15099
|
-
defaultPreRollMs = 0;
|
|
15100
|
-
/**
|
|
15101
|
-
* Cluster-wide default RECORDING pre-roll (ms). `undefined` until the addon
|
|
15102
|
-
* pushes its config — which is a different fact from `0` and must stay so:
|
|
15103
|
-
* `0` is an operator turning the feature OFF, `undefined` is a boot that has
|
|
15104
|
-
* not read the setting yet and takes `DEFAULT_RECORDING_PRE_ROLL_MS`.
|
|
15105
|
-
*/
|
|
15106
|
-
defaultRecordingPreRollMs = void 0;
|
|
15107
15129
|
webrtcServer = null;
|
|
15108
15130
|
/**
|
|
15109
15131
|
* brokerId → the remote-encoded adapter backing a non-owned camera's WebRTC
|
|
@@ -15709,8 +15731,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15709
15731
|
return always;
|
|
15710
15732
|
}
|
|
15711
15733
|
}
|
|
15734
|
+
/**
|
|
15735
|
+
* The cluster-wide prebuffer SECONDS — the only duration in this subsystem
|
|
15736
|
+
* (D381). Pushed into every live broker, because a broker created before the
|
|
15737
|
+
* edit would otherwise keep the old retention for the life of the stream.
|
|
15738
|
+
*/
|
|
15712
15739
|
setDefaultPreBufferSec(sec) {
|
|
15713
15740
|
this.defaultPreBufferSec = sec;
|
|
15741
|
+
this.pushPrebufferToLiveBrokers();
|
|
15714
15742
|
}
|
|
15715
15743
|
/**
|
|
15716
15744
|
* Set the cluster-wide default join-burst span bound and push the newly
|
|
@@ -15742,48 +15770,16 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15742
15770
|
}));
|
|
15743
15771
|
}
|
|
15744
15772
|
/**
|
|
15745
|
-
* Set the cluster-wide default
|
|
15746
|
-
* into every live broker —
|
|
15747
|
-
*
|
|
15773
|
+
* Set the cluster-wide default prebuffer SECONDS and push the newly
|
|
15774
|
+
* effective configuration into every live broker — a broker outlives the
|
|
15775
|
+
* setting that configured it, so an un-pushed edit would apply to cameras
|
|
15776
|
+
* published afterwards and to no others.
|
|
15748
15777
|
*/
|
|
15749
|
-
|
|
15750
|
-
this.defaultPreRollMs = ms;
|
|
15778
|
+
pushPrebufferToLiveBrokers() {
|
|
15751
15779
|
for (const [brokerId, broker] of this.brokers) {
|
|
15752
15780
|
const parsed = parseBrokerId(brokerId);
|
|
15753
15781
|
if (!parsed) continue;
|
|
15754
|
-
this.
|
|
15755
|
-
}
|
|
15756
|
-
}
|
|
15757
|
-
/**
|
|
15758
|
-
* Resolve and install this stream's pre-roll (Stage 2). A `pull-http` relay
|
|
15759
|
-
* — the terminal cameras — takes the cluster default as 0 (operator
|
|
15760
|
-
* decision); an explicit per-stream override still reaches it.
|
|
15761
|
-
*/
|
|
15762
|
-
applyPreRoll(broker, deviceId, camStreamId) {
|
|
15763
|
-
const terminalRelay = this.cameraStreams.get(deviceId)?.get(camStreamId)?.kind === "pull-http";
|
|
15764
|
-
broker.setDetectionPreRollMs(resolvePreRollMs({
|
|
15765
|
-
camStreamId,
|
|
15766
|
-
clusterMs: this.defaultPreRollMs,
|
|
15767
|
-
overrides: this.deviceOverrides.get(deviceId)?.preRollMs,
|
|
15768
|
-
terminalRelay
|
|
15769
|
-
}));
|
|
15770
|
-
broker.setRecordingPreRollMs(resolveRecordingPreRollMs({
|
|
15771
|
-
camStreamId,
|
|
15772
|
-
clusterMs: this.defaultRecordingPreRollMs,
|
|
15773
|
-
terminalRelay,
|
|
15774
|
-
batteryOperated: this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated)
|
|
15775
|
-
}));
|
|
15776
|
-
}
|
|
15777
|
-
/**
|
|
15778
|
-
* Set the cluster-wide default RECORDING pre-roll and push it into every live
|
|
15779
|
-
* broker — same reasoning as detection's: a broker outlives its setting.
|
|
15780
|
-
*/
|
|
15781
|
-
setDefaultRecordingPreRollMs(ms) {
|
|
15782
|
-
this.defaultRecordingPreRollMs = ms;
|
|
15783
|
-
for (const [brokerId, broker] of this.brokers) {
|
|
15784
|
-
const parsed = parseBrokerId(brokerId);
|
|
15785
|
-
if (!parsed) continue;
|
|
15786
|
-
this.applyPreRoll(broker, parsed.deviceId, parsed.camStreamId);
|
|
15782
|
+
this.applyPrebufferConfig(broker, parsed.deviceId, parsed.camStreamId);
|
|
15787
15783
|
}
|
|
15788
15784
|
}
|
|
15789
15785
|
setEventBus(bus) {
|
|
@@ -17767,7 +17763,17 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17767
17763
|
}
|
|
17768
17764
|
loadPersistedDeviceOverrides(overrides) {
|
|
17769
17765
|
this.deviceOverrides.clear();
|
|
17770
|
-
for (const [
|
|
17766
|
+
for (const [deviceId, persisted] of overrides) {
|
|
17767
|
+
const { override, migrated, superseded } = migrateRetiredPreRollOverride(persisted);
|
|
17768
|
+
if (migrated.length > 0 || superseded.length > 0) this.logger.info("retired per-stream pre-roll folded into the prebuffer", {
|
|
17769
|
+
tags: { deviceId },
|
|
17770
|
+
meta: {
|
|
17771
|
+
migrated,
|
|
17772
|
+
superseded
|
|
17773
|
+
}
|
|
17774
|
+
});
|
|
17775
|
+
this.deviceOverrides.set(deviceId, override);
|
|
17776
|
+
}
|
|
17771
17777
|
(async () => {
|
|
17772
17778
|
for (const [deviceId, override] of overrides) {
|
|
17773
17779
|
if (!override.derivedStreams) continue;
|
|
@@ -17799,19 +17805,6 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17799
17805
|
})();
|
|
17800
17806
|
}
|
|
17801
17807
|
/**
|
|
17802
|
-
* Effective pre-buffer duration for a brokerId's profile.
|
|
17803
|
-
*
|
|
17804
|
-
* Priority: per-profile override → legacy single-value override →
|
|
17805
|
-
* battery-aware default (0 when the device has BatteryOperated and
|
|
17806
|
-
* no override) → addon default.
|
|
17807
|
-
*/
|
|
17808
|
-
getEffectivePreBufferSec(deviceId, camStreamId, addonDefault, forceForDetection = false) {
|
|
17809
|
-
const perStream = this.deviceOverrides.get(deviceId)?.preBuffer?.[camStreamId];
|
|
17810
|
-
if (perStream) return perStream.enabled || forceForDetection ? perStream.seconds : 0;
|
|
17811
|
-
if (this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated)) return 0;
|
|
17812
|
-
return addonDefault;
|
|
17813
|
-
}
|
|
17814
|
-
/**
|
|
17815
17808
|
* Per-device streaming debug flag — controls verbose info-level
|
|
17816
17809
|
* logging across the broker, WebRTC server, and session for one
|
|
17817
17810
|
* device. Off by default.
|
|
@@ -17951,9 +17944,9 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17951
17944
|
const profile = findProfileForStream(assignment.map, camStreamId);
|
|
17952
17945
|
const rtspEnabled = this.rtspProvider.isEnabled(brokerId);
|
|
17953
17946
|
const rtspEntry = this.rtspProvider.getEntry(brokerId);
|
|
17954
|
-
const
|
|
17955
|
-
const preBufferEnabled =
|
|
17956
|
-
const preBufferSec =
|
|
17947
|
+
const effectivePrebuffer = this.effectivePrebuffer(deviceId, camStreamId);
|
|
17948
|
+
const preBufferEnabled = effectivePrebuffer.enabled;
|
|
17949
|
+
const preBufferSec = effectivePrebuffer.enabled ? effectivePrebuffer.seconds : this.defaultPreBufferSec;
|
|
17957
17950
|
const hwAccelValue = override?.hwaccel?.[camStreamId] ?? "copy";
|
|
17958
17951
|
const sourceParserOptionSettings = sourceParserOptionsFor(sourceTypeForParser(stream.kind));
|
|
17959
17952
|
const sourceParserField = sourceParserOptionSettings.length > 0 ? [{
|
|
@@ -18049,16 +18042,17 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18049
18042
|
style: "switch",
|
|
18050
18043
|
key: `preBufferEnabled:${camStreamId}`,
|
|
18051
18044
|
label: "Pre-buffer",
|
|
18052
|
-
description: "
|
|
18053
|
-
default:
|
|
18045
|
+
description: "Hold this stream dialed and keep the last few seconds of it, so anything that attaches after an event — a recording, a detection decode — starts BEFORE the event instead of at the live edge. This is the only history setting in the system: recordings and detection ask for what is kept here and never configure a window of their own. Costs a permanent connection to the camera and memory on the ingest node. On by default for a mains camera's main profile; off for a battery camera, which has nothing to hold between wakes.",
|
|
18046
|
+
default: preBufferEnabled,
|
|
18054
18047
|
value: preBufferEnabled
|
|
18055
18048
|
},
|
|
18056
18049
|
{
|
|
18057
18050
|
type: "slider",
|
|
18058
18051
|
key: `preBufferSec:${camStreamId}`,
|
|
18059
18052
|
label: "Pre-buffer Duration",
|
|
18053
|
+
description: "Seconds of history kept while the pre-buffer is on. A consumer is served this much or less — never more, and never more than the stream has actually been warm for.",
|
|
18060
18054
|
min: 0,
|
|
18061
|
-
max:
|
|
18055
|
+
max: 15,
|
|
18062
18056
|
step: 1,
|
|
18063
18057
|
unit: "s",
|
|
18064
18058
|
default: this.defaultPreBufferSec,
|
|
@@ -18078,19 +18072,6 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18078
18072
|
default: 0,
|
|
18079
18073
|
value: override?.joinBurstMaxSpanMs?.[camStreamId] ?? 0
|
|
18080
18074
|
},
|
|
18081
|
-
{
|
|
18082
|
-
type: "number",
|
|
18083
|
-
key: `preRollMs:${camStreamId}`,
|
|
18084
|
-
label: "Detection pre-roll",
|
|
18085
|
-
description: "How much video from BEFORE the motion edge a detection decode is handed when it attaches, so the first analysed frames predate the trigger. Costs retained memory on the ingest node while this stream is dialed, and only reaches back as far as the stream has been warm — it never dials a camera. Served to the detection decode alone: viewers, recordings and HomeKit legs are unaffected. 0 = inherit the cluster default (off).",
|
|
18086
|
-
min: 0,
|
|
18087
|
-
max: PRE_ROLL_MAX_MS,
|
|
18088
|
-
step: 250,
|
|
18089
|
-
unit: "ms",
|
|
18090
|
-
span: 2,
|
|
18091
|
-
default: 0,
|
|
18092
|
-
value: override?.preRollMs?.[camStreamId] ?? 0
|
|
18093
|
-
},
|
|
18094
18075
|
{
|
|
18095
18076
|
type: "select",
|
|
18096
18077
|
key: `hwaccel:${camStreamId}`,
|
|
@@ -18299,36 +18280,29 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18299
18280
|
this.rtspProvider.setEnabled(brokerId, Boolean(value));
|
|
18300
18281
|
continue;
|
|
18301
18282
|
}
|
|
18302
|
-
if (fieldKey.startsWith("preBufferEnabled:")) {
|
|
18303
|
-
const
|
|
18283
|
+
if (fieldKey.startsWith("preBufferEnabled:") || fieldKey.startsWith("preBufferSec:")) {
|
|
18284
|
+
const isSwitch = fieldKey.startsWith("preBufferEnabled:");
|
|
18285
|
+
const camStreamId = fieldKey.slice((isSwitch ? "preBufferEnabled:" : "preBufferSec:").length);
|
|
18304
18286
|
overrideDirty = true;
|
|
18305
18287
|
const pb = { ...nextOverride.preBuffer };
|
|
18306
|
-
const current = pb[camStreamId] ?? this.
|
|
18307
|
-
|
|
18308
|
-
|
|
18288
|
+
const current = pb[camStreamId] ?? this.effectivePrebuffer(deviceId, camStreamId);
|
|
18289
|
+
pb[camStreamId] = isSwitch ? {
|
|
18290
|
+
enabled: Boolean(value),
|
|
18291
|
+
seconds: current.seconds > 0 ? current.seconds : this.defaultPreBufferSec
|
|
18292
|
+
} : {
|
|
18309
18293
|
...current,
|
|
18310
|
-
|
|
18294
|
+
seconds: typeof value === "number" && Number.isFinite(value) ? value : this.defaultPreBufferSec
|
|
18311
18295
|
};
|
|
18312
18296
|
nextOverride.preBuffer = pb;
|
|
18313
18297
|
const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
|
|
18314
|
-
if (broker) {
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18319
|
-
|
|
18320
|
-
|
|
18321
|
-
|
|
18322
|
-
const seconds = typeof value === "number" && Number.isFinite(value) ? value : this.defaultPreBufferSec;
|
|
18323
|
-
overrideDirty = true;
|
|
18324
|
-
const pb = { ...nextOverride.preBuffer };
|
|
18325
|
-
pb[camStreamId] = {
|
|
18326
|
-
...pb[camStreamId] ?? this.defaultStreamPreBuffer(deviceId, camStreamId),
|
|
18327
|
-
seconds
|
|
18328
|
-
};
|
|
18329
|
-
nextOverride.preBuffer = pb;
|
|
18330
|
-
const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
|
|
18331
|
-
if (broker && pb[camStreamId]?.enabled !== false) broker.setPreBufferDuration(seconds);
|
|
18298
|
+
if (broker) broker.setPrebuffer(require_prebuffer.resolvePrebuffer({
|
|
18299
|
+
camStreamId,
|
|
18300
|
+
clusterSeconds: this.defaultPreBufferSec,
|
|
18301
|
+
overrides: pb,
|
|
18302
|
+
defaultWarm: this.isDefaultWarm(deviceId, camStreamId),
|
|
18303
|
+
batteryOperated: this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated),
|
|
18304
|
+
terminalRelay: this.cameraStreams.get(deviceId)?.get(camStreamId)?.kind === "pull-http"
|
|
18305
|
+
}));
|
|
18332
18306
|
continue;
|
|
18333
18307
|
}
|
|
18334
18308
|
if (fieldKey.startsWith("joinBurstMaxSpanMs:")) {
|
|
@@ -18346,22 +18320,6 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18346
18320
|
}));
|
|
18347
18321
|
continue;
|
|
18348
18322
|
}
|
|
18349
|
-
if (fieldKey.startsWith("preRollMs:")) {
|
|
18350
|
-
const camStreamId = fieldKey.slice(10);
|
|
18351
|
-
overrideDirty = true;
|
|
18352
|
-
const pr = { ...nextOverride.preRollMs };
|
|
18353
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) pr[camStreamId] = Math.min(PRE_ROLL_MAX_MS, Math.max(250, value));
|
|
18354
|
-
else delete pr[camStreamId];
|
|
18355
|
-
nextOverride.preRollMs = pr;
|
|
18356
|
-
const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
|
|
18357
|
-
if (broker) broker.setDetectionPreRollMs(resolvePreRollMs({
|
|
18358
|
-
camStreamId,
|
|
18359
|
-
clusterMs: this.defaultPreRollMs,
|
|
18360
|
-
overrides: pr,
|
|
18361
|
-
terminalRelay: this.cameraStreams.get(deviceId)?.get(camStreamId)?.kind === "pull-http"
|
|
18362
|
-
}));
|
|
18363
|
-
continue;
|
|
18364
|
-
}
|
|
18365
18323
|
if (fieldKey.startsWith("hwaccel:")) {
|
|
18366
18324
|
const camStreamId = fieldKey.slice(8);
|
|
18367
18325
|
overrideDirty = true;
|
|
@@ -18707,7 +18665,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18707
18665
|
if (current.map.high !== newMap.high) for (const camStreamId of [current.map.high, newMap.high]) {
|
|
18708
18666
|
if (!camStreamId) continue;
|
|
18709
18667
|
const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
|
|
18710
|
-
if (broker) this.
|
|
18668
|
+
if (broker) this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
18711
18669
|
}
|
|
18712
18670
|
for (const [camId, prevCount] of oldPushConsumers) if ((newPushConsumers.get(camId) ?? 0) === 0 && prevCount > 0) this.emitCamStreamIdle(deviceId, camId);
|
|
18713
18671
|
for (const [camId, nextCount] of newPushConsumers) if ((oldPushConsumers.get(camId) ?? 0) === 0 && nextCount > 0) {
|
|
@@ -18990,11 +18948,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18990
18948
|
const cam = this.cameraStreams.get(deviceId)?.get(camStreamId);
|
|
18991
18949
|
if (!cam) return;
|
|
18992
18950
|
const brokerId = brokerIdFor(deviceId, camStreamId);
|
|
18993
|
-
|
|
18994
|
-
if (live) {
|
|
18995
|
-
this.applyPreRoll(live, deviceId, camStreamId);
|
|
18996
|
-
return;
|
|
18997
|
-
}
|
|
18951
|
+
if (this.brokers.get(brokerId)) return;
|
|
18998
18952
|
const existingEnsure = this.brokerEnsures.get(brokerId);
|
|
18999
18953
|
if (existingEnsure) {
|
|
19000
18954
|
await existingEnsure;
|
|
@@ -19041,10 +18995,9 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19041
18995
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
19042
18996
|
broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
|
|
19043
18997
|
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
19044
|
-
this.
|
|
18998
|
+
this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
19045
18999
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
19046
19000
|
this.applyJoinBurstBound(broker, deviceId, camStreamId);
|
|
19047
|
-
this.applyPreRoll(broker, deviceId, camStreamId);
|
|
19048
19001
|
this.applyClipRetention(deviceId);
|
|
19049
19002
|
if (this.deviceOverrides.get(deviceId)?.streamingDebug) broker.setStreamingDebug(true);
|
|
19050
19003
|
broker.setSourceProvider(() => {
|
|
@@ -19262,63 +19215,47 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19262
19215
|
return this.brokers.get(brokerIdFor(deviceId, camStreamId))?.getStats().audio?.codec ?? null;
|
|
19263
19216
|
}
|
|
19264
19217
|
/**
|
|
19265
|
-
* Whether a stream is warmed BY DEFAULT
|
|
19266
|
-
* `high` profile, on a non-battery camera, from a source that is worth
|
|
19267
|
-
* keeping hot. Single source of truth used by both the broker-config default
|
|
19268
|
-
* (`applyPreBufferConfig`) and the settings-patch override synthesis
|
|
19269
|
-
* (`defaultStreamPreBuffer`) so a seconds-only patch never silently disables
|
|
19270
|
-
* the high-profile warm default.
|
|
19218
|
+
* Whether a stream is warmed BY DEFAULT — the device's `high` profile.
|
|
19271
19219
|
*
|
|
19272
|
-
*
|
|
19273
|
-
*
|
|
19274
|
-
* `
|
|
19275
|
-
* of
|
|
19276
|
-
*
|
|
19277
|
-
* the live hub: the terminal addon's glances relay burned ~12% of a core
|
|
19278
|
-
* and held its PTY open for hours with zero viewers. The trade the warm
|
|
19279
|
-
* default buys elsewhere — a seeded GOP instead of a ~5-8 s cold start —
|
|
19280
|
-
* does not apply here: an MJPEG relay is cheap to re-dial (no camera
|
|
19281
|
-
* handshake) and expensive to keep hot. An explicit operator ON override
|
|
19282
|
-
* still warms it; this is a DEFAULT, not a ban.
|
|
19220
|
+
* ONE input to {@link resolvePrebuffer}, not a decision of its own. Battery
|
|
19221
|
+
* and `pull-http` used to be tested here as well AND in
|
|
19222
|
+
* `getEffectivePreBufferSec` AND in `resolveRecordingPreRollMs`: three
|
|
19223
|
+
* statements of one rule, which is how they came to be able to disagree
|
|
19224
|
+
* about a single camera. They are stated once now, in `prebuffer.ts`.
|
|
19283
19225
|
*/
|
|
19284
19226
|
isDefaultWarm(deviceId, camStreamId) {
|
|
19285
|
-
|
|
19286
|
-
if (this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated)) return false;
|
|
19287
|
-
return this.cameraStreams.get(deviceId)?.get(camStreamId)?.kind !== "pull-http";
|
|
19227
|
+
return findProfileForStream(this.assignments.get(deviceId)?.map ?? {}, camStreamId) === "high";
|
|
19288
19228
|
}
|
|
19289
19229
|
/**
|
|
19290
|
-
*
|
|
19291
|
-
*
|
|
19292
|
-
*
|
|
19293
|
-
*
|
|
19294
|
-
*
|
|
19295
|
-
*
|
|
19296
|
-
*
|
|
19297
|
-
*
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19230
|
+
* THE prebuffer for a (device, stream) — the one answer every caller uses.
|
|
19231
|
+
*
|
|
19232
|
+
* The settings screen reads it to display, the patch handler reads it to
|
|
19233
|
+
* apply, and {@link applyPrebufferConfig} reads it to install. That is the
|
|
19234
|
+
* D315-shaped half of this record: what the operator is SHOWN is resolved by
|
|
19235
|
+
* the same function that decides what runs, so a switch can no longer report
|
|
19236
|
+
* off on a function that is on — which is exactly what the settings
|
|
19237
|
+
* contribution did while it answered `perStream?.enabled ?? false` and the
|
|
19238
|
+
* broker's own flag shipped `true`.
|
|
19239
|
+
*/
|
|
19240
|
+
effectivePrebuffer(deviceId, camStreamId) {
|
|
19241
|
+
return require_prebuffer.resolvePrebuffer({
|
|
19242
|
+
camStreamId,
|
|
19243
|
+
clusterSeconds: this.defaultPreBufferSec,
|
|
19244
|
+
overrides: this.deviceOverrides.get(deviceId)?.preBuffer,
|
|
19245
|
+
defaultWarm: this.isDefaultWarm(deviceId, camStreamId),
|
|
19246
|
+
batteryOperated: this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated),
|
|
19247
|
+
terminalRelay: this.cameraStreams.get(deviceId)?.get(camStreamId)?.kind === "pull-http"
|
|
19248
|
+
});
|
|
19304
19249
|
}
|
|
19305
19250
|
/**
|
|
19306
|
-
*
|
|
19307
|
-
*
|
|
19308
|
-
*
|
|
19309
|
-
*
|
|
19310
|
-
*
|
|
19251
|
+
* Install the effective prebuffer on a broker. Idempotent — safe on every
|
|
19252
|
+
* publish, every re-publish and every settings edit, which is what lets a
|
|
19253
|
+
* camera discovered late (a battery flag that arrives after the first dial)
|
|
19254
|
+
* converge instead of holding the wrong retention for the life of the
|
|
19255
|
+
* process.
|
|
19311
19256
|
*/
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
if (perStream) {
|
|
19315
|
-
broker.setPreBufferEnabled(perStream.enabled);
|
|
19316
|
-
broker.setPreBufferDuration(perStream.enabled ? perStream.seconds : 0);
|
|
19317
|
-
return;
|
|
19318
|
-
}
|
|
19319
|
-
const defaultWarm = this.isDefaultWarm(deviceId, camStreamId);
|
|
19320
|
-
broker.setPreBufferEnabled(defaultWarm);
|
|
19321
|
-
broker.setPreBufferDuration(defaultWarm ? this.defaultPreBufferSec : 0);
|
|
19257
|
+
applyPrebufferConfig(broker, deviceId, camStreamId) {
|
|
19258
|
+
broker.setPrebuffer(this.effectivePrebuffer(deviceId, camStreamId));
|
|
19322
19259
|
}
|
|
19323
19260
|
/** Push the persisted mute onto a broker. Idempotent — safe on every pass. */
|
|
19324
19261
|
applyAudioMuteConfig(broker, deviceId) {
|
|
@@ -29658,12 +29595,10 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
29658
29595
|
lastProbePersistAt = 0;
|
|
29659
29596
|
constructor() {
|
|
29660
29597
|
super({
|
|
29661
|
-
defaultPreBufferSec:
|
|
29598
|
+
defaultPreBufferSec: 5,
|
|
29662
29599
|
rtspPort: 8554,
|
|
29663
29600
|
catalogReconcileIntervalSec: 30,
|
|
29664
29601
|
joinBurstMaxSpanMs: DEFAULT_PRIME_BURST_BOUND.maxSpanMs,
|
|
29665
|
-
preRollMs: 0,
|
|
29666
|
-
recordingPreRollMs: DEFAULT_RECORDING_PRE_ROLL_MS,
|
|
29667
29602
|
binaryPath: "",
|
|
29668
29603
|
hwaccel: "copy"
|
|
29669
29604
|
});
|
|
@@ -29733,8 +29668,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
29733
29668
|
this.brokerManager.setCostRegistry(this.costRegistry);
|
|
29734
29669
|
this.brokerManager.setDefaultPreBufferSec(this.config.defaultPreBufferSec);
|
|
29735
29670
|
this.brokerManager.setDefaultJoinBurstMaxSpanMs(this.config.joinBurstMaxSpanMs);
|
|
29736
|
-
this.brokerManager.setDefaultPreRollMs(this.config.preRollMs);
|
|
29737
|
-
this.brokerManager.setDefaultRecordingPreRollMs(this.config.recordingPreRollMs);
|
|
29738
29671
|
this.brokerManager.setEventBus(this.ctx.eventBus);
|
|
29739
29672
|
const localNode = this.ctx.kernel.localNodeId;
|
|
29740
29673
|
const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
|
|
@@ -30221,7 +30154,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
30221
30154
|
*/
|
|
30222
30155
|
async purgeFlattenedSettingsKeys() {
|
|
30223
30156
|
try {
|
|
30224
|
-
await
|
|
30157
|
+
await require_prebuffer.retireRootKeys({
|
|
30225
30158
|
store: this.ctx.api.settingsStore,
|
|
30226
30159
|
addonId: STREAM_BROKER_ADDON_ID,
|
|
30227
30160
|
logger: this.ctx.logger,
|
|
@@ -30256,11 +30189,11 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
30256
30189
|
type: "number",
|
|
30257
30190
|
key: "defaultPreBufferSec",
|
|
30258
30191
|
label: "Pre-buffer Duration",
|
|
30259
|
-
description: "Seconds of
|
|
30260
|
-
min:
|
|
30261
|
-
max:
|
|
30192
|
+
description: "Seconds of history a warm stream keeps. This is the ONLY history setting in the system: an event recording and a detection decode are both served from it and neither configures a window of its own — each is handed this much or whatever the stream has actually been warm for, whichever is less. It costs memory on whichever node ingests the camera, roughly this many seconds of that stream’s bitrate. Turn the pre-buffer on or off per camera stream in the camera’s Streams settings.",
|
|
30193
|
+
min: 1,
|
|
30194
|
+
max: 15,
|
|
30262
30195
|
step: 1,
|
|
30263
|
-
default:
|
|
30196
|
+
default: 5,
|
|
30264
30197
|
unit: "s"
|
|
30265
30198
|
},
|
|
30266
30199
|
{
|
|
@@ -30305,28 +30238,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
30305
30238
|
default: DEFAULT_PRIME_BURST_BOUND.maxSpanMs,
|
|
30306
30239
|
unit: "ms"
|
|
30307
30240
|
},
|
|
30308
|
-
{
|
|
30309
|
-
type: "number",
|
|
30310
|
-
key: "preRollMs",
|
|
30311
|
-
label: "Detection pre-roll",
|
|
30312
|
-
description: "Cluster-wide default for how much video from BEFORE the motion edge a detection decode is handed when it attaches, so the first analysed frames predate the trigger. Off (0) by default: it retains extra video in memory on whichever node ingests the camera, and it can only reach as far back as that stream has been warm — it never dials a camera on its own. Only the detection decode receives it; viewers, recordings and HomeKit legs keep the ordinary join burst. Terminal (MJPEG relay) cameras ignore this default. Override per camera stream.",
|
|
30313
|
-
min: 0,
|
|
30314
|
-
max: PRE_ROLL_MAX_MS,
|
|
30315
|
-
step: 250,
|
|
30316
|
-
default: 0,
|
|
30317
|
-
unit: "ms"
|
|
30318
|
-
},
|
|
30319
|
-
{
|
|
30320
|
-
type: "number",
|
|
30321
|
-
key: "recordingPreRollMs",
|
|
30322
|
-
label: "Recording pre-roll",
|
|
30323
|
-
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.",
|
|
30324
|
-
min: 0,
|
|
30325
|
-
max: RECORDING_PRE_ROLL_MAX_MS,
|
|
30326
|
-
step: 500,
|
|
30327
|
-
default: DEFAULT_RECORDING_PRE_ROLL_MS,
|
|
30328
|
-
unit: "ms"
|
|
30329
|
-
},
|
|
30330
30241
|
{
|
|
30331
30242
|
type: "number",
|
|
30332
30243
|
key: "catalogReconcileIntervalSec",
|