@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.
Files changed (43) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-B5v8qEI0.js → default-detection-model-BsmxtyZx.js} +1 -1
  4. package/dist/{default-detection-model-CASpAESz.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-BxP0SvPz.js → dist-D2YTTkRf.js} +436 -419
  8. package/dist/{dist-oFIHnD88.mjs → dist-mEsrAAc3.mjs} +436 -419
  9. package/dist/{lazy-sharp-VKOG3WW4.js → lazy-sharp-B5bW34uC.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-B4zETsJ9.mjs → node-CUxep5tc.mjs} +1 -1
  13. package/dist/{node-BIKrNEtS.js → node-x3YokJkD.js} +1 -1
  14. package/dist/pipeline-runner/index.js +8 -7
  15. package/dist/pipeline-runner/index.mjs +7 -6
  16. package/dist/{process-memory-D9xYwm6L.mjs → process-memory-BCqpTruN.mjs} +1 -1
  17. package/dist/{process-memory-CMHOmj75.js → process-memory-CFc4D9A2.js} +1 -1
  18. package/dist/recorder/index.js +169 -334
  19. package/dist/recorder/index.mjs +164 -329
  20. package/dist/restream-intent-B1Difcnq.js +313 -0
  21. package/dist/restream-intent-DWe2kInY.mjs +248 -0
  22. package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
  23. package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
  24. package/dist/{segment-demux-js-DLwL1vmq.js → segment-demux-js-DUZVURQd.js} +1 -1
  25. package/dist/{segment-demux-js-DZysrgdC.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
  26. package/dist/session-decode/decode-worker-child.js +25 -5
  27. package/dist/session-decode/decode-worker-child.mjs +24 -4
  28. package/dist/stream-broker/_stub.js +2 -2
  29. 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
  30. 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
  31. 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
  32. package/dist/stream-broker/demux-worker-child.js +1 -1
  33. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  34. package/dist/stream-broker/{hostInit-C8tu7NS4.mjs → hostInit-D_cZQhBA.mjs} +3 -3
  35. package/dist/stream-broker/index.js +181 -48
  36. package/dist/stream-broker/index.mjs +172 -39
  37. package/dist/stream-broker/remoteEntry.js +1 -1
  38. package/dist/{worker-protocol-D2mMjUXv.js → worker-protocol-CRDaBlJl.js} +1 -1
  39. package/dist/{worker-protocol-DFlZm_wr.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
  40. package/package.json +1 -1
  41. package/dist/restream-intent-B4BXZra7.mjs +0 -72
  42. package/dist/restream-intent-Cv9x3jmu.js +0 -89
  43. 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
- export { retireRootKeys as a, resolvePrebuffer as i, capPreRollRequestMs as n, RowMapStore as o, prebufferRetentionMs as r, PREBUFFER_MAX_MS as t };
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
+ });