@camstack/addon-pipeline 1.2.198 → 1.2.200
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-B5v8qEI0.js → default-detection-model-BsmxtyZx.js} +1 -1
- package/dist/{default-detection-model-CASpAESz.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-BxP0SvPz.js → dist-D2YTTkRf.js} +436 -419
- package/dist/{dist-oFIHnD88.mjs → dist-mEsrAAc3.mjs} +436 -419
- package/dist/{lazy-sharp-VKOG3WW4.js → lazy-sharp-B5bW34uC.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-B4zETsJ9.mjs → node-CUxep5tc.mjs} +1 -1
- package/dist/{node-BIKrNEtS.js → node-x3YokJkD.js} +1 -1
- package/dist/pipeline-runner/index.js +8 -7
- package/dist/pipeline-runner/index.mjs +7 -6
- package/dist/{process-memory-D9xYwm6L.mjs → process-memory-BCqpTruN.mjs} +1 -1
- package/dist/{process-memory-CMHOmj75.js → process-memory-CFc4D9A2.js} +1 -1
- package/dist/recorder/index.js +169 -334
- package/dist/recorder/index.mjs +164 -329
- package/dist/restream-intent-B1Difcnq.js +313 -0
- package/dist/restream-intent-DWe2kInY.mjs +248 -0
- package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
- package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
- package/dist/{segment-demux-js-DLwL1vmq.js → segment-demux-js-DUZVURQd.js} +1 -1
- package/dist/{segment-demux-js-DZysrgdC.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +25 -5
- package/dist/session-decode/decode-worker-child.mjs +24 -4
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2eTdxvd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-NqqfpkYF.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D62O5bRa.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-C8tu7NS4.mjs → hostInit-D_cZQhBA.mjs} +3 -3
- package/dist/stream-broker/index.js +181 -48
- package/dist/stream-broker/index.mjs +172 -39
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-D2mMjUXv.js → worker-protocol-CRDaBlJl.js} +1 -1
- package/dist/{worker-protocol-DFlZm_wr.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
- package/package.json +1 -1
- package/dist/restream-intent-B4BXZra7.mjs +0 -72
- package/dist/restream-intent-Cv9x3jmu.js +0 -89
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BcjLrxhF.mjs +0 -26
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/** {@link PREBUFFER_MAX_SEC} in ms — the widest window any consumer can be served. */
|
|
2
|
+
var PREBUFFER_MAX_MS = 15 * 1e3;
|
|
3
|
+
/** Clamp an enabled window's seconds into `[MIN, MAX]`; `0`/invalid means off. */
|
|
4
|
+
function clampSeconds(value) {
|
|
5
|
+
if (value === void 0) return 5;
|
|
6
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
7
|
+
return Math.min(15, Math.max(1, value));
|
|
8
|
+
}
|
|
9
|
+
/** `enabled: false` is the only representation of off — never `seconds: 0` alone. */
|
|
10
|
+
var OFF = {
|
|
11
|
+
enabled: false,
|
|
12
|
+
seconds: 0
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The ONE prebuffer decision.
|
|
16
|
+
*
|
|
17
|
+
* Precedence: explicit per-stream override -> battery / terminal relay (off) ->
|
|
18
|
+
* the profile's warm default -> off. Every layer that used to make this
|
|
19
|
+
* decision somewhere else — `isDefaultWarm`, `applyPreBufferConfig`,
|
|
20
|
+
* `getEffectivePreBufferSec`, `resolvePreRollMs`, `resolveRecordingPreRollMs` —
|
|
21
|
+
* is this function now, so the four of them cannot disagree about one camera.
|
|
22
|
+
*/
|
|
23
|
+
function resolvePrebuffer(input) {
|
|
24
|
+
const override = input.overrides?.[input.camStreamId];
|
|
25
|
+
if (override !== void 0) {
|
|
26
|
+
if (!override.enabled) return OFF;
|
|
27
|
+
const seconds = clampSeconds(override.seconds);
|
|
28
|
+
return seconds > 0 ? {
|
|
29
|
+
enabled: true,
|
|
30
|
+
seconds
|
|
31
|
+
} : OFF;
|
|
32
|
+
}
|
|
33
|
+
if (input.batteryOperated === true) return OFF;
|
|
34
|
+
if (input.terminalRelay === true) return OFF;
|
|
35
|
+
if (input.defaultWarm !== true) return OFF;
|
|
36
|
+
const seconds = clampSeconds(input.clusterSeconds);
|
|
37
|
+
return seconds > 0 ? {
|
|
38
|
+
enabled: true,
|
|
39
|
+
seconds
|
|
40
|
+
} : OFF;
|
|
41
|
+
}
|
|
42
|
+
/** The retention, in ms — the single number the ring is trimmed to. */
|
|
43
|
+
function prebufferRetentionMs(config) {
|
|
44
|
+
return config.enabled ? config.seconds * 1e3 : 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* What a consumer asking for `requestedMs` of history is actually served.
|
|
48
|
+
*
|
|
49
|
+
* The cap is the point of the record: a consumer states a WISH and the ring
|
|
50
|
+
* states the truth.
|
|
51
|
+
*
|
|
52
|
+
* `requestedMs` is REQUIRED, and `0` means none (D382). It used to accept
|
|
53
|
+
* `undefined` for "whatever you have", which is how every detection dial came
|
|
54
|
+
* to be served the whole retained window: detection declares an intent for an
|
|
55
|
+
* unrelated reason (the live-edge exemption) and never named an amount, so the
|
|
56
|
+
* omission decided for it. A parameter that can be left out is a default that
|
|
57
|
+
* acts without anybody choosing it — the shape D381 retired for the retention
|
|
58
|
+
* and this one retires for the ask.
|
|
59
|
+
*
|
|
60
|
+
* A nonsense request resolves to `0`, never to the whole window: an
|
|
61
|
+
* unparseable number arriving on a dial path must not be able to widen a burst.
|
|
62
|
+
*/
|
|
63
|
+
function capPreRollRequestMs(requestedMs, retainedMs) {
|
|
64
|
+
const retained = Number.isFinite(retainedMs) && retainedMs > 0 ? retainedMs : 0;
|
|
65
|
+
if (retained === 0) return 0;
|
|
66
|
+
if (!Number.isFinite(requestedMs) || requestedMs <= 0) return 0;
|
|
67
|
+
return Math.min(requestedMs, retained);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Path suffix for the audio-free variant dialed by a DETECTION decode session.
|
|
71
|
+
* Muted in every respect (no audio, still `isMuted()`), plus exempt from the
|
|
72
|
+
* live-edge join withhold.
|
|
73
|
+
*/
|
|
74
|
+
var DETECTION_MUTED_PATH_SUFFIX = "/muted-detection";
|
|
75
|
+
/**
|
|
76
|
+
* Path suffix for a RECORDING dial — the `events`-band recorder's ffmpeg.
|
|
77
|
+
*
|
|
78
|
+
* NOT muted: a recording keeps its audio, so this cannot ride the detection
|
|
79
|
+
* suffix. What it shares with detection is the reason for existing — it attaches
|
|
80
|
+
* AFTER the trigger, so without the retained history its footage begins after
|
|
81
|
+
* the event. Measured on camera 1438 (audit 2026-08-20): the footage started
|
|
82
|
+
* after the motion in 60 % of episodes, because the writer is spawned at the
|
|
83
|
+
* trigger and ffmpeg's first frame is at best the live edge.
|
|
84
|
+
*/
|
|
85
|
+
var RECORDING_PATH_SUFFIX = "/recording";
|
|
86
|
+
/**
|
|
87
|
+
* Path segment prefix carrying the AMOUNT of retained history a dial asks for.
|
|
88
|
+
*
|
|
89
|
+
* A path segment and not a query string: the listen server matches the request
|
|
90
|
+
* URI with a path regex and strips a trailing `/trackID=N`, so anything that is
|
|
91
|
+
* not a segment would either break the token lookup or be dropped silently
|
|
92
|
+
* between DESCRIBE and SETUP.
|
|
93
|
+
*/
|
|
94
|
+
var PRE_ROLL_REQUEST_PREFIX = "/preroll-";
|
|
95
|
+
/** A dial that declared nothing: no purpose, no history. */
|
|
96
|
+
var NO_DECLARATION = {
|
|
97
|
+
intent: null,
|
|
98
|
+
preRollRequestMs: 0
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* What the DETECTION dial asks for.
|
|
102
|
+
*
|
|
103
|
+
* {@link PREBUFFER_MAX_MS} — "whatever you retain" — which is byte-for-byte
|
|
104
|
+
* what D381 served this population by omission, so the ask becomes explicit
|
|
105
|
+
* without the behaviour changing under an operator who was never asked. It is a
|
|
106
|
+
* REQUEST, not a configuration: `capPreRollRequestMs` answers it with the
|
|
107
|
+
* smaller of this and what the ring holds, and the prebuffer stays the only
|
|
108
|
+
* place the amount of history is decided.
|
|
109
|
+
*
|
|
110
|
+
* Whether detection should ask for LESS is an operator's call on measured
|
|
111
|
+
* numbers, not something a session decides for itself. The per-attach evidence
|
|
112
|
+
* is the broker's `restream attach live-edge` line and the runner's
|
|
113
|
+
* `session-decode: detection first frame`, which now names this request.
|
|
114
|
+
*/
|
|
115
|
+
var DETECTION_PRE_ROLL_REQUEST_MS = PREBUFFER_MAX_MS;
|
|
116
|
+
/**
|
|
117
|
+
* What the RECORDING dial asks for: the whole retained window.
|
|
118
|
+
*
|
|
119
|
+
* Not a tuning knob and not a second ceiling — it is the recorder saying "as
|
|
120
|
+
* much as you kept", the only correct answer for a writer whose keep gate is
|
|
121
|
+
* itself derived from {@link PREBUFFER_MAX_MS} (D381). A narrower request would
|
|
122
|
+
* write a segment that begins after media the broker was holding for it.
|
|
123
|
+
*/
|
|
124
|
+
var RECORDING_PRE_ROLL_REQUEST_MS = PREBUFFER_MAX_MS;
|
|
125
|
+
/**
|
|
126
|
+
* A request the ring could conceivably answer, or `0`.
|
|
127
|
+
*
|
|
128
|
+
* Clamped at {@link PREBUFFER_MAX_MS} — the widest window any prebuffer holds —
|
|
129
|
+
* so a hand-typed or corrupted path cannot name a number the mechanics
|
|
130
|
+
* downstream would then have to defend against. Anything unparseable is `0` and
|
|
131
|
+
* never "everything": an amount nobody can read is not a declaration.
|
|
132
|
+
*/
|
|
133
|
+
function clampRequest(value) {
|
|
134
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
135
|
+
return Math.min(PREBUFFER_MAX_MS, Math.floor(value));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Render an amount as the request segment, or as nothing at all.
|
|
139
|
+
*
|
|
140
|
+
* A zero request appends NOTHING rather than `/preroll-0`: the two mean the
|
|
141
|
+
* same thing, and a path with two spellings is a path with two parsers.
|
|
142
|
+
*/
|
|
143
|
+
function requestSegment(requestMs) {
|
|
144
|
+
const ms = clampRequest(requestMs);
|
|
145
|
+
return ms > 0 ? `${PRE_ROLL_REQUEST_PREFIX}${ms}` : "";
|
|
146
|
+
}
|
|
147
|
+
/** Split a trailing `/preroll-<ms>` off a path: the rest, and the amount. */
|
|
148
|
+
function splitRequest(streamPath) {
|
|
149
|
+
const at = streamPath.lastIndexOf(PRE_ROLL_REQUEST_PREFIX);
|
|
150
|
+
if (at < 0) return {
|
|
151
|
+
rest: streamPath,
|
|
152
|
+
requestMs: 0
|
|
153
|
+
};
|
|
154
|
+
const digits = streamPath.slice(at + 9);
|
|
155
|
+
if (!/^\d+$/.test(digits)) return {
|
|
156
|
+
rest: streamPath.slice(0, at),
|
|
157
|
+
requestMs: 0
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
rest: streamPath.slice(0, at),
|
|
161
|
+
requestMs: clampRequest(Number(digits))
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Split a restream request path (already stripped of `rtsp://host:port/` and of
|
|
166
|
+
* any `/trackID=N` control suffix) into its token, mute bit and declaration.
|
|
167
|
+
*
|
|
168
|
+
* The request segment is read FIRST because it is always last on the wire, and
|
|
169
|
+
* an amount with no intent in front of it is discarded: history is served on
|
|
170
|
+
* the strength of a declared purpose, so a bare number has nothing to attach
|
|
171
|
+
* to and the token keeps whatever it was called.
|
|
172
|
+
*/
|
|
173
|
+
function parseRestreamPath(streamPath) {
|
|
174
|
+
const { rest, requestMs } = splitRequest(streamPath);
|
|
175
|
+
const declare = (intent) => ({
|
|
176
|
+
intent,
|
|
177
|
+
preRollRequestMs: requestMs
|
|
178
|
+
});
|
|
179
|
+
if (rest.endsWith("/recording")) return {
|
|
180
|
+
lookupPath: rest.slice(0, -10),
|
|
181
|
+
muted: false,
|
|
182
|
+
declaration: declare("recording")
|
|
183
|
+
};
|
|
184
|
+
if (rest.endsWith("/muted-detection")) return {
|
|
185
|
+
lookupPath: rest.slice(0, -16),
|
|
186
|
+
muted: true,
|
|
187
|
+
declaration: declare("detection")
|
|
188
|
+
};
|
|
189
|
+
if (rest.endsWith("/muted")) return {
|
|
190
|
+
lookupPath: rest.slice(0, -6),
|
|
191
|
+
muted: true,
|
|
192
|
+
declaration: NO_DECLARATION
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
lookupPath: streamPath,
|
|
196
|
+
muted: false,
|
|
197
|
+
declaration: NO_DECLARATION
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The amount a dial URL asks for, read back off the URL itself.
|
|
202
|
+
*
|
|
203
|
+
* For the LOG on the consumer's side. The runner's
|
|
204
|
+
* `session-decode: detection first frame` line has to be able to say what this
|
|
205
|
+
* attach asked for, and reading it off the string that was actually dialed —
|
|
206
|
+
* rather than off the constant the caller meant to pass — is what makes the
|
|
207
|
+
* line evidence instead of a restatement of intent.
|
|
208
|
+
*/
|
|
209
|
+
function preRollRequestOf(restreamUrl) {
|
|
210
|
+
return splitRequest(restreamUrl).requestMs;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Rewrite an acquired restream URL to declare the detection intent and the
|
|
214
|
+
* history it asks for.
|
|
215
|
+
*
|
|
216
|
+
* `requestMs` is REQUIRED. It used to be absent, and absent meant the whole
|
|
217
|
+
* retained window — the defect D382 removes. `0` is a legitimate, explicit
|
|
218
|
+
* "none": the session still declares detection, which is what lifts the
|
|
219
|
+
* live-edge withhold it dialed this path for in the first place.
|
|
220
|
+
*
|
|
221
|
+
* Only a MUTED url is rewritten. `resolveSourceUrl` falls back to the
|
|
222
|
+
* audio-bearing url when the broker has no muted variant; that session is not
|
|
223
|
+
* muted, the live-edge withhold never applied to it, and appending the suffix
|
|
224
|
+
* would only break the token lookup. Idempotent, amount included.
|
|
225
|
+
*/
|
|
226
|
+
function withDetectionIntent(restreamUrl, requestMs) {
|
|
227
|
+
const { rest } = splitRequest(restreamUrl);
|
|
228
|
+
if (rest.endsWith("/muted-detection")) return `${rest}${requestSegment(requestMs)}`;
|
|
229
|
+
if (!rest.endsWith("/muted")) return restreamUrl;
|
|
230
|
+
return `${rest.slice(0, -6)}${DETECTION_MUTED_PATH_SUFFIX}${requestSegment(requestMs)}`;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Rewrite an acquired restream URL to declare the RECORDING intent and the
|
|
234
|
+
* history it asks for.
|
|
235
|
+
*
|
|
236
|
+
* The mirror image of {@link withDetectionIntent}'s guard: only a NON-muted url
|
|
237
|
+
* is rewritten. A recorder handed the muted variant (a device with no audio, or
|
|
238
|
+
* `audio: 'none'`) is already the detection population's shape, and appending
|
|
239
|
+
* this suffix would only break the token lookup. Idempotent, amount included.
|
|
240
|
+
*/
|
|
241
|
+
function withRecordingIntent(restreamUrl, requestMs) {
|
|
242
|
+
const { rest } = splitRequest(restreamUrl);
|
|
243
|
+
if (rest.endsWith("/muted") || rest.endsWith("/muted-detection")) return restreamUrl;
|
|
244
|
+
if (rest.endsWith("/recording")) return `${rest}${requestSegment(requestMs)}`;
|
|
245
|
+
return `${rest}${RECORDING_PATH_SUFFIX}${requestSegment(requestMs)}`;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
Object.defineProperty(exports, "DETECTION_PRE_ROLL_REQUEST_MS", {
|
|
249
|
+
enumerable: true,
|
|
250
|
+
get: function() {
|
|
251
|
+
return DETECTION_PRE_ROLL_REQUEST_MS;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
Object.defineProperty(exports, "NO_DECLARATION", {
|
|
255
|
+
enumerable: true,
|
|
256
|
+
get: function() {
|
|
257
|
+
return NO_DECLARATION;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
Object.defineProperty(exports, "PREBUFFER_MAX_MS", {
|
|
261
|
+
enumerable: true,
|
|
262
|
+
get: function() {
|
|
263
|
+
return PREBUFFER_MAX_MS;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
Object.defineProperty(exports, "RECORDING_PRE_ROLL_REQUEST_MS", {
|
|
267
|
+
enumerable: true,
|
|
268
|
+
get: function() {
|
|
269
|
+
return RECORDING_PRE_ROLL_REQUEST_MS;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
Object.defineProperty(exports, "capPreRollRequestMs", {
|
|
273
|
+
enumerable: true,
|
|
274
|
+
get: function() {
|
|
275
|
+
return capPreRollRequestMs;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
Object.defineProperty(exports, "parseRestreamPath", {
|
|
279
|
+
enumerable: true,
|
|
280
|
+
get: function() {
|
|
281
|
+
return parseRestreamPath;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
Object.defineProperty(exports, "preRollRequestOf", {
|
|
285
|
+
enumerable: true,
|
|
286
|
+
get: function() {
|
|
287
|
+
return preRollRequestOf;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
Object.defineProperty(exports, "prebufferRetentionMs", {
|
|
291
|
+
enumerable: true,
|
|
292
|
+
get: function() {
|
|
293
|
+
return prebufferRetentionMs;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
Object.defineProperty(exports, "resolvePrebuffer", {
|
|
297
|
+
enumerable: true,
|
|
298
|
+
get: function() {
|
|
299
|
+
return resolvePrebuffer;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
Object.defineProperty(exports, "withDetectionIntent", {
|
|
303
|
+
enumerable: true,
|
|
304
|
+
get: function() {
|
|
305
|
+
return withDetectionIntent;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
Object.defineProperty(exports, "withRecordingIntent", {
|
|
309
|
+
enumerable: true,
|
|
310
|
+
get: function() {
|
|
311
|
+
return withRecordingIntent;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/** {@link PREBUFFER_MAX_SEC} in ms — the widest window any consumer can be served. */
|
|
2
|
+
var PREBUFFER_MAX_MS = 15 * 1e3;
|
|
3
|
+
/** Clamp an enabled window's seconds into `[MIN, MAX]`; `0`/invalid means off. */
|
|
4
|
+
function clampSeconds(value) {
|
|
5
|
+
if (value === void 0) return 5;
|
|
6
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
7
|
+
return Math.min(15, Math.max(1, value));
|
|
8
|
+
}
|
|
9
|
+
/** `enabled: false` is the only representation of off — never `seconds: 0` alone. */
|
|
10
|
+
var OFF = {
|
|
11
|
+
enabled: false,
|
|
12
|
+
seconds: 0
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The ONE prebuffer decision.
|
|
16
|
+
*
|
|
17
|
+
* Precedence: explicit per-stream override -> battery / terminal relay (off) ->
|
|
18
|
+
* the profile's warm default -> off. Every layer that used to make this
|
|
19
|
+
* decision somewhere else — `isDefaultWarm`, `applyPreBufferConfig`,
|
|
20
|
+
* `getEffectivePreBufferSec`, `resolvePreRollMs`, `resolveRecordingPreRollMs` —
|
|
21
|
+
* is this function now, so the four of them cannot disagree about one camera.
|
|
22
|
+
*/
|
|
23
|
+
function resolvePrebuffer(input) {
|
|
24
|
+
const override = input.overrides?.[input.camStreamId];
|
|
25
|
+
if (override !== void 0) {
|
|
26
|
+
if (!override.enabled) return OFF;
|
|
27
|
+
const seconds = clampSeconds(override.seconds);
|
|
28
|
+
return seconds > 0 ? {
|
|
29
|
+
enabled: true,
|
|
30
|
+
seconds
|
|
31
|
+
} : OFF;
|
|
32
|
+
}
|
|
33
|
+
if (input.batteryOperated === true) return OFF;
|
|
34
|
+
if (input.terminalRelay === true) return OFF;
|
|
35
|
+
if (input.defaultWarm !== true) return OFF;
|
|
36
|
+
const seconds = clampSeconds(input.clusterSeconds);
|
|
37
|
+
return seconds > 0 ? {
|
|
38
|
+
enabled: true,
|
|
39
|
+
seconds
|
|
40
|
+
} : OFF;
|
|
41
|
+
}
|
|
42
|
+
/** The retention, in ms — the single number the ring is trimmed to. */
|
|
43
|
+
function prebufferRetentionMs(config) {
|
|
44
|
+
return config.enabled ? config.seconds * 1e3 : 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* What a consumer asking for `requestedMs` of history is actually served.
|
|
48
|
+
*
|
|
49
|
+
* The cap is the point of the record: a consumer states a WISH and the ring
|
|
50
|
+
* states the truth.
|
|
51
|
+
*
|
|
52
|
+
* `requestedMs` is REQUIRED, and `0` means none (D382). It used to accept
|
|
53
|
+
* `undefined` for "whatever you have", which is how every detection dial came
|
|
54
|
+
* to be served the whole retained window: detection declares an intent for an
|
|
55
|
+
* unrelated reason (the live-edge exemption) and never named an amount, so the
|
|
56
|
+
* omission decided for it. A parameter that can be left out is a default that
|
|
57
|
+
* acts without anybody choosing it — the shape D381 retired for the retention
|
|
58
|
+
* and this one retires for the ask.
|
|
59
|
+
*
|
|
60
|
+
* A nonsense request resolves to `0`, never to the whole window: an
|
|
61
|
+
* unparseable number arriving on a dial path must not be able to widen a burst.
|
|
62
|
+
*/
|
|
63
|
+
function capPreRollRequestMs(requestedMs, retainedMs) {
|
|
64
|
+
const retained = Number.isFinite(retainedMs) && retainedMs > 0 ? retainedMs : 0;
|
|
65
|
+
if (retained === 0) return 0;
|
|
66
|
+
if (!Number.isFinite(requestedMs) || requestedMs <= 0) return 0;
|
|
67
|
+
return Math.min(requestedMs, retained);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Path suffix for the audio-free variant dialed by a DETECTION decode session.
|
|
71
|
+
* Muted in every respect (no audio, still `isMuted()`), plus exempt from the
|
|
72
|
+
* live-edge join withhold.
|
|
73
|
+
*/
|
|
74
|
+
var DETECTION_MUTED_PATH_SUFFIX = "/muted-detection";
|
|
75
|
+
/**
|
|
76
|
+
* Path suffix for a RECORDING dial — the `events`-band recorder's ffmpeg.
|
|
77
|
+
*
|
|
78
|
+
* NOT muted: a recording keeps its audio, so this cannot ride the detection
|
|
79
|
+
* suffix. What it shares with detection is the reason for existing — it attaches
|
|
80
|
+
* AFTER the trigger, so without the retained history its footage begins after
|
|
81
|
+
* the event. Measured on camera 1438 (audit 2026-08-20): the footage started
|
|
82
|
+
* after the motion in 60 % of episodes, because the writer is spawned at the
|
|
83
|
+
* trigger and ffmpeg's first frame is at best the live edge.
|
|
84
|
+
*/
|
|
85
|
+
var RECORDING_PATH_SUFFIX = "/recording";
|
|
86
|
+
/**
|
|
87
|
+
* Path segment prefix carrying the AMOUNT of retained history a dial asks for.
|
|
88
|
+
*
|
|
89
|
+
* A path segment and not a query string: the listen server matches the request
|
|
90
|
+
* URI with a path regex and strips a trailing `/trackID=N`, so anything that is
|
|
91
|
+
* not a segment would either break the token lookup or be dropped silently
|
|
92
|
+
* between DESCRIBE and SETUP.
|
|
93
|
+
*/
|
|
94
|
+
var PRE_ROLL_REQUEST_PREFIX = "/preroll-";
|
|
95
|
+
/** A dial that declared nothing: no purpose, no history. */
|
|
96
|
+
var NO_DECLARATION = {
|
|
97
|
+
intent: null,
|
|
98
|
+
preRollRequestMs: 0
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* What the DETECTION dial asks for.
|
|
102
|
+
*
|
|
103
|
+
* {@link PREBUFFER_MAX_MS} — "whatever you retain" — which is byte-for-byte
|
|
104
|
+
* what D381 served this population by omission, so the ask becomes explicit
|
|
105
|
+
* without the behaviour changing under an operator who was never asked. It is a
|
|
106
|
+
* REQUEST, not a configuration: `capPreRollRequestMs` answers it with the
|
|
107
|
+
* smaller of this and what the ring holds, and the prebuffer stays the only
|
|
108
|
+
* place the amount of history is decided.
|
|
109
|
+
*
|
|
110
|
+
* Whether detection should ask for LESS is an operator's call on measured
|
|
111
|
+
* numbers, not something a session decides for itself. The per-attach evidence
|
|
112
|
+
* is the broker's `restream attach live-edge` line and the runner's
|
|
113
|
+
* `session-decode: detection first frame`, which now names this request.
|
|
114
|
+
*/
|
|
115
|
+
var DETECTION_PRE_ROLL_REQUEST_MS = PREBUFFER_MAX_MS;
|
|
116
|
+
/**
|
|
117
|
+
* What the RECORDING dial asks for: the whole retained window.
|
|
118
|
+
*
|
|
119
|
+
* Not a tuning knob and not a second ceiling — it is the recorder saying "as
|
|
120
|
+
* much as you kept", the only correct answer for a writer whose keep gate is
|
|
121
|
+
* itself derived from {@link PREBUFFER_MAX_MS} (D381). A narrower request would
|
|
122
|
+
* write a segment that begins after media the broker was holding for it.
|
|
123
|
+
*/
|
|
124
|
+
var RECORDING_PRE_ROLL_REQUEST_MS = PREBUFFER_MAX_MS;
|
|
125
|
+
/**
|
|
126
|
+
* A request the ring could conceivably answer, or `0`.
|
|
127
|
+
*
|
|
128
|
+
* Clamped at {@link PREBUFFER_MAX_MS} — the widest window any prebuffer holds —
|
|
129
|
+
* so a hand-typed or corrupted path cannot name a number the mechanics
|
|
130
|
+
* downstream would then have to defend against. Anything unparseable is `0` and
|
|
131
|
+
* never "everything": an amount nobody can read is not a declaration.
|
|
132
|
+
*/
|
|
133
|
+
function clampRequest(value) {
|
|
134
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
135
|
+
return Math.min(PREBUFFER_MAX_MS, Math.floor(value));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Render an amount as the request segment, or as nothing at all.
|
|
139
|
+
*
|
|
140
|
+
* A zero request appends NOTHING rather than `/preroll-0`: the two mean the
|
|
141
|
+
* same thing, and a path with two spellings is a path with two parsers.
|
|
142
|
+
*/
|
|
143
|
+
function requestSegment(requestMs) {
|
|
144
|
+
const ms = clampRequest(requestMs);
|
|
145
|
+
return ms > 0 ? `${PRE_ROLL_REQUEST_PREFIX}${ms}` : "";
|
|
146
|
+
}
|
|
147
|
+
/** Split a trailing `/preroll-<ms>` off a path: the rest, and the amount. */
|
|
148
|
+
function splitRequest(streamPath) {
|
|
149
|
+
const at = streamPath.lastIndexOf(PRE_ROLL_REQUEST_PREFIX);
|
|
150
|
+
if (at < 0) return {
|
|
151
|
+
rest: streamPath,
|
|
152
|
+
requestMs: 0
|
|
153
|
+
};
|
|
154
|
+
const digits = streamPath.slice(at + 9);
|
|
155
|
+
if (!/^\d+$/.test(digits)) return {
|
|
156
|
+
rest: streamPath.slice(0, at),
|
|
157
|
+
requestMs: 0
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
rest: streamPath.slice(0, at),
|
|
161
|
+
requestMs: clampRequest(Number(digits))
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Split a restream request path (already stripped of `rtsp://host:port/` and of
|
|
166
|
+
* any `/trackID=N` control suffix) into its token, mute bit and declaration.
|
|
167
|
+
*
|
|
168
|
+
* The request segment is read FIRST because it is always last on the wire, and
|
|
169
|
+
* an amount with no intent in front of it is discarded: history is served on
|
|
170
|
+
* the strength of a declared purpose, so a bare number has nothing to attach
|
|
171
|
+
* to and the token keeps whatever it was called.
|
|
172
|
+
*/
|
|
173
|
+
function parseRestreamPath(streamPath) {
|
|
174
|
+
const { rest, requestMs } = splitRequest(streamPath);
|
|
175
|
+
const declare = (intent) => ({
|
|
176
|
+
intent,
|
|
177
|
+
preRollRequestMs: requestMs
|
|
178
|
+
});
|
|
179
|
+
if (rest.endsWith("/recording")) return {
|
|
180
|
+
lookupPath: rest.slice(0, -10),
|
|
181
|
+
muted: false,
|
|
182
|
+
declaration: declare("recording")
|
|
183
|
+
};
|
|
184
|
+
if (rest.endsWith("/muted-detection")) return {
|
|
185
|
+
lookupPath: rest.slice(0, -16),
|
|
186
|
+
muted: true,
|
|
187
|
+
declaration: declare("detection")
|
|
188
|
+
};
|
|
189
|
+
if (rest.endsWith("/muted")) return {
|
|
190
|
+
lookupPath: rest.slice(0, -6),
|
|
191
|
+
muted: true,
|
|
192
|
+
declaration: NO_DECLARATION
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
lookupPath: streamPath,
|
|
196
|
+
muted: false,
|
|
197
|
+
declaration: NO_DECLARATION
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The amount a dial URL asks for, read back off the URL itself.
|
|
202
|
+
*
|
|
203
|
+
* For the LOG on the consumer's side. The runner's
|
|
204
|
+
* `session-decode: detection first frame` line has to be able to say what this
|
|
205
|
+
* attach asked for, and reading it off the string that was actually dialed —
|
|
206
|
+
* rather than off the constant the caller meant to pass — is what makes the
|
|
207
|
+
* line evidence instead of a restatement of intent.
|
|
208
|
+
*/
|
|
209
|
+
function preRollRequestOf(restreamUrl) {
|
|
210
|
+
return splitRequest(restreamUrl).requestMs;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Rewrite an acquired restream URL to declare the detection intent and the
|
|
214
|
+
* history it asks for.
|
|
215
|
+
*
|
|
216
|
+
* `requestMs` is REQUIRED. It used to be absent, and absent meant the whole
|
|
217
|
+
* retained window — the defect D382 removes. `0` is a legitimate, explicit
|
|
218
|
+
* "none": the session still declares detection, which is what lifts the
|
|
219
|
+
* live-edge withhold it dialed this path for in the first place.
|
|
220
|
+
*
|
|
221
|
+
* Only a MUTED url is rewritten. `resolveSourceUrl` falls back to the
|
|
222
|
+
* audio-bearing url when the broker has no muted variant; that session is not
|
|
223
|
+
* muted, the live-edge withhold never applied to it, and appending the suffix
|
|
224
|
+
* would only break the token lookup. Idempotent, amount included.
|
|
225
|
+
*/
|
|
226
|
+
function withDetectionIntent(restreamUrl, requestMs) {
|
|
227
|
+
const { rest } = splitRequest(restreamUrl);
|
|
228
|
+
if (rest.endsWith("/muted-detection")) return `${rest}${requestSegment(requestMs)}`;
|
|
229
|
+
if (!rest.endsWith("/muted")) return restreamUrl;
|
|
230
|
+
return `${rest.slice(0, -6)}${DETECTION_MUTED_PATH_SUFFIX}${requestSegment(requestMs)}`;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Rewrite an acquired restream URL to declare the RECORDING intent and the
|
|
234
|
+
* history it asks for.
|
|
235
|
+
*
|
|
236
|
+
* The mirror image of {@link withDetectionIntent}'s guard: only a NON-muted url
|
|
237
|
+
* is rewritten. A recorder handed the muted variant (a device with no audio, or
|
|
238
|
+
* `audio: 'none'`) is already the detection population's shape, and appending
|
|
239
|
+
* this suffix would only break the token lookup. Idempotent, amount included.
|
|
240
|
+
*/
|
|
241
|
+
function withRecordingIntent(restreamUrl, requestMs) {
|
|
242
|
+
const { rest } = splitRequest(restreamUrl);
|
|
243
|
+
if (rest.endsWith("/muted") || rest.endsWith("/muted-detection")) return restreamUrl;
|
|
244
|
+
if (rest.endsWith("/recording")) return `${rest}${requestSegment(requestMs)}`;
|
|
245
|
+
return `${rest}${RECORDING_PATH_SUFFIX}${requestSegment(requestMs)}`;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { preRollRequestOf as a, PREBUFFER_MAX_MS as c, resolvePrebuffer as d, parseRestreamPath as i, capPreRollRequestMs as l, NO_DECLARATION as n, withDetectionIntent as o, RECORDING_PRE_ROLL_REQUEST_MS as r, withRecordingIntent as s, DETECTION_PRE_ROLL_REQUEST_MS as t, prebufferRetentionMs as u };
|
|
@@ -323,68 +323,16 @@ function isRecord(value) {
|
|
|
323
323
|
function errText(err) {
|
|
324
324
|
return err instanceof Error ? err.message : String(err);
|
|
325
325
|
}
|
|
326
|
-
/** {@link PREBUFFER_MAX_SEC} in ms — the widest window any consumer can be served. */
|
|
327
|
-
var PREBUFFER_MAX_MS = 15 * 1e3;
|
|
328
|
-
/** Clamp an enabled window's seconds into `[MIN, MAX]`; `0`/invalid means off. */
|
|
329
|
-
function clampSeconds(value) {
|
|
330
|
-
if (value === void 0) return 5;
|
|
331
|
-
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
332
|
-
return Math.min(15, Math.max(1, value));
|
|
333
|
-
}
|
|
334
|
-
/** `enabled: false` is the only representation of off — never `seconds: 0` alone. */
|
|
335
|
-
var OFF = {
|
|
336
|
-
enabled: false,
|
|
337
|
-
seconds: 0
|
|
338
|
-
};
|
|
339
|
-
/**
|
|
340
|
-
* The ONE prebuffer decision.
|
|
341
|
-
*
|
|
342
|
-
* Precedence: explicit per-stream override -> battery / terminal relay (off) ->
|
|
343
|
-
* the profile's warm default -> off. Every layer that used to make this
|
|
344
|
-
* decision somewhere else — `isDefaultWarm`, `applyPreBufferConfig`,
|
|
345
|
-
* `getEffectivePreBufferSec`, `resolvePreRollMs`, `resolveRecordingPreRollMs` —
|
|
346
|
-
* is this function now, so the four of them cannot disagree about one camera.
|
|
347
|
-
*/
|
|
348
|
-
function resolvePrebuffer(input) {
|
|
349
|
-
const override = input.overrides?.[input.camStreamId];
|
|
350
|
-
if (override !== void 0) {
|
|
351
|
-
if (!override.enabled) return OFF;
|
|
352
|
-
const seconds = clampSeconds(override.seconds);
|
|
353
|
-
return seconds > 0 ? {
|
|
354
|
-
enabled: true,
|
|
355
|
-
seconds
|
|
356
|
-
} : OFF;
|
|
357
|
-
}
|
|
358
|
-
if (input.batteryOperated === true) return OFF;
|
|
359
|
-
if (input.terminalRelay === true) return OFF;
|
|
360
|
-
if (input.defaultWarm !== true) return OFF;
|
|
361
|
-
const seconds = clampSeconds(input.clusterSeconds);
|
|
362
|
-
return seconds > 0 ? {
|
|
363
|
-
enabled: true,
|
|
364
|
-
seconds
|
|
365
|
-
} : OFF;
|
|
366
|
-
}
|
|
367
|
-
/** The retention, in ms — the single number the ring is trimmed to. */
|
|
368
|
-
function prebufferRetentionMs(config) {
|
|
369
|
-
return config.enabled ? config.seconds * 1e3 : 0;
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* What a consumer asking for `requestedMs` of history is actually served.
|
|
373
|
-
*
|
|
374
|
-
* The cap is the point of the record: a consumer states a WISH and the ring
|
|
375
|
-
* states the truth. `undefined` means "whatever you have", which is what a
|
|
376
|
-
* consumer that configures nothing asks for — and is why the recorder no
|
|
377
|
-
* longer carries a duration of its own.
|
|
378
|
-
*
|
|
379
|
-
* A nonsense request resolves to `0`, never to the whole window: an
|
|
380
|
-
* unparseable number arriving on a dial path must not be able to widen a burst.
|
|
381
|
-
*/
|
|
382
|
-
function capPreRollRequestMs(requestedMs, retainedMs) {
|
|
383
|
-
const retained = Number.isFinite(retainedMs) && retainedMs > 0 ? retainedMs : 0;
|
|
384
|
-
if (retained === 0) return 0;
|
|
385
|
-
if (requestedMs === void 0) return retained;
|
|
386
|
-
if (!Number.isFinite(requestedMs) || requestedMs <= 0) return 0;
|
|
387
|
-
return Math.min(requestedMs, retained);
|
|
388
|
-
}
|
|
389
326
|
//#endregion
|
|
390
|
-
|
|
327
|
+
Object.defineProperty(exports, "RowMapStore", {
|
|
328
|
+
enumerable: true,
|
|
329
|
+
get: function() {
|
|
330
|
+
return RowMapStore;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
Object.defineProperty(exports, "retireRootKeys", {
|
|
334
|
+
enumerable: true,
|
|
335
|
+
get: function() {
|
|
336
|
+
return retireRootKeys;
|
|
337
|
+
}
|
|
338
|
+
});
|