@camstack/addon-pipeline 1.2.142 → 1.2.144
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/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-3bEUjzHN.js → dist-Dpy1KP1q.js} +63 -29
- package/dist/{dist-B25xMJDp.mjs → dist-uX-YDOrc.mjs} +64 -24
- package/dist/{event-loop-stall-monitor-CrWg3jrh.mjs → event-loop-stall-monitor-6YKQeg7_.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-BQeD-JuB.js → event-loop-stall-monitor-D947d97M.js} +1 -1
- package/dist/{lazy-sharp-buaVXzJ6.js → lazy-sharp-zDl97I3e.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-CFEV-qgx.mjs → node-DuMWMV01.mjs} +1 -1
- package/dist/{node-Cn9gEOs4.js → node-v9ZAGK2b.js} +1 -1
- package/dist/pipeline-runner/index.js +5 -5
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-B4SGuNSB.mjs → process-memory-CNISjywZ.mjs} +1 -1
- package/dist/{process-memory-D7R-j96w.js → process-memory-ClAfQ1Nl.js} +1 -1
- package/dist/recorder/index.js +337 -72
- package/dist/recorder/index.mjs +337 -72
- package/dist/{segment-demux-js-J1AdcHSK.js → segment-demux-js-Bpedn-T8.js} +1 -1
- package/dist/{segment-demux-js-CwNYlHRC.mjs → segment-demux-js-CCMG51zw.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/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-O4QuB94V.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-MRLUZgkR.mjs} +3 -3
- 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-DXcoBCs4.mjs → hostInit-w0ytBmjx.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DYOG2ris.mjs → worker-protocol-Cd-G92Qt.mjs} +1 -1
- package/dist/{worker-protocol-CxvZ6FP4.js → worker-protocol-Cn5x8dbW.js} +1 -1
- package/package.json +1 -1
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
1
|
+
import { $ as isFirstLevelMacroClass, $t as record, B as defineCustomActions, Bt as nodePin, E as RecordingConfigSchema, It as hydrateSchema, J as evaluateSensorEdge, Kt as array, L as customAction, N as batteryCapability, Nt as DeviceFeature, Pt as DeviceType, Qt as object, T as RECORDING_EXPORT_MAX_READ_BYTES, U as deriveRecordingMode, Ut as selectAssignedProfileSlots, V as deriveBatteryPresence, Zt as number, bt as storageEvictableCapability, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as string, et as loadContributionCapability, g as ExportRecordSchema, i as BatteryStatusSchema, jt as BaseAddon, mt as recordingExportCapability, nn as EventCategory, pt as recordingCapability, qt as boolean, vt as resolveRecordingProfiles, wt as errMsg, x as OpsLogEntrySchema } from "../dist-uX-YDOrc.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-DuMWMV01.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
5
5
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DMolfhsP.mjs";
|
|
@@ -10,26 +10,41 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { open } from "node:fs/promises";
|
|
11
11
|
import { contentTypeFor, createFileDataPlaneHandler, parseRangeHeader } from "@camstack/system/addon-utils";
|
|
12
12
|
//#region src/recorder/event-capture.ts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
/**
|
|
14
|
+
* La sottoscrizione motion del recorder: bus → `onTrigger(deviceId, atMs)`.
|
|
15
|
+
*
|
|
16
|
+
* ## Cosa NON c'è più qui (2026-08-29)
|
|
17
|
+
*
|
|
18
|
+
* Fino al 2026-08-29 questo file costruiva anche un `EventMarker` per evento e
|
|
19
|
+
* lo spingeva in un `EventMap` per-device — annotazioni di playback in RAM, con
|
|
20
|
+
* dentro il payload `regions` grezzo del provider. `EventMap.events()` e
|
|
21
|
+
* `EventMap.prune()` non avevano **nessun chiamante in tutto il repo**: la
|
|
22
|
+
* mappa veniva solo scritta, e cresceva con l'uptime del runner.
|
|
23
|
+
*
|
|
24
|
+
* È stata cancellata, non cablata, e la ragione è nel suo stesso header: la
|
|
25
|
+
* timeline che l'operatore vede viene dalle righe durabili di
|
|
26
|
+
* `pipelineAnalytics` (`getMotionEvents`/`getObjectEvents`/`getAudioEvents`), i
|
|
27
|
+
* marker non erano esposti su nessuna cap (`recording.getPlaybackManifest`
|
|
28
|
+
* restituisce solo URL di playlist) e ADR-0194 aveva già rifiutato
|
|
29
|
+
* esplicitamente di costruirci sopra il reconcile dei trigger, perché una
|
|
30
|
+
* struttura in RAM non sopravvive né alla perdita dell'evento né al respawn del
|
|
31
|
+
* runner. Un lettore non è mai esistito e non ne era previsto uno: tenerla
|
|
32
|
+
* sarebbe stato "un residuo che descrive il design giusto", cioè una verifica
|
|
33
|
+
* che non verifica niente.
|
|
34
|
+
*
|
|
35
|
+
* Quello che resta è l'unica cosa che qualcuno leggeva davvero: il fronte di
|
|
36
|
+
* salita della motion che apre una finestra della banda `events` (B3).
|
|
37
|
+
*/
|
|
38
|
+
/** Solo il fronte `detected: true` è un trigger. Il fronte di discesa non apre
|
|
39
|
+
* niente — la finestra la chiude il post-buffer, non l'evento. */
|
|
40
|
+
function isMotionTrigger(data) {
|
|
41
|
+
return data.detected;
|
|
22
42
|
}
|
|
23
43
|
function subscribeEventCapture(deps) {
|
|
24
|
-
const { eventBus,
|
|
44
|
+
const { eventBus, onMotion } = deps;
|
|
25
45
|
return eventBus.subscribe({ category: EventCategory.MotionOnMotionChanged }, (event) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
padPostMs
|
|
29
|
-
});
|
|
30
|
-
if (!mk) return;
|
|
31
|
-
map.record(event.data.deviceId, mk);
|
|
32
|
-
onMotion?.(event.data.deviceId, event.data.timestamp);
|
|
46
|
+
if (!isMotionTrigger(event.data)) return;
|
|
47
|
+
onMotion(event.data.deviceId, event.data.timestamp);
|
|
33
48
|
});
|
|
34
49
|
}
|
|
35
50
|
//#endregion
|
|
@@ -46,44 +61,180 @@ function subscribeEventCapture(deps) {
|
|
|
46
61
|
* Puro: nessun bus, nessun orologio, nessuna config. La scelta di QUALI classi
|
|
47
62
|
* ascoltare è della banda (`bandTriggerSources`); qui si decide solo se
|
|
48
63
|
* l'evento è una detection utilizzabile e di che macro classe è.
|
|
49
|
-
|
|
64
|
+
*
|
|
65
|
+
* ## Perché la forma è DISCRIMINATA su `kind` (2026-08-29)
|
|
66
|
+
*
|
|
67
|
+
* Fino al 2026-08-29 questo file dichiarava UNA sola forma piatta — quella
|
|
68
|
+
* dell'evento `object` — e la pretendeva da OGNI payload del topic. Ma
|
|
69
|
+
* `EventCategory.PipelineAnalyticsDetectionEvent` è un topic condiviso da tre
|
|
70
|
+
* emettitori in `addon-post-analysis/src/pipeline-analytics/index.ts`: object
|
|
71
|
+
* (`className`, `source`, `confidence`, `zones`), motion e audio — e motion e
|
|
72
|
+
* audio non portano `className`, perché non ne hanno una. Il risultato misurato:
|
|
73
|
+
* `malformed` su **394 righe campionate su 394**, a 3.3–3.9 eventi/s, per
|
|
74
|
+
* settimane. `malformed` significa "l'emettitore è andato in deriva" ed era
|
|
75
|
+
* invece il traffico normale del topic; l'unico secchio che poteva segnalare
|
|
76
|
+
* una vera rottura di protocollo era saturo al 100% di eventi sani, quindi
|
|
77
|
+
* inservibile — ed è stato letto come "il trigger object non ha mai funzionato".
|
|
78
|
+
*
|
|
79
|
+
* Quindi: si valida prima la BUSTA comune (`deviceId`, `kind`, `timestamp`),
|
|
80
|
+
* poi si discrimina su `kind`, e solo il ramo `object` pretende i campi
|
|
81
|
+
* dell'object. Un `object` senza `className` resta un difetto e ha il suo nome
|
|
82
|
+
* (`malformed-object`), distinto da una busta illeggibile.
|
|
83
|
+
*/
|
|
84
|
+
/** Il `kind` dell'unico ramo del topic che può aprire una finestra. */
|
|
85
|
+
var OBJECT_EVENT_KIND = "object";
|
|
86
|
+
/** La sorgente implicita delle righe legacy senza `source` (`DetectionSourceSchema`). */
|
|
87
|
+
var DEFAULT_DETECTION_SOURCE = "pipeline";
|
|
50
88
|
/** Type guard, non cast: `event.data` è `unknown` finché non passa di qui. */
|
|
51
|
-
function
|
|
89
|
+
function isDetectionEventEnvelope(value) {
|
|
52
90
|
if (typeof value !== "object" || value === null) return false;
|
|
53
91
|
if (!("deviceId" in value) || typeof value.deviceId !== "number") return false;
|
|
92
|
+
if (!Number.isFinite(value.deviceId)) return false;
|
|
54
93
|
if (!("kind" in value) || typeof value.kind !== "string") return false;
|
|
55
94
|
if (!("timestamp" in value) || typeof value.timestamp !== "number") return false;
|
|
56
95
|
if (!Number.isFinite(value.timestamp) || value.timestamp <= 0) return false;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/** I campi che SOLO il ramo object porta. Chiamata dopo la busta. */
|
|
99
|
+
function isObjectDetectionEventData(value) {
|
|
57
100
|
if (!("className" in value) || typeof value.className !== "string") return false;
|
|
58
101
|
if ("source" in value && value.source !== void 0 && typeof value.source !== "string") return false;
|
|
59
102
|
return true;
|
|
60
103
|
}
|
|
61
104
|
function qualifyObjectTrigger(data) {
|
|
62
|
-
if (!
|
|
105
|
+
if (!isDetectionEventEnvelope(data)) return {
|
|
63
106
|
qualified: false,
|
|
64
107
|
reason: "malformed"
|
|
65
108
|
};
|
|
66
|
-
|
|
109
|
+
const deviceId = data.deviceId;
|
|
110
|
+
if (data.kind !== OBJECT_EVENT_KIND) return {
|
|
111
|
+
qualified: false,
|
|
112
|
+
reason: "not-object",
|
|
113
|
+
deviceId
|
|
114
|
+
};
|
|
115
|
+
if (!isObjectDetectionEventData(data)) return {
|
|
67
116
|
qualified: false,
|
|
68
|
-
reason: "
|
|
117
|
+
reason: "malformed-object",
|
|
118
|
+
deviceId
|
|
69
119
|
};
|
|
70
|
-
if ((data.source ??
|
|
120
|
+
if ((data.source ?? DEFAULT_DETECTION_SOURCE) !== DEFAULT_DETECTION_SOURCE) return {
|
|
71
121
|
qualified: false,
|
|
72
|
-
reason: "not-pipeline"
|
|
122
|
+
reason: "not-pipeline",
|
|
123
|
+
deviceId
|
|
73
124
|
};
|
|
74
125
|
const className = data.className.trim().toLowerCase();
|
|
75
126
|
if (!isFirstLevelMacroClass(className)) return {
|
|
76
127
|
qualified: false,
|
|
77
|
-
reason: "not-a-macro"
|
|
128
|
+
reason: "not-a-macro",
|
|
129
|
+
deviceId
|
|
78
130
|
};
|
|
79
131
|
return {
|
|
80
132
|
qualified: true,
|
|
81
|
-
deviceId
|
|
133
|
+
deviceId,
|
|
82
134
|
macroClass: className,
|
|
83
135
|
atMs: data.timestamp
|
|
84
136
|
};
|
|
85
137
|
}
|
|
86
138
|
//#endregion
|
|
139
|
+
//#region src/recorder/object-trigger-health.ts
|
|
140
|
+
/** Ampiezza della finestra di aggregazione. Un minuto: abbastanza corto perché
|
|
141
|
+
* la riga arrivi entro il primo minuto di un deploy rotto, abbastanza lungo
|
|
142
|
+
* perché a 4 eventi/s il campione sia significativo. */
|
|
143
|
+
var OBJECT_TRIGGER_HEALTH_WINDOW_MS = 6e4;
|
|
144
|
+
function emptyTally() {
|
|
145
|
+
return {
|
|
146
|
+
objectEvents: 0,
|
|
147
|
+
qualified: 0,
|
|
148
|
+
otherKinds: 0,
|
|
149
|
+
byReason: /* @__PURE__ */ new Map()
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function dominantReason(byReason) {
|
|
153
|
+
let reason = null;
|
|
154
|
+
let best = 0;
|
|
155
|
+
const counts = {};
|
|
156
|
+
for (const [r, n] of byReason) {
|
|
157
|
+
counts[r] = n;
|
|
158
|
+
if (n > best) {
|
|
159
|
+
best = n;
|
|
160
|
+
reason = r;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
reason,
|
|
165
|
+
counts
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
var ObjectTriggerHealth = class {
|
|
169
|
+
windowMs;
|
|
170
|
+
minSample;
|
|
171
|
+
buckets = /* @__PURE__ */ new Map();
|
|
172
|
+
windowStartMs = null;
|
|
173
|
+
constructor(windowMs = OBJECT_TRIGGER_HEALTH_WINDOW_MS, minSample = 5) {
|
|
174
|
+
this.windowMs = windowMs;
|
|
175
|
+
this.minSample = minSample;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Registra un verdetto e, se la finestra si è chiusa con questo evento,
|
|
179
|
+
* restituisce i report da stampare (e riapre la finestra). Nessun timer:
|
|
180
|
+
* il feed viaggia a ~4/s, quindi la chiusura arriva sempre in tempo, e una
|
|
181
|
+
* finestra che non si chiude è una finestra in cui non è successo nulla.
|
|
182
|
+
*/
|
|
183
|
+
record(verdict, nowMs) {
|
|
184
|
+
const reports = this.windowStartMs !== null && nowMs - this.windowStartMs >= this.windowMs ? this.drain(nowMs) : [];
|
|
185
|
+
this.windowStartMs ??= nowMs;
|
|
186
|
+
this.count(verdict);
|
|
187
|
+
return reports;
|
|
188
|
+
}
|
|
189
|
+
count(verdict) {
|
|
190
|
+
if (verdict.qualified) {
|
|
191
|
+
const t = this.bucket(verdict.deviceId);
|
|
192
|
+
t.objectEvents += 1;
|
|
193
|
+
t.qualified += 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (verdict.reason === "not-object") {
|
|
197
|
+
this.bucket(verdict.deviceId).otherKinds += 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const key = verdict.reason === "malformed" ? null : verdict.deviceId;
|
|
201
|
+
const t = this.bucket(key);
|
|
202
|
+
t.objectEvents += 1;
|
|
203
|
+
t.byReason.set(verdict.reason, (t.byReason.get(verdict.reason) ?? 0) + 1);
|
|
204
|
+
}
|
|
205
|
+
bucket(key) {
|
|
206
|
+
const existing = this.buckets.get(key);
|
|
207
|
+
if (existing !== void 0) return existing;
|
|
208
|
+
const fresh = emptyTally();
|
|
209
|
+
this.buckets.set(key, fresh);
|
|
210
|
+
return fresh;
|
|
211
|
+
}
|
|
212
|
+
/** Chiude la finestra e restituisce un report per secchio non vuoto. */
|
|
213
|
+
drain(nowMs) {
|
|
214
|
+
const windowMs = this.windowStartMs === null ? 0 : nowMs - this.windowStartMs;
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const [deviceId, t] of this.buckets) {
|
|
217
|
+
if (t.objectEvents === 0 && t.otherKinds === 0) continue;
|
|
218
|
+
const rejected = t.objectEvents - t.qualified;
|
|
219
|
+
const { reason, counts } = dominantReason(t.byReason);
|
|
220
|
+
out.push({
|
|
221
|
+
deviceId,
|
|
222
|
+
windowMs,
|
|
223
|
+
objectEvents: t.objectEvents,
|
|
224
|
+
qualified: t.qualified,
|
|
225
|
+
rejectedPct: t.objectEvents === 0 ? 0 : Math.round(rejected / t.objectEvents * 100),
|
|
226
|
+
otherKinds: t.otherKinds,
|
|
227
|
+
topReason: reason,
|
|
228
|
+
byReason: counts,
|
|
229
|
+
totalRejection: t.objectEvents >= this.minSample && t.qualified === 0
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
this.buckets.clear();
|
|
233
|
+
this.windowStartMs = nowMs;
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
//#endregion
|
|
87
238
|
//#region src/recorder/sensor-trigger.ts
|
|
88
239
|
/**
|
|
89
240
|
* Da `DeviceStateChanged` a "quali camere devono registrare".
|
|
@@ -212,31 +363,6 @@ var SensorTriggerRouter = class {
|
|
|
212
363
|
}
|
|
213
364
|
};
|
|
214
365
|
//#endregion
|
|
215
|
-
//#region src/recorder/event-map.ts
|
|
216
|
-
var EventMap = class {
|
|
217
|
-
byDevice = /* @__PURE__ */ new Map();
|
|
218
|
-
record(deviceId, marker) {
|
|
219
|
-
const list = this.byDevice.get(deviceId) ?? [];
|
|
220
|
-
list.push({ ...marker });
|
|
221
|
-
this.byDevice.set(deviceId, list);
|
|
222
|
-
}
|
|
223
|
-
events(deviceId, fromMs, toMs) {
|
|
224
|
-
const list = this.byDevice.get(deviceId);
|
|
225
|
-
if (!list) return [];
|
|
226
|
-
return list.filter((e) => e.t >= fromMs && e.t <= toMs).toSorted((a, b) => a.t - b.t);
|
|
227
|
-
}
|
|
228
|
-
prune(nowMs, maxAgeMs) {
|
|
229
|
-
const cutoff = nowMs - maxAgeMs;
|
|
230
|
-
let dropped = 0;
|
|
231
|
-
for (const [deviceId, list] of this.byDevice) {
|
|
232
|
-
const kept = list.filter((e) => e.t >= cutoff);
|
|
233
|
-
dropped += list.length - kept.length;
|
|
234
|
-
this.byDevice.set(deviceId, kept);
|
|
235
|
-
}
|
|
236
|
-
return dropped;
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
//#endregion
|
|
240
366
|
//#region src/recorder/init-info.ts
|
|
241
367
|
/** Walk sibling boxes in [from, to), yielding [type, start, end]. */
|
|
242
368
|
function* boxes$1(buf, view, from, to) {
|
|
@@ -8597,6 +8723,47 @@ var EPOCH_NAME_RE = /^(\d+)\.m4s$/;
|
|
|
8597
8723
|
*/
|
|
8598
8724
|
var PENDING_GRACE_MS = 3e4;
|
|
8599
8725
|
/**
|
|
8726
|
+
* How many entries ffmpeg's `-segment_list` must retain — the argument for
|
|
8727
|
+
* `-segment_list_size`.
|
|
8728
|
+
*
|
|
8729
|
+
* ## Why the list is bounded at all
|
|
8730
|
+
*
|
|
8731
|
+
* It was not, and the muxer default (0) means "keep every segment for the
|
|
8732
|
+
* writer's whole life". `parseLivePlaylist` re-splits and re-objectifies the
|
|
8733
|
+
* WHOLE body on every 2 s tick, per writer: at 10 s segments a 5-hour writer
|
|
8734
|
+
* holds 1 800 entries ⇒ ~220 KB/tick ⇒ **3.3 MB/s across 30 writers**, growing
|
|
8735
|
+
* linearly with uptime and bounded by nothing. It is latent today only because
|
|
8736
|
+
* writers restart constantly (59 restarts in 16 minutes, measured 2026-08-29)
|
|
8737
|
+
* and ffmpeg truncates the list on every spawn.
|
|
8738
|
+
*
|
|
8739
|
+
* ## Why THIS size
|
|
8740
|
+
*
|
|
8741
|
+
* The size is what the PARSER has to be able to answer, and the parser has
|
|
8742
|
+
* exactly two questions open at any moment:
|
|
8743
|
+
*
|
|
8744
|
+
* 1. the live tail, which is deliberately held back until its successor
|
|
8745
|
+
* exists ({@link correctedDurMs}) — **1 entry**;
|
|
8746
|
+
* 2. every entry whose flat file has not landed yet. That entry stays this
|
|
8747
|
+
* watcher's business for at most {@link PENDING_GRACE_MS}, after which the
|
|
8748
|
+
* pass gives up on it and advances — and the pass that gives up may be as
|
|
8749
|
+
* far as {@link WATCHER_PASS_WORST_MS} behind the one that first saw it.
|
|
8750
|
+
*
|
|
8751
|
+
* So the window is `1 + (grace + worst pass) / segment length`, rounded up.
|
|
8752
|
+
* At the default 10 s segments that is 1 + ceil(132/10) = **15 entries**
|
|
8753
|
+
* (~2.5 minutes of playlist, ~1.8 KB) instead of an unbounded list. A shorter
|
|
8754
|
+
* segment length buys proportionally more entries, which is exactly right: the
|
|
8755
|
+
* bound is denominated in TIME, and the entry count is only how that time is
|
|
8756
|
+
* spelled for a given `-segment_time`.
|
|
8757
|
+
*
|
|
8758
|
+
* Undersizing does not lose footage — a segment that rolls off unhandled stays
|
|
8759
|
+
* flat in staging and `staging-reconcile` recovers it at the next boot — but it
|
|
8760
|
+
* delays it by a whole process lifetime, so the tick loop reports it
|
|
8761
|
+
* ({@link FinalizationPlan.rolledOff}) rather than advancing in silence.
|
|
8762
|
+
*/
|
|
8763
|
+
function segmentListSize(segmentSeconds) {
|
|
8764
|
+
return 1 + Math.ceil(132e3 / ((segmentSeconds > 0 ? segmentSeconds : 1) * 1e3));
|
|
8765
|
+
}
|
|
8766
|
+
/**
|
|
8600
8767
|
* Decide whether a missing flat file is a late flush or a previous run's entry.
|
|
8601
8768
|
*
|
|
8602
8769
|
* ffmpeg opens `-segment_list` in TRUNCATE mode when it spawns, but not
|
|
@@ -8651,6 +8818,27 @@ function correctedDurMs(durSeconds, startMs, nextStartMs) {
|
|
|
8651
8818
|
if (gapMs <= 0) return extinfMs;
|
|
8652
8819
|
return Math.min(extinfMs, gapMs);
|
|
8653
8820
|
}
|
|
8821
|
+
var MEDIA_SEQUENCE_RE = /^#EXT-X-MEDIA-SEQUENCE:(\d+)/m;
|
|
8822
|
+
/**
|
|
8823
|
+
* How many entries ffmpeg has already dropped off the FRONT of this playlist.
|
|
8824
|
+
*
|
|
8825
|
+
* With `-segment_list_size` the list is a sliding window, so an index into the
|
|
8826
|
+
* body is not a stable name for a segment: index 0 means a different file every
|
|
8827
|
+
* time the window advances. `#EXT-X-MEDIA-SEQUENCE` is the offset that turns a
|
|
8828
|
+
* body index back into a monotone global one, and ffmpeg maintains it for
|
|
8829
|
+
* exactly this purpose — verified against the ffmpeg 6.1.1 build the hub image
|
|
8830
|
+
* ships (12 segments, `-segment_list_size 4` ⇒ 4 entries and
|
|
8831
|
+
* `#EXT-X-MEDIA-SEQUENCE:8`; unbounded ⇒ 12 entries and `:0`).
|
|
8832
|
+
*
|
|
8833
|
+
* Absent (an unbounded list, or a pre-truncation body) it is 0, which is the
|
|
8834
|
+
* old behaviour exactly.
|
|
8835
|
+
*/
|
|
8836
|
+
function parseMediaSequence(body) {
|
|
8837
|
+
const m = MEDIA_SEQUENCE_RE.exec(body);
|
|
8838
|
+
if (m?.[1] === void 0) return 0;
|
|
8839
|
+
const n = Number(m[1]);
|
|
8840
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
8841
|
+
}
|
|
8654
8842
|
/** Parse a `live.m3u8` body into ordered `#EXTINF` → segment-path pairs. */
|
|
8655
8843
|
function parseLivePlaylist(body) {
|
|
8656
8844
|
const lines = body.split("\n");
|
|
@@ -8728,14 +8916,30 @@ async function handleSegmentEntry(outDir, entry, durMs, onFinalized, logger, wri
|
|
|
8728
8916
|
* duration needs its SUCCESSOR's start (ffmpeg's first-segment EXTINF is
|
|
8729
8917
|
* inflated by the initial PTS offset). Once `#EXT-X-ENDLIST` appears the tail
|
|
8730
8918
|
* is flushed too — by then it is never the unreliable first segment.
|
|
8919
|
+
*
|
|
8920
|
+
* ## The mark is GLOBAL, not an index into the body
|
|
8921
|
+
*
|
|
8922
|
+
* `-segment_list_size` (see {@link segmentListSize}) makes the playlist a
|
|
8923
|
+
* sliding window, and the moment it starts sliding a body index stops naming a
|
|
8924
|
+
* segment: entry 0 is a different file after every roll. A high-water mark kept
|
|
8925
|
+
* as a body index would sit at `entries.length - 1` forever and finalize
|
|
8926
|
+
* NOTHING while ffmpeg kept writing — a bounded list without this rebase would
|
|
8927
|
+
* have stopped the recorder indexing footage while looking like a memory fix.
|
|
8928
|
+
* So the mark counts segments since the writer's first, `#EXT-X-MEDIA-SEQUENCE`
|
|
8929
|
+
* supplies the offset, and an unbounded list (sequence 0) behaves exactly as
|
|
8930
|
+
* before.
|
|
8731
8931
|
*/
|
|
8732
8932
|
function planFinalizations(body, processed, forceFinal = false) {
|
|
8733
8933
|
const entries = parseLivePlaylist(body);
|
|
8734
8934
|
const endlist = body.includes("#EXT-X-ENDLIST");
|
|
8735
|
-
const
|
|
8736
|
-
const
|
|
8935
|
+
const base = parseMediaSequence(body);
|
|
8936
|
+
const restarted = base + entries.length < processed;
|
|
8937
|
+
const rolledOff = restarted ? 0 : Math.max(0, base - processed);
|
|
8938
|
+
const hw = restarted ? base : Math.max(processed, base);
|
|
8939
|
+
const finalizable = base + (endlist || forceFinal ? entries.length : Math.max(0, entries.length - 1));
|
|
8737
8940
|
const toHandle = [];
|
|
8738
|
-
for (let
|
|
8941
|
+
for (let g = hw; g < finalizable; g++) {
|
|
8942
|
+
const i = g - base;
|
|
8739
8943
|
const entry = entries[i];
|
|
8740
8944
|
const startMs = parseEpochStartMs(entry.segPath);
|
|
8741
8945
|
const nextStartMs = i + 1 < entries.length ? parseEpochStartMs(entries[i + 1].segPath) : null;
|
|
@@ -8748,7 +8952,8 @@ function planFinalizations(body, processed, forceFinal = false) {
|
|
|
8748
8952
|
return {
|
|
8749
8953
|
toHandle,
|
|
8750
8954
|
from: hw,
|
|
8751
|
-
processed: Math.max(hw, finalizable)
|
|
8955
|
+
processed: Math.max(hw, finalizable),
|
|
8956
|
+
rolledOff
|
|
8752
8957
|
};
|
|
8753
8958
|
}
|
|
8754
8959
|
/**
|
|
@@ -8776,6 +8981,11 @@ function startSegmentWatcher(deps) {
|
|
|
8776
8981
|
return;
|
|
8777
8982
|
}
|
|
8778
8983
|
const plan = planFinalizations(body, processed, forceFinal);
|
|
8984
|
+
if (plan.rolledOff > 0) deps.logger.warn("segment playlist rolled past entries this watcher had not handled", { meta: {
|
|
8985
|
+
rolledOff: plan.rolledOff,
|
|
8986
|
+
processed,
|
|
8987
|
+
from: plan.from
|
|
8988
|
+
} });
|
|
8779
8989
|
let index = plan.from;
|
|
8780
8990
|
for (const { entry, durMs } of plan.toHandle) {
|
|
8781
8991
|
if (await handleSegmentEntry(deps.outDir, entry, durMs, deps.onFinalized, deps.logger, deps.writerStartedMs) === "pending") {
|
|
@@ -8840,6 +9050,23 @@ function startSegmentWatcher(deps) {
|
|
|
8840
9050
|
}
|
|
8841
9051
|
//#endregion
|
|
8842
9052
|
//#region src/recorder/addon/ffmpeg-args.ts
|
|
9053
|
+
/**
|
|
9054
|
+
* ffmpeg argv builder for recorder passthrough segmentation.
|
|
9055
|
+
*
|
|
9056
|
+
* Copied from the old recorder (`addon-pipeline/src/recorder/ffmpeg-args.ts`)
|
|
9057
|
+
* essentially verbatim — the passthrough + Opus-audio + fragmented-mp4 segment
|
|
9058
|
+
* recipe is proven across every vendor. The ONLY behavioural difference from
|
|
9059
|
+
* the old recorder lives in the WATCHER (recorder relocates via the v2
|
|
9060
|
+
* `SegmentStore.onFinalized`, not inline) — the ffmpeg invocation is identical:
|
|
9061
|
+
* FLAT epoch-named `.m4s` segments under `outDir` plus a rolling `live.m3u8`.
|
|
9062
|
+
*
|
|
9063
|
+
* Audio is re-encoded to **Opus** (`-c:a libopus`, in an `Opus`/`dOps`
|
|
9064
|
+
* ISOBMFF sample entry, verified muxable + mp4box-readable on the ffmpeg 7.x
|
|
9065
|
+
* builds shipped in dev/hub/agent images). Opus is what the WebRTC audio sender
|
|
9066
|
+
* already passes through un-transcoded (live PCMU/PCMA/OPUS => copy), so a
|
|
9067
|
+
* recorded fragment's audio can be read in-process (mp4box) and pushed straight
|
|
9068
|
+
* to the Opus sender with no per-fragment ffmpeg spawn on playback.
|
|
9069
|
+
*/
|
|
8843
9070
|
/** ffmpeg argv: pull RTSP (TCP), copy video / transcode audio to Opus, write
|
|
8844
9071
|
* fragmented-mp4 segments named by epoch seconds FLAT under `outDir`, plus a
|
|
8845
9072
|
* rolling `live.m3u8` playlist. The recorder segment-watcher tails the
|
|
@@ -8878,6 +9105,8 @@ function buildPassthroughArgs(a) {
|
|
|
8878
9105
|
`${a.outDir}/live.m3u8`,
|
|
8879
9106
|
"-segment_list_type",
|
|
8880
9107
|
"m3u8",
|
|
9108
|
+
"-segment_list_size",
|
|
9109
|
+
String(segmentListSize(a.segmentSeconds)),
|
|
8881
9110
|
`${a.outDir}/%s.m4s`
|
|
8882
9111
|
];
|
|
8883
9112
|
}
|
|
@@ -12564,15 +12793,6 @@ var DEFAULT_CONFIG = {
|
|
|
12564
12793
|
recordingNodeId: RECORDING_NODE_DEFAULT,
|
|
12565
12794
|
recordingHubHostname: ""
|
|
12566
12795
|
};
|
|
12567
|
-
/** Playback-marker window padding around a motion event (EventMap only; the
|
|
12568
|
-
* controller's events-band gating uses the raw trigger ms, not this pad).
|
|
12569
|
-
* Derived from the SHARED `EVENT_PAD_MS` (R5/C1): markers are ephemeral
|
|
12570
|
-
* in-RAM and consumed by nothing today, but aligning their window with the
|
|
12571
|
-
* canonical clip pad means that IF they are ever surfaced, their boundaries
|
|
12572
|
-
* match the videoclips/admin-ui clip window for the same event — one pad
|
|
12573
|
-
* convention, not three. */
|
|
12574
|
-
var MARKER_PAD_PRE_MS = EVENT_PAD_MS.preMs;
|
|
12575
|
-
var MARKER_PAD_POST_MS = EVENT_PAD_MS.postMs;
|
|
12576
12796
|
/** This addon's manifest id + the playback data-plane prefix — together they
|
|
12577
12797
|
* form the hub reverse-proxy client path `/addon/recorder/playback`. */
|
|
12578
12798
|
/**
|
|
@@ -12736,8 +12956,6 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12736
12956
|
});
|
|
12737
12957
|
return this.lastKnownGoodRead;
|
|
12738
12958
|
}
|
|
12739
|
-
/** Per-device motion/audio markers for playback (B3 events feed the controller). */
|
|
12740
|
-
eventMap = new EventMap();
|
|
12741
12959
|
/** Unsubscribe handle for the EventCapture motion subscription. */
|
|
12742
12960
|
eventUnsub = null;
|
|
12743
12961
|
/**
|
|
@@ -12755,6 +12973,14 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12755
12973
|
*/
|
|
12756
12974
|
detectionTriggerUnsub = null;
|
|
12757
12975
|
/**
|
|
12976
|
+
* Per-window, per-camera tally behind the detection-trigger subscription. A
|
|
12977
|
+
* per-event `debug` line cannot say "NOTHING qualifies any more", which is the
|
|
12978
|
+
* regime this feed sat in — 100% rejected at 3.3–3.9/s — from the day it was
|
|
12979
|
+
* wired until 2026-08-29. This is what turns that into one `warn` a minute
|
|
12980
|
+
* naming the camera and the dominant reason.
|
|
12981
|
+
*/
|
|
12982
|
+
detectionTriggerHealth = new ObjectTriggerHealth();
|
|
12983
|
+
/**
|
|
12758
12984
|
* Unsubscribe handle for the `DeviceStateChanged` subscription — the feed
|
|
12759
12985
|
* behind `RecordingTriggers.sensorDeviceIds` (Traccia B).
|
|
12760
12986
|
*/
|
|
@@ -13343,9 +13569,6 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
13343
13569
|
this.recoverStagedOrphans(stagingBootMs);
|
|
13344
13570
|
if (this.eventUnsub === null) this.eventUnsub = subscribeEventCapture({
|
|
13345
13571
|
eventBus: this.ctx.eventBus,
|
|
13346
|
-
map: this.eventMap,
|
|
13347
|
-
padPreMs: MARKER_PAD_PRE_MS,
|
|
13348
|
-
padPostMs: MARKER_PAD_POST_MS,
|
|
13349
13572
|
onMotion: (deviceId, atMs) => {
|
|
13350
13573
|
this.controller?.onTrigger(deviceId, { kind: "motion" }, atMs);
|
|
13351
13574
|
}
|
|
@@ -13357,8 +13580,12 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
13357
13580
|
});
|
|
13358
13581
|
if (this.detectionTriggerUnsub === null) this.detectionTriggerUnsub = this.ctx.eventBus.subscribe({ category: EventCategory.PipelineAnalyticsDetectionEvent }, (event) => {
|
|
13359
13582
|
const verdict = qualifyObjectTrigger(event.data);
|
|
13583
|
+
for (const report of this.detectionTriggerHealth.record(verdict, Date.now())) this.logDetectionTriggerHealth(report);
|
|
13360
13584
|
if (!verdict.qualified) {
|
|
13361
|
-
this.ctx.logger.debug("recorder: detection event did not qualify as a trigger", {
|
|
13585
|
+
this.ctx.logger.debug("recorder: detection event did not qualify as a trigger", {
|
|
13586
|
+
...verdict.reason === "malformed" ? {} : { tags: { deviceId: verdict.deviceId } },
|
|
13587
|
+
meta: { reason: verdict.reason }
|
|
13588
|
+
});
|
|
13362
13589
|
return;
|
|
13363
13590
|
}
|
|
13364
13591
|
this.controller?.onTrigger(verdict.deviceId, {
|
|
@@ -13440,6 +13667,44 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
13440
13667
|
this.redundancySweeper?.start();
|
|
13441
13668
|
}
|
|
13442
13669
|
/**
|
|
13670
|
+
* Print one window of detection-trigger health.
|
|
13671
|
+
*
|
|
13672
|
+
* The level is the whole point: a window in which SOME object detections
|
|
13673
|
+
* opened a recording is normal operation and stays at `debug`; a window in
|
|
13674
|
+
* which a camera produced object detections and NONE of them opened one is a
|
|
13675
|
+
* dead feature, and it goes out at `warn` with the camera and the dominant
|
|
13676
|
+
* reason on it. The unbucketable `malformed` envelopes are the only line here
|
|
13677
|
+
* without a `deviceId`, because a payload whose envelope did not parse names
|
|
13678
|
+
* no camera — and that line is always `warn`, since after 2026-08-29
|
|
13679
|
+
* `malformed` can only mean the emitter drifted.
|
|
13680
|
+
*/
|
|
13681
|
+
logDetectionTriggerHealth(report) {
|
|
13682
|
+
const meta = {
|
|
13683
|
+
objectEvents: report.objectEvents,
|
|
13684
|
+
qualified: report.qualified,
|
|
13685
|
+
rejectedPct: report.rejectedPct,
|
|
13686
|
+
otherKinds: report.otherKinds,
|
|
13687
|
+
topReason: report.topReason,
|
|
13688
|
+
byReason: report.byReason,
|
|
13689
|
+
windowMs: report.windowMs
|
|
13690
|
+
};
|
|
13691
|
+
if (report.deviceId === null) {
|
|
13692
|
+
this.ctx.logger.warn("recorder: detection events with an UNREADABLE envelope — the emitter and this consumer disagree", { meta });
|
|
13693
|
+
return;
|
|
13694
|
+
}
|
|
13695
|
+
if (report.totalRejection) {
|
|
13696
|
+
this.ctx.logger.warn("recorder: EVERY object detection was rejected in this window — the object recording trigger is doing nothing for this camera", {
|
|
13697
|
+
tags: { deviceId: report.deviceId },
|
|
13698
|
+
meta
|
|
13699
|
+
});
|
|
13700
|
+
return;
|
|
13701
|
+
}
|
|
13702
|
+
this.ctx.logger.debug("recorder: object trigger window", {
|
|
13703
|
+
tags: { deviceId: report.deviceId },
|
|
13704
|
+
meta
|
|
13705
|
+
});
|
|
13706
|
+
}
|
|
13707
|
+
/**
|
|
13443
13708
|
* Rebuild the sensor trigger's reverse index from the persisted configs.
|
|
13444
13709
|
*
|
|
13445
13710
|
* D49: this is a MIRROR refreshed off the event path — at boot and on every
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-Dpy1KP1q.js");
|
|
2
2
|
let node_child_process = require("node:child_process");
|
|
3
3
|
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
4
4
|
var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { wt as errMsg } from "./dist-uX-YDOrc.mjs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
5
5
|
var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
3
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
2
|
+
const require_worker_protocol = require("../worker-protocol-Cn5x8dbW.js");
|
|
3
|
+
const require_lazy_sharp = require("../lazy-sharp-zDl97I3e.js");
|
|
4
4
|
//#region src/session-decode/color-conversion.ts
|
|
5
5
|
/**
|
|
6
6
|
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-
|
|
1
|
+
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-Cd-G92Qt.mjs";
|
|
2
2
|
import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
3
3
|
//#region src/session-decode/color-conversion.ts
|
|
4
4
|
/**
|
|
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
|
|
|
3
3
|
var e = {
|
|
4
4
|
"@camstack/sdk": {
|
|
5
5
|
name: "@camstack/sdk",
|
|
6
|
-
version: "1.2.
|
|
6
|
+
version: "1.2.44",
|
|
7
7
|
scope: ["default"],
|
|
8
8
|
loaded: !1,
|
|
9
9
|
from: "addon_stream_broker_widgets",
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.122",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_stream_broker_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.85",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_stream_broker_widgets",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
1
|
+
const require_segment_demux_js = require("../segment-demux-js-Bpedn-T8.js");
|
|
2
2
|
let node_worker_threads = require("node:worker_threads");
|
|
3
3
|
//#region src/stream-broker/recorded/demux-worker-child.ts
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
1
|
+
import { t as demuxSegmentFullJs } from "../segment-demux-js-CCMG51zw.mjs";
|
|
2
2
|
import { parentPort } from "node:worker_threads";
|
|
3
3
|
//#region src/stream-broker/recorded/demux-worker-child.ts
|
|
4
4
|
/**
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.122",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -45,7 +45,7 @@ async function r() {
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"@camstack/sdk": {
|
|
48
|
-
version: "1.2.
|
|
48
|
+
version: "1.2.44",
|
|
49
49
|
scope: "default",
|
|
50
50
|
shareConfig: {
|
|
51
51
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.85",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|