@camstack/addon-pipeline 1.2.129 → 1.2.131
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/{addon-utils-CKB-2oM9.js → addon-utils-DCQ5AP_v.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +332 -20
- package/dist/detection-pipeline/index.mjs +330 -18
- package/dist/{dist-D9x2_csL.js → dist-CqoMNE1e.js} +2010 -1753
- package/dist/{dist-CMio-1qq.mjs → dist-DO0UVhOk.mjs} +2010 -1753
- package/dist/{event-loop-stall-monitor-B9dxYxyf.mjs → event-loop-stall-monitor-D0Juf7Dn.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-UW8nsiMb.js → event-loop-stall-monitor-eM7_yYfn.js} +1 -1
- package/dist/{lazy-sharp-DrLwLKpg.js → lazy-sharp-BGrpS8yi.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-C6es8dMA.mjs → process-memory-Bac3FuG6.mjs} +1 -1
- package/dist/{process-memory-CSJ7-Wlp.js → process-memory-xAPjvSw9.js} +1 -1
- package/dist/recorder/index.js +560 -31
- package/dist/recorder/index.mjs +558 -30
- package/dist/{segment-demux-js-DiLDoV9_.js → segment-demux-js-Cu0oDMUL.js} +1 -1
- package/dist/{segment-demux-js-BRCjXbI0.mjs → segment-demux-js-skhsAZx6.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-1O99xPTr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DGQkDEYw.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.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-Do2DEtRt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CQKwHIHI.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-8scvCWua.mjs → hostInit-D7FwwZgj.mjs} +3 -3
- package/dist/stream-broker/index.js +426 -319
- package/dist/stream-broker/index.mjs +426 -319
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BmBYCioC.js → worker-protocol-G3HXTY8z.js} +1 -1
- package/dist/{worker-protocol-BHNWB9Ve.mjs → worker-protocol-_Ujvj1hl.mjs} +1 -1
- package/package.json +1 -1
- package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
- package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxppPmXr.mjs +0 -26
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as deriveRecordingMode, Dt as hydrateSchema, Gt as string, Ht as number, M as batteryCapability, Pt as selectAssignedProfileSlots, R as deriveBatteryPresence, St as BaseAddon, T as RecordingConfigSchema, Tt as DeviceType, Ut as object, Wt as record, at as recordingCapability, b as OpsLogEntrySchema, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, ft as storageEvictableCapability, g as ExportRecordSchema, gt as errMsg, i as BatteryStatusSchema, jt as nodePin, m as EVENT_PAD_MS, ot as recordingExportCapability, qt as EventCategory, ut as resolveRecordingProfiles, w as RECORDING_EXPORT_MAX_READ_BYTES, wt as DeviceFeature } from "../dist-
|
|
1
|
+
import { B as deriveRecordingMode, Dt as hydrateSchema, F as customAction, Gt as string, Ht as number, L as defineCustomActions, Lt as array, M as batteryCapability, Pt as selectAssignedProfileSlots, R as deriveBatteryPresence, Rt as boolean, St as BaseAddon, T as RecordingConfigSchema, Tt as DeviceType, Ut as object, Wt as record, at as recordingCapability, b as OpsLogEntrySchema, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, ft as storageEvictableCapability, g as ExportRecordSchema, gt as errMsg, i as BatteryStatusSchema, jt as nodePin, m as EVENT_PAD_MS, ot as recordingExportCapability, qt as EventCategory, ut as resolveRecordingProfiles, w as RECORDING_EXPORT_MAX_READ_BYTES, wt as DeviceFeature } from "../dist-DO0UVhOk.mjs";
|
|
2
2
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
3
|
import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
4
4
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-CgBCF-mE.mjs";
|
|
@@ -335,6 +335,70 @@ function fragmentRangeFor(index, offsetTicks) {
|
|
|
335
335
|
sampleIndex: chosen
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Convert a byte budget into the SET of consecutive fragments that fits,
|
|
340
|
+
* starting at the fragment covering `offsetTicks` and walking FORWARD —
|
|
341
|
+
* matching the read direction a scrub or a sequential-forward strategy
|
|
342
|
+
* actually uses. Pure: `index` is the already-parsed `tfra` table, so this
|
|
343
|
+
* never touches the segment's payload.
|
|
344
|
+
*
|
|
345
|
+
* Greedy: adds the next fragment only while the running total stays within
|
|
346
|
+
* `maxBytes`. The anchor fragment is ALWAYS included, even when it alone
|
|
347
|
+
* exceeds `maxBytes` — {@link FragmentBudgetPlan.oversizeFragment} says so
|
|
348
|
+
* explicitly rather than the caller receiving an empty set and reading it as
|
|
349
|
+
* "nothing to read" (D49's letter, applied to a byte budget: a fallible
|
|
350
|
+
* computation must never silently look like an unbind).
|
|
351
|
+
*/
|
|
352
|
+
function fragmentsWithinBudget(index, offsetTicks, maxBytes) {
|
|
353
|
+
const { samples, mfraOffset } = index;
|
|
354
|
+
let lo = 0;
|
|
355
|
+
let hi = samples.length - 1;
|
|
356
|
+
let anchor = 0;
|
|
357
|
+
while (lo <= hi) {
|
|
358
|
+
const mid = lo + hi >> 1;
|
|
359
|
+
const s = samples[mid];
|
|
360
|
+
if (s === void 0) break;
|
|
361
|
+
if (s.timeTicks <= offsetTicks) {
|
|
362
|
+
anchor = mid;
|
|
363
|
+
lo = mid + 1;
|
|
364
|
+
} else hi = mid - 1;
|
|
365
|
+
}
|
|
366
|
+
const endOf = (i) => samples[i + 1]?.moofOffset ?? mfraOffset;
|
|
367
|
+
const anchorStart = samples[anchor]?.moofOffset ?? 0;
|
|
368
|
+
const anchorEnd = endOf(anchor);
|
|
369
|
+
const fragments = [{
|
|
370
|
+
start: anchorStart,
|
|
371
|
+
end: anchorEnd,
|
|
372
|
+
sampleIndex: anchor
|
|
373
|
+
}];
|
|
374
|
+
let total = anchorEnd - anchorStart;
|
|
375
|
+
const oversizeFragment = total > maxBytes;
|
|
376
|
+
let i = anchor;
|
|
377
|
+
while (i + 1 < samples.length) {
|
|
378
|
+
const nextIndex = i + 1;
|
|
379
|
+
const nextSample = samples[nextIndex];
|
|
380
|
+
if (nextSample === void 0) break;
|
|
381
|
+
const nextStart = nextSample.moofOffset;
|
|
382
|
+
const nextEnd = endOf(nextIndex);
|
|
383
|
+
const nextSize = nextEnd - nextStart;
|
|
384
|
+
if (total + nextSize > maxBytes) break;
|
|
385
|
+
fragments.push({
|
|
386
|
+
start: nextStart,
|
|
387
|
+
end: nextEnd,
|
|
388
|
+
sampleIndex: nextIndex
|
|
389
|
+
});
|
|
390
|
+
total += nextSize;
|
|
391
|
+
i = nextIndex;
|
|
392
|
+
}
|
|
393
|
+
const last = fragments[fragments.length - 1];
|
|
394
|
+
return {
|
|
395
|
+
fragments,
|
|
396
|
+
start: fragments[0].start,
|
|
397
|
+
end: last.end,
|
|
398
|
+
reachesSegmentEnd: last.end >= mfraOffset,
|
|
399
|
+
oversizeFragment
|
|
400
|
+
};
|
|
401
|
+
}
|
|
338
402
|
//#endregion
|
|
339
403
|
//#region src/recorder/mfra-capture.ts
|
|
340
404
|
/**
|
|
@@ -2736,6 +2800,271 @@ function activeModeForConfig(config) {
|
|
|
2736
2800
|
return deriveRecordingMode(config);
|
|
2737
2801
|
}
|
|
2738
2802
|
//#endregion
|
|
2803
|
+
//#region src/recorder/addon/store/read-census.ts
|
|
2804
|
+
/**
|
|
2805
|
+
* Longest window the census may be armed for, in ms.
|
|
2806
|
+
*
|
|
2807
|
+
* Ten minutes is several report windows - enough to catch a sweep that fires
|
|
2808
|
+
* every few minutes - and short enough that an operator who forgets about it
|
|
2809
|
+
* pays for ten minutes, not until the next restart.
|
|
2810
|
+
*/
|
|
2811
|
+
var CENSUS_MAX_WINDOW_MS = 10 * 6e4;
|
|
2812
|
+
/** Store procedures worth attributing. Reads only: a write is an operator
|
|
2813
|
+
* gesture or a pipeline product, and neither is a mystery. */
|
|
2814
|
+
var CENSUSED_PROCEDURES = new Set([
|
|
2815
|
+
"query",
|
|
2816
|
+
"count",
|
|
2817
|
+
"get",
|
|
2818
|
+
"histogram",
|
|
2819
|
+
"getAll"
|
|
2820
|
+
]);
|
|
2821
|
+
/**
|
|
2822
|
+
* Can `new Proxy` wrap this, and is it worth wrapping?
|
|
2823
|
+
*
|
|
2824
|
+
* Functions count. The generated store client is a **tRPC proxy** and every
|
|
2825
|
+
* intermediate node of one is CALLABLE — `typeof client.query` is `'function'`,
|
|
2826
|
+
* not `'object'`. A wrapper that tested only for `'object'` handed the whole
|
|
2827
|
+
* client back unwrapped, deployed, and reported zero `query` and zero `count`
|
|
2828
|
+
* call sites while the engine was recording thousands a minute against the same
|
|
2829
|
+
* tables. Silence read as "nobody in this addon does it".
|
|
2830
|
+
*/
|
|
2831
|
+
function isProxyable(value) {
|
|
2832
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
2833
|
+
}
|
|
2834
|
+
function rowsOf(value) {
|
|
2835
|
+
if (Array.isArray(value)) return value.length;
|
|
2836
|
+
if (typeof value === "number") return value;
|
|
2837
|
+
if (value === void 0 || value === null) return 0;
|
|
2838
|
+
return 1;
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* One V8 stack line reduced to `Function@line`.
|
|
2842
|
+
*
|
|
2843
|
+
* A deployed addon is ONE bundled file, so the path is identical on every frame
|
|
2844
|
+
* and carries no information; the line number does. A frame that parses as
|
|
2845
|
+
* neither keeps its trimmed text rather than being dropped - an unrecognised
|
|
2846
|
+
* frame is still an attribution, and dropping it would silently shift the site
|
|
2847
|
+
* one frame up.
|
|
2848
|
+
*/
|
|
2849
|
+
function compactFrame(line) {
|
|
2850
|
+
const trimmed = line.trim().replace(/^at\s+/, "");
|
|
2851
|
+
const named = /^(.+?)\s+\((?:.*?):(\d+):\d+\)$/.exec(trimmed);
|
|
2852
|
+
if (named !== null) return `${named[1]}@${named[2]}`;
|
|
2853
|
+
const bare = /^(?:.*?):(\d+):\d+$/.exec(trimmed);
|
|
2854
|
+
if (bare !== null) return `@${bare[1]}`;
|
|
2855
|
+
return trimmed;
|
|
2856
|
+
}
|
|
2857
|
+
/** The census's view of a stack: the top {@link CENSUS_STACK_FRAMES} frames,
|
|
2858
|
+
* innermost first, joined by ` < ` so a line reads caller-ward. */
|
|
2859
|
+
function siteFromStack(stack) {
|
|
2860
|
+
if (stack === void 0) return "unknown";
|
|
2861
|
+
const frames = stack.split("\n").slice(1, 4);
|
|
2862
|
+
if (frames.length === 0) return "unknown";
|
|
2863
|
+
return frames.map(compactFrame).join(" < ");
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Armed-window call-site counter.
|
|
2867
|
+
*
|
|
2868
|
+
* Disarmed it is one comparison per read and captures nothing - the stack is
|
|
2869
|
+
* the whole cost, and it is not taken.
|
|
2870
|
+
*/
|
|
2871
|
+
var StoreReadCensus = class {
|
|
2872
|
+
now;
|
|
2873
|
+
counts = /* @__PURE__ */ new Map();
|
|
2874
|
+
armedUntil = 0;
|
|
2875
|
+
armedAt = 0;
|
|
2876
|
+
constructor(now = () => Date.now()) {
|
|
2877
|
+
this.now = now;
|
|
2878
|
+
}
|
|
2879
|
+
/** Arm for `forMs`, clamped to {@link CENSUS_MAX_WINDOW_MS}. Re-arming
|
|
2880
|
+
* restarts the window and CLEARS the counts: two overlapping measurements in
|
|
2881
|
+
* one table is a table nobody can divide by a duration. */
|
|
2882
|
+
arm(forMs) {
|
|
2883
|
+
const at = this.now();
|
|
2884
|
+
this.counts.clear();
|
|
2885
|
+
this.armedAt = at;
|
|
2886
|
+
this.armedUntil = at + Math.max(0, Math.min(forMs, CENSUS_MAX_WINDOW_MS));
|
|
2887
|
+
}
|
|
2888
|
+
disarm() {
|
|
2889
|
+
this.armedUntil = 0;
|
|
2890
|
+
}
|
|
2891
|
+
get armed() {
|
|
2892
|
+
return this.now() < this.armedUntil;
|
|
2893
|
+
}
|
|
2894
|
+
/** How long the current (or just-finished) window collected, in ms. The
|
|
2895
|
+
* divisor for a calls-per-minute rate - a count without it is a number that
|
|
2896
|
+
* cannot be compared to the next one. */
|
|
2897
|
+
elapsedMs() {
|
|
2898
|
+
const at = this.now();
|
|
2899
|
+
return Math.max(0, Math.min(at, this.armedUntil) - this.armedAt);
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* The call site of whoever called `cutoff`, or `null` when disarmed.
|
|
2903
|
+
*
|
|
2904
|
+
* **Taken BEFORE the await, always.** The first version of this class
|
|
2905
|
+
* captured the stack next to the counter, after the wrapped call had
|
|
2906
|
+
* resolved - and by then the synchronous caller is gone: V8 stitches an async
|
|
2907
|
+
* frame in its place and the site reads
|
|
2908
|
+
* `Proxy.<anonymous> < processTicksAndRejections`, which is the census
|
|
2909
|
+
* describing itself. It passed one of its own tests anyway, because that one
|
|
2910
|
+
* caller happened to RETURN the promise rather than await it. An instrument
|
|
2911
|
+
* that reports its own frames is worse than no instrument: the counts are
|
|
2912
|
+
* real, so the wrong name gets believed.
|
|
2913
|
+
*
|
|
2914
|
+
* `cutoff` is the wrapper function itself, so every frame from the wrapper
|
|
2915
|
+
* inwards is dropped and frame 0 is the code that asked.
|
|
2916
|
+
*/
|
|
2917
|
+
siteOf(cutoff) {
|
|
2918
|
+
if (!this.armed) return null;
|
|
2919
|
+
const holder = {};
|
|
2920
|
+
Error.captureStackTrace(holder, cutoff);
|
|
2921
|
+
return siteFromStack(holder.stack);
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Count one read at an already-captured site.
|
|
2925
|
+
*
|
|
2926
|
+
* A `null` site means the census was disarmed when the call STARTED. It is
|
|
2927
|
+
* not counted even if it lands inside a later window - a read belongs to the
|
|
2928
|
+
* window that issued it.
|
|
2929
|
+
*/
|
|
2930
|
+
record(op, site, result) {
|
|
2931
|
+
if (site === null) return;
|
|
2932
|
+
const key = `${op} ${site}`;
|
|
2933
|
+
const existing = this.counts.get(key);
|
|
2934
|
+
if (existing === void 0) {
|
|
2935
|
+
this.counts.set(key, {
|
|
2936
|
+
op,
|
|
2937
|
+
site,
|
|
2938
|
+
calls: 1,
|
|
2939
|
+
rows: rowsOf(result)
|
|
2940
|
+
});
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
existing.calls += 1;
|
|
2944
|
+
existing.rows += rowsOf(result);
|
|
2945
|
+
}
|
|
2946
|
+
/** Busiest first. Does NOT clear - a window may be read as many times as the
|
|
2947
|
+
* operator likes and always describes the same window. */
|
|
2948
|
+
rows() {
|
|
2949
|
+
return [...this.counts.values()].map((r) => ({
|
|
2950
|
+
op: r.op,
|
|
2951
|
+
site: r.site,
|
|
2952
|
+
calls: r.calls,
|
|
2953
|
+
rows: r.rows
|
|
2954
|
+
})).sort((a, b) => b.calls - a.calls);
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
/**
|
|
2958
|
+
* Wrap a settings-store client so every read is counted.
|
|
2959
|
+
*
|
|
2960
|
+
* A `Proxy` over the generated tRPC client rather than a hand-written facade:
|
|
2961
|
+
* the client has a procedure per cap method and a facade would need updating
|
|
2962
|
+
* every time the cap gains one - which is how a diagnostic quietly stops seeing
|
|
2963
|
+
* half the traffic. Unrecognised procedures pass straight through.
|
|
2964
|
+
*/
|
|
2965
|
+
function censusStoreClient(client, census) {
|
|
2966
|
+
return new Proxy(client, { get(target, prop, receiver) {
|
|
2967
|
+
const procedure = Reflect.get(target, prop, receiver);
|
|
2968
|
+
if (typeof prop !== "string" || !CENSUSED_PROCEDURES.has(prop)) return procedure;
|
|
2969
|
+
if (!isProxyable(procedure)) return procedure;
|
|
2970
|
+
const op = prop;
|
|
2971
|
+
return new Proxy(procedure, { get(procTarget, procProp, procReceiver) {
|
|
2972
|
+
const call = Reflect.get(procTarget, procProp, procReceiver);
|
|
2973
|
+
if (procProp !== "query" && procProp !== "mutate") return call;
|
|
2974
|
+
if (typeof call !== "function") return call;
|
|
2975
|
+
const wrapped = async (...args) => {
|
|
2976
|
+
const site = census.siteOf(wrapped);
|
|
2977
|
+
const result = await Reflect.apply(call, procTarget, args);
|
|
2978
|
+
census.record(op, site, result);
|
|
2979
|
+
return result;
|
|
2980
|
+
};
|
|
2981
|
+
return wrapped;
|
|
2982
|
+
} });
|
|
2983
|
+
} });
|
|
2984
|
+
}
|
|
2985
|
+
/** Log the census as one line, including when it collected nothing - silence
|
|
2986
|
+
* after an arm is indistinguishable from a census that never armed. */
|
|
2987
|
+
function logCensus(census, logger, topN) {
|
|
2988
|
+
const rows = census.rows();
|
|
2989
|
+
const elapsedMs = census.elapsedMs();
|
|
2990
|
+
const perMin = (calls) => elapsedMs === 0 ? 0 : Math.round(calls * 6e4 / elapsedMs);
|
|
2991
|
+
logger.info("store read census", { meta: {
|
|
2992
|
+
elapsedMs,
|
|
2993
|
+
sites: rows.length,
|
|
2994
|
+
calls: rows.reduce((acc, r) => acc + r.calls, 0),
|
|
2995
|
+
top: rows.slice(0, topN).map((r) => ({
|
|
2996
|
+
op: r.op,
|
|
2997
|
+
site: r.site,
|
|
2998
|
+
calls: r.calls,
|
|
2999
|
+
perMin: perMin(r.calls),
|
|
3000
|
+
rows: r.rows
|
|
3001
|
+
}))
|
|
3002
|
+
} });
|
|
3003
|
+
return rows;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* `armMs: 0` (or omitted) READS the window that already ran — that is how a
|
|
3007
|
+
* measurement is collected after its window closed, and re-arming to read
|
|
3008
|
+
* would throw away exactly the numbers being asked for (`StoreReadCensus.arm`
|
|
3009
|
+
* clears on every call).
|
|
3010
|
+
*/
|
|
3011
|
+
var StoreCensusInputSchema = object({
|
|
3012
|
+
armMs: number().int().min(0).max(CENSUS_MAX_WINDOW_MS).optional(),
|
|
3013
|
+
limit: number().int().positive().max(200).optional()
|
|
3014
|
+
});
|
|
3015
|
+
var StoreCensusRowSchema = object({
|
|
3016
|
+
op: string(),
|
|
3017
|
+
site: string(),
|
|
3018
|
+
calls: number(),
|
|
3019
|
+
/** Calls extrapolated to a minute, directly comparable with the live-hub
|
|
3020
|
+
* profiler's own `get recorder:recording-configs` rate. */
|
|
3021
|
+
perMin: number(),
|
|
3022
|
+
rows: number()
|
|
3023
|
+
});
|
|
3024
|
+
var recorderDebugActions = defineCustomActions({ "debug.storeCensus": customAction(StoreCensusInputSchema, object({
|
|
3025
|
+
armed: boolean(),
|
|
3026
|
+
elapsedMs: number(),
|
|
3027
|
+
sites: number(),
|
|
3028
|
+
calls: number(),
|
|
3029
|
+
rows: array(StoreCensusRowSchema).readonly()
|
|
3030
|
+
}), { auth: "admin" }) });
|
|
3031
|
+
/**
|
|
3032
|
+
* Arm-or-read, pulled out of the addon class so it is testable without
|
|
3033
|
+
* booting `RecorderV2Addon` — feed it a bare `StoreReadCensus` and a logger.
|
|
3034
|
+
*
|
|
3035
|
+
* The result is also logged (via {@link logCensus}), because a measurement
|
|
3036
|
+
* that exists only in an HTTP response is a measurement that is gone when the
|
|
3037
|
+
* next session asks what happened.
|
|
3038
|
+
*/
|
|
3039
|
+
function handleStoreCensusAction(census, logger, input) {
|
|
3040
|
+
const limit = input.limit ?? 200;
|
|
3041
|
+
if (input.armMs !== void 0 && input.armMs > 0) {
|
|
3042
|
+
census.arm(input.armMs);
|
|
3043
|
+
return {
|
|
3044
|
+
armed: true,
|
|
3045
|
+
elapsedMs: 0,
|
|
3046
|
+
sites: 0,
|
|
3047
|
+
calls: 0,
|
|
3048
|
+
rows: []
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
const rows = logCensus(census, logger, limit);
|
|
3052
|
+
const elapsedMs = census.elapsedMs();
|
|
3053
|
+
return {
|
|
3054
|
+
armed: census.armed,
|
|
3055
|
+
elapsedMs,
|
|
3056
|
+
sites: rows.length,
|
|
3057
|
+
calls: rows.reduce((acc, r) => acc + r.calls, 0),
|
|
3058
|
+
rows: rows.slice(0, limit).map((r) => ({
|
|
3059
|
+
op: r.op,
|
|
3060
|
+
site: r.site,
|
|
3061
|
+
calls: r.calls,
|
|
3062
|
+
perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs),
|
|
3063
|
+
rows: r.rows
|
|
3064
|
+
}))
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
//#endregion
|
|
2739
3068
|
//#region src/recorder/addon/directory-handler.ts
|
|
2740
3069
|
var HOUR_MS$3 = 36e5;
|
|
2741
3070
|
/** A window wider than this is refused: it is a client bug, and answering it
|
|
@@ -4093,6 +4422,18 @@ function buildMasterPlaylist(variants) {
|
|
|
4093
4422
|
* against a real captured segment with every read recorded — "never the whole
|
|
4094
4423
|
* file" is asserted in bytes.
|
|
4095
4424
|
*/
|
|
4425
|
+
/**
|
|
4426
|
+
* Byte budget for the `readGopBytes` fallback read, used with
|
|
4427
|
+
* {@link GopRangeArgs.maxBytes}.
|
|
4428
|
+
*
|
|
4429
|
+
* Same conceptual ceiling as `SCRUB_WHOLE_SEGMENT_MAX_BYTES` in
|
|
4430
|
+
* `stream-broker/recorded/recorded-feeder.ts` (D163, 1.5 MB) — declared here
|
|
4431
|
+
* independently rather than imported, because that module is the CLIENT
|
|
4432
|
+
* feeder (a different addon-internal concern; importing its whole dependency
|
|
4433
|
+
* graph for one number is the wrong direction). If D163's number moves,
|
|
4434
|
+
* this one moves with it.
|
|
4435
|
+
*/
|
|
4436
|
+
var SCRUB_GOP_BUDGET_MAX_BYTES = 15e5;
|
|
4096
4437
|
/** Ticks → ms on the video timescale, rounded like the demux anchors samples. */
|
|
4097
4438
|
function ticksToMs(ticks, timescale) {
|
|
4098
4439
|
return Math.round(ticks / timescale * 1e3);
|
|
@@ -4122,11 +4463,36 @@ async function readGopRange(file, args) {
|
|
|
4122
4463
|
if (init === null) return null;
|
|
4123
4464
|
const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
|
|
4124
4465
|
if (videoIndex === null) return null;
|
|
4125
|
-
const
|
|
4466
|
+
const offsetTicks = Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale;
|
|
4467
|
+
let start;
|
|
4468
|
+
let end;
|
|
4469
|
+
let firstSampleIndex;
|
|
4470
|
+
let lastSampleIndex;
|
|
4471
|
+
let reachesSegmentEnd;
|
|
4472
|
+
let oversizeFragment;
|
|
4473
|
+
if (args.maxBytes === void 0) {
|
|
4474
|
+
const single = fragmentRangeFor(videoIndex, offsetTicks);
|
|
4475
|
+
start = single.start;
|
|
4476
|
+
end = single.end;
|
|
4477
|
+
firstSampleIndex = single.sampleIndex;
|
|
4478
|
+
lastSampleIndex = single.sampleIndex;
|
|
4479
|
+
reachesSegmentEnd = videoIndex.samples[single.sampleIndex + 1] === void 0;
|
|
4480
|
+
oversizeFragment = false;
|
|
4481
|
+
} else {
|
|
4482
|
+
const plan = fragmentsWithinBudget(videoIndex, offsetTicks, args.maxBytes);
|
|
4483
|
+
const firstFragment = plan.fragments[0];
|
|
4484
|
+
const lastFragment = plan.fragments[plan.fragments.length - 1];
|
|
4485
|
+
start = plan.start;
|
|
4486
|
+
end = plan.end;
|
|
4487
|
+
firstSampleIndex = firstFragment.sampleIndex;
|
|
4488
|
+
lastSampleIndex = lastFragment.sampleIndex;
|
|
4489
|
+
reachesSegmentEnd = plan.reachesSegmentEnd;
|
|
4490
|
+
oversizeFragment = plan.oversizeFragment;
|
|
4491
|
+
}
|
|
4126
4492
|
if (start <= 0 || end <= start || end > file.size) return null;
|
|
4127
4493
|
const fragment = await read(start, end - start);
|
|
4128
|
-
const startTicks = videoIndex.samples[
|
|
4129
|
-
const nextTicks = videoIndex.samples[
|
|
4494
|
+
const startTicks = videoIndex.samples[firstSampleIndex]?.timeTicks ?? 0;
|
|
4495
|
+
const nextTicks = videoIndex.samples[lastSampleIndex + 1]?.timeTicks;
|
|
4130
4496
|
const gopStartOffsetMs = ticksToMs(startTicks, init.videoTimescale);
|
|
4131
4497
|
const gopDurMs = nextTicks !== void 0 ? ticksToMs(nextTicks - startTicks, init.videoTimescale) : Math.max(1, args.segmentDurMs - gopStartOffsetMs);
|
|
4132
4498
|
const data = new Uint8Array(head.length + fragment.length);
|
|
@@ -4136,7 +4502,9 @@ async function readGopRange(file, args) {
|
|
|
4136
4502
|
data,
|
|
4137
4503
|
gopStartMs: args.segmentStartMs + gopStartOffsetMs,
|
|
4138
4504
|
gopDurMs,
|
|
4139
|
-
bytesRead
|
|
4505
|
+
bytesRead,
|
|
4506
|
+
reachesSegmentEnd,
|
|
4507
|
+
oversizeFragment
|
|
4140
4508
|
};
|
|
4141
4509
|
}
|
|
4142
4510
|
/**
|
|
@@ -4669,6 +5037,30 @@ function resolveSegmentFile(deps, deviceId, profile, startMs) {
|
|
|
4669
5037
|
abs: path.join(loc.root, seg.path)
|
|
4670
5038
|
};
|
|
4671
5039
|
}
|
|
5040
|
+
/**
|
|
5041
|
+
* The log line's `meta`, or null when `readGopBytes` served nothing worth a
|
|
5042
|
+
* line. Split out from the handler so the decision is testable against a
|
|
5043
|
+
* plain object — no real fMP4 bytes needed to prove a >1.5 MB fragment
|
|
5044
|
+
* triggers it.
|
|
5045
|
+
*
|
|
5046
|
+
* The single-fragment budget overshoot is the notable case, not the routine
|
|
5047
|
+
* one: a `high`/`mid` scrub goes through this path on every tick by design
|
|
5048
|
+
* (client-side `profile-degraded`, already logged once per drag on the
|
|
5049
|
+
* feeder), and logging every such call here would spam nothing new. A
|
|
5050
|
+
* fragment that alone exceeds the budget is different — it is never returned
|
|
5051
|
+
* empty (never-empty is {@link fragmentsWithinBudget}'s contract), but an
|
|
5052
|
+
* operator debugging a slow drag needs to know it happened.
|
|
5053
|
+
*/
|
|
5054
|
+
function oversizeFragmentLogMeta(gop, args) {
|
|
5055
|
+
if (!gop.oversizeFragment) return null;
|
|
5056
|
+
return {
|
|
5057
|
+
profile: args.profile,
|
|
5058
|
+
startMs: args.startMs,
|
|
5059
|
+
epochMs: args.epochMs,
|
|
5060
|
+
fragmentBytes: gop.data.length,
|
|
5061
|
+
maxBytes: SCRUB_GOP_BUDGET_MAX_BYTES
|
|
5062
|
+
};
|
|
5063
|
+
}
|
|
4672
5064
|
/** Default ranged opener: ONE `fs.open` per GOP answer serving all its reads. */
|
|
4673
5065
|
async function openRangeFromDisk(absPath, size) {
|
|
4674
5066
|
const handle = await promises.open(absPath, "r");
|
|
@@ -4888,13 +5280,25 @@ function buildRecordingProvider(deps) {
|
|
|
4888
5280
|
segmentStartMs: seg.startMs,
|
|
4889
5281
|
segmentDurMs: seg.durMs,
|
|
4890
5282
|
epochMs,
|
|
4891
|
-
seededIndex: deps.mfraFor?.(deviceId, profile, startMs)
|
|
5283
|
+
seededIndex: deps.mfraFor?.(deviceId, profile, startMs),
|
|
5284
|
+
maxBytes: SCRUB_GOP_BUDGET_MAX_BYTES
|
|
4892
5285
|
});
|
|
4893
|
-
if (gop !== null)
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
5286
|
+
if (gop !== null) {
|
|
5287
|
+
const oversizeMeta = oversizeFragmentLogMeta(gop, {
|
|
5288
|
+
profile,
|
|
5289
|
+
startMs,
|
|
5290
|
+
epochMs
|
|
5291
|
+
});
|
|
5292
|
+
if (oversizeMeta !== null) deps.logger.warn("recording: readGopBytes served one fragment that alone exceeds the scrub budget", {
|
|
5293
|
+
tags: { deviceId },
|
|
5294
|
+
meta: oversizeMeta
|
|
5295
|
+
});
|
|
5296
|
+
return {
|
|
5297
|
+
data: gop.data,
|
|
5298
|
+
gopStartMs: gop.gopStartMs,
|
|
5299
|
+
gopDurMs: gop.gopDurMs
|
|
5300
|
+
};
|
|
5301
|
+
}
|
|
4898
5302
|
} finally {
|
|
4899
5303
|
await file.close();
|
|
4900
5304
|
}
|
|
@@ -7116,7 +7520,46 @@ function resolveBandBufferMs(band) {
|
|
|
7116
7520
|
*/
|
|
7117
7521
|
function eventsBandCanEverDemand(band) {
|
|
7118
7522
|
if (band.mode !== "events") return true;
|
|
7119
|
-
return (band.
|
|
7523
|
+
return bandTriggerSources(band).length > 0 && resolveBandBufferMs(band).postMs > 0;
|
|
7524
|
+
}
|
|
7525
|
+
/**
|
|
7526
|
+
* LE sorgenti che questa banda ascolta — l'unico posto dove i `triggers` di
|
|
7527
|
+
* una banda diventano `TriggerSource[]`. `eventsBandCanEverDemand` e
|
|
7528
|
+
* `RecordingController.reportUnrecordable` lo leggono invece di ripetere
|
|
7529
|
+
* l'if-ladder motion/audio a mano.
|
|
7530
|
+
*/
|
|
7531
|
+
function bandTriggerSources(band) {
|
|
7532
|
+
const t = band.triggers;
|
|
7533
|
+
if (t === void 0) return [];
|
|
7534
|
+
const sources = [];
|
|
7535
|
+
if (t.motion === true) sources.push("motion");
|
|
7536
|
+
if (t.audioThresholdDbfs !== void 0) sources.push("audio");
|
|
7537
|
+
return sources;
|
|
7538
|
+
}
|
|
7539
|
+
/**
|
|
7540
|
+
* Does this audio window's level clear the ACTIVE band's configured
|
|
7541
|
+
* threshold?
|
|
7542
|
+
*
|
|
7543
|
+
* `PipelineAudioInferenceResultPayload.frame.level.dbfs`
|
|
7544
|
+
* (`packages/types/src/types/detection.ts:448`) and
|
|
7545
|
+
* `RecordingTriggers.audioThresholdDbfs` are the SAME unit and the SAME
|
|
7546
|
+
* scale — dBFS, 0 = full scale, more negative = quieter — produced by
|
|
7547
|
+
* `audio-analyzer-provider.ts:77` (`20 * log10(rms)`). No conversion exists
|
|
7548
|
+
* between them because none is needed: the one other place in this repo that
|
|
7549
|
+
* compares the two values does the same bare `>=`
|
|
7550
|
+
* (`notification-center/audio-rule-matcher.ts:221`). Inventing a conversion
|
|
7551
|
+
* here would recreate the exact defect this track exists to close, only
|
|
7552
|
+
* harder to see because a wrong scale still fires occasionally.
|
|
7553
|
+
*
|
|
7554
|
+
* `band` must be the band ACTIVE AT the audio window's own timestamp — a
|
|
7555
|
+
* device can carry several bands with different thresholds (day/night), and
|
|
7556
|
+
* only one is ever "the" threshold at a given instant. The caller
|
|
7557
|
+
* (`RecordingController.onAudioLevel`) resolves it the same way
|
|
7558
|
+
* `postBufferMsFor` resolves the post-buffer.
|
|
7559
|
+
*/
|
|
7560
|
+
function audioLevelQualifies(band, dbfs) {
|
|
7561
|
+
const threshold = band?.triggers?.audioThresholdDbfs;
|
|
7562
|
+
return threshold !== void 0 && dbfs >= threshold;
|
|
7120
7563
|
}
|
|
7121
7564
|
/** No trigger has been seen for this device yet. */
|
|
7122
7565
|
var EMPTY_TRIGGER_STATE = {
|
|
@@ -8979,6 +9422,43 @@ var RecordingController = class {
|
|
|
8979
9422
|
}
|
|
8980
9423
|
}
|
|
8981
9424
|
/**
|
|
9425
|
+
* Feed one audio window's LEVEL into the trigger pipeline — the counterpart
|
|
9426
|
+
* of `onTrigger('motion', …)` for the `audioThresholdDbfs` source, and the
|
|
9427
|
+
* feed that source was missing until 2026-08-26 (D62: authorized by the UI,
|
|
9428
|
+
* read by every pure function in `band-decision.ts`, alimented by nobody).
|
|
9429
|
+
*
|
|
9430
|
+
* Qualification happens HERE, not upstream like motion's `data.detected`,
|
|
9431
|
+
* because it depends on a PER-BAND threshold: `audioThresholdDbfs` is
|
|
9432
|
+
* authored per band, not once per device, so it needs the same config read
|
|
9433
|
+
* {@link postBufferMsFor} already does to resolve which band is active at
|
|
9434
|
+
* `atMs` before it can know which threshold applies.
|
|
9435
|
+
*
|
|
9436
|
+
* Fed from the event bus (telemetry, may be dropped — D8/D11), so a
|
|
9437
|
+
* config-read failure here drops only THIS window's qualification test
|
|
9438
|
+
* rather than guessing a threshold — logged, never silent, and never fatal:
|
|
9439
|
+
* the device's recordability does not depend on any single audio sample,
|
|
9440
|
+
* the next window (or motion) can still open the band.
|
|
9441
|
+
*/
|
|
9442
|
+
async onAudioLevel(deviceId, dbfs, atMs) {
|
|
9443
|
+
if (this.stopped) return;
|
|
9444
|
+
let band;
|
|
9445
|
+
try {
|
|
9446
|
+
band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
|
|
9447
|
+
} catch (err) {
|
|
9448
|
+
this.deps.logger.warn("recorder: audio trigger qualification skipped — config read failed", {
|
|
9449
|
+
tags: { deviceId },
|
|
9450
|
+
meta: {
|
|
9451
|
+
atMs,
|
|
9452
|
+
dbfs,
|
|
9453
|
+
error: errMsg(err)
|
|
9454
|
+
}
|
|
9455
|
+
});
|
|
9456
|
+
return;
|
|
9457
|
+
}
|
|
9458
|
+
if (!audioLevelQualifies(band, dbfs)) return;
|
|
9459
|
+
await this.onTrigger(deviceId, "audio", atMs);
|
|
9460
|
+
}
|
|
9461
|
+
/**
|
|
8982
9462
|
* The post-buffer that governs this device's trigger history right now: it is
|
|
8983
9463
|
* both the MERGE GAP (a trigger landing inside the open live window belongs to
|
|
8984
9464
|
* the running episode) and the retention horizon. Falls back to the shipped
|
|
@@ -9093,7 +9573,7 @@ var RecordingController = class {
|
|
|
9093
9573
|
meta: {
|
|
9094
9574
|
postBufferSec: band.postBufferSec,
|
|
9095
9575
|
triggers: band.triggers,
|
|
9096
|
-
reason: band.
|
|
9576
|
+
reason: bandTriggerSources(band).length > 0 ? "postBufferSec is 0" : "no trigger configured"
|
|
9097
9577
|
}
|
|
9098
9578
|
});
|
|
9099
9579
|
}
|
|
@@ -11147,6 +11627,14 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
11147
11627
|
eventMap = new EventMap();
|
|
11148
11628
|
/** Unsubscribe handle for the EventCapture motion subscription. */
|
|
11149
11629
|
eventUnsub = null;
|
|
11630
|
+
/**
|
|
11631
|
+
* Unsubscribe handle for the `PipelineAudioInferenceResult` audio-level
|
|
11632
|
+
* subscription — the feed `audioThresholdDbfs` was missing (D62, Traccia 0
|
|
11633
|
+
* of `docs/design/plans/2026-08-26-trigger-detection-e-sensori.md`). Kept
|
|
11634
|
+
* separate from `eventUnsub` (a distinct bus category) but guarded and torn
|
|
11635
|
+
* down the same way, idempotent re-entry included.
|
|
11636
|
+
*/
|
|
11637
|
+
audioTriggerUnsub = null;
|
|
11150
11638
|
/** Resolved recordings locations (id + root). Refreshed on boot + rescan.
|
|
11151
11639
|
* Already ALIAS-GUARDED: a writable same-type duplicate of another
|
|
11152
11640
|
* location's physical root arrives here demoted to read-only. */
|
|
@@ -11175,6 +11663,23 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
11175
11663
|
retentionSweeper = null;
|
|
11176
11664
|
/** Reversible migration pause lease; never reflected in RecordingConfig. */
|
|
11177
11665
|
storageMigrationLeaseId = null;
|
|
11666
|
+
/**
|
|
11667
|
+
* Call-site attribution for `recorder:recording-configs` / `-exports` /
|
|
11668
|
+
* `-ops-log` row reads (`store/read-census.ts`).
|
|
11669
|
+
*
|
|
11670
|
+
* Disarmed by default; armed for a bounded window through
|
|
11671
|
+
* `debug.storeCensus`. It exists because the live-hub profiler names a
|
|
11672
|
+
* TABLE — `get recorder:recording-configs` ran 581-892 calls/min on
|
|
11673
|
+
* 2026-08-26, finding a row only ~56% of the time — and cannot say WHICH of
|
|
11674
|
+
* `loadDeviceConfig`'s several cap-handler call sites (`getStatus`,
|
|
11675
|
+
* `getDeviceConfig`, `rescanStorage`, `pruneFootage`) is issuing them.
|
|
11676
|
+
* Picking the reader that LOOKS like a loop has cost this repo half a day
|
|
11677
|
+
* twice, wrong both times — this measures instead.
|
|
11678
|
+
*/
|
|
11679
|
+
readCensus = new StoreReadCensus();
|
|
11680
|
+
/** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
|
|
11681
|
+
* call shares ONE census instead of each wrapping the raw client afresh. */
|
|
11682
|
+
censusedSettingsStoreClient = null;
|
|
11178
11683
|
constructor() {
|
|
11179
11684
|
super({ ...DEFAULT_CONFIG });
|
|
11180
11685
|
}
|
|
@@ -11579,24 +12084,30 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
11579
12084
|
}
|
|
11580
12085
|
});
|
|
11581
12086
|
warnLostBands(this.configStore(), this.ctx.logger);
|
|
11582
|
-
return {
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
12087
|
+
return {
|
|
12088
|
+
providers: [
|
|
12089
|
+
{
|
|
12090
|
+
capability: recordingCapability,
|
|
12091
|
+
provider: recordingProvider
|
|
12092
|
+
},
|
|
12093
|
+
{
|
|
12094
|
+
capability: storageEvictableCapability,
|
|
12095
|
+
provider: evictableProvider
|
|
12096
|
+
},
|
|
12097
|
+
{
|
|
12098
|
+
capability: recordingExportCapability,
|
|
12099
|
+
provider: exportProvider
|
|
12100
|
+
}
|
|
12101
|
+
],
|
|
12102
|
+
customActions: recorderDebugActions,
|
|
12103
|
+
actionHandlers: { "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input) }
|
|
12104
|
+
};
|
|
11596
12105
|
}
|
|
11597
12106
|
async onShutdown() {
|
|
11598
12107
|
this.eventUnsub?.();
|
|
11599
12108
|
this.eventUnsub = null;
|
|
12109
|
+
this.audioTriggerUnsub?.();
|
|
12110
|
+
this.audioTriggerUnsub = null;
|
|
11600
12111
|
await this.controller?.stop();
|
|
11601
12112
|
this.controller = null;
|
|
11602
12113
|
this.segmentStore = null;
|
|
@@ -11671,6 +12182,11 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
11671
12182
|
this.controller?.onTrigger(deviceId, "motion", atMs);
|
|
11672
12183
|
}
|
|
11673
12184
|
});
|
|
12185
|
+
if (this.audioTriggerUnsub === null) this.audioTriggerUnsub = this.ctx.eventBus.subscribe({ category: EventCategory.PipelineAudioInferenceResult }, (event) => {
|
|
12186
|
+
const dbfs = event.data.frame.level?.dbfs;
|
|
12187
|
+
if (dbfs === void 0) return;
|
|
12188
|
+
this.controller?.onAudioLevel(event.data.deviceId, dbfs, event.data.frame.timestamp);
|
|
12189
|
+
});
|
|
11674
12190
|
try {
|
|
11675
12191
|
await this.segmentHours?.load();
|
|
11676
12192
|
this.segmentHours?.hydrateIndex(this.index);
|
|
@@ -12241,17 +12757,29 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
12241
12757
|
this.ctx.logger.warn("recorder: retiring the flattened settings keys failed", { meta: { error: errMsg(err) } });
|
|
12242
12758
|
}
|
|
12243
12759
|
}
|
|
12760
|
+
/**
|
|
12761
|
+
* `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
|
|
12762
|
+
*
|
|
12763
|
+
* LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
|
|
12764
|
+
* runs, so this cannot be a field initialiser. The wrap is permanent and
|
|
12765
|
+
* cheap when disarmed (one `get` trap and a comparison) — what is armed and
|
|
12766
|
+
* disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
|
|
12767
|
+
*/
|
|
12768
|
+
censusedSettingsStore() {
|
|
12769
|
+
this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
|
|
12770
|
+
return this.censusedSettingsStoreClient;
|
|
12771
|
+
}
|
|
12244
12772
|
configStore() {
|
|
12245
|
-
return recordingConfigsState(this.
|
|
12773
|
+
return recordingConfigsState(this.censusedSettingsStore(), this.ctx.logger);
|
|
12246
12774
|
}
|
|
12247
12775
|
/** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
|
|
12248
12776
|
* wrapper as the config store). */
|
|
12249
12777
|
exportsStore() {
|
|
12250
|
-
return recordingExportsState(this.
|
|
12778
|
+
return recordingExportsState(this.censusedSettingsStore(), this.ctx.logger);
|
|
12251
12779
|
}
|
|
12252
12780
|
/** Durable handle over the bounded recordings ops-log ring. */
|
|
12253
12781
|
opsLogStore() {
|
|
12254
|
-
return recordingOpsLogState(this.
|
|
12782
|
+
return recordingOpsLogState(this.censusedSettingsStore(), this.ctx.logger);
|
|
12255
12783
|
}
|
|
12256
12784
|
/** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
|
|
12257
12785
|
placementStore() {
|