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