@camstack/addon-pipeline 1.2.129 → 1.2.130

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 (123) hide show
  1. package/dist/{addon-utils-CKB-2oM9.js → addon-utils-DH78p79i.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +5 -5
  5. package/dist/detection-pipeline/index.mjs +3 -3
  6. package/dist/{dist-D9x2_csL.js → dist-DOw-izoI.js} +201 -49
  7. package/dist/{dist-CMio-1qq.mjs → dist-Wt81pN5Y.mjs} +201 -49
  8. package/dist/{event-loop-stall-monitor-UW8nsiMb.js → event-loop-stall-monitor-Bld7jNCb.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-B9dxYxyf.mjs → event-loop-stall-monitor-CtQCOZqm.mjs} +1 -1
  10. package/dist/{lazy-sharp-DrLwLKpg.js → lazy-sharp-BbNI2ZD-.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-C6es8dMA.mjs → process-memory-1_D3EZT9.mjs} +1 -1
  16. package/dist/{process-memory-CSJ7-Wlp.js → process-memory-D9H5qjDl.js} +1 -1
  17. package/dist/recorder/index.js +467 -29
  18. package/dist/recorder/index.mjs +465 -28
  19. package/dist/{segment-demux-js-BRCjXbI0.mjs → segment-demux-js-BW-Q-j2A.mjs} +1 -1
  20. package/dist/{segment-demux-js-DiLDoV9_.js → segment-demux-js-C2jBBqGW.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +2 -2
  22. package/dist/session-decode/decode-worker-child.mjs +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-1O99xPTr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dw-iwP4y.mjs} +3 -3
  25. 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
  26. 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-D36MoVV1.mjs} +1 -1
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-8scvCWua.mjs → hostInit-DEVCEYgu.mjs} +3 -3
  30. package/dist/stream-broker/index.js +426 -319
  31. package/dist/stream-broker/index.mjs +426 -319
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-BmBYCioC.js → worker-protocol-nAZl_Rsv.js} +1 -1
  34. package/dist/{worker-protocol-BHNWB9Ve.mjs → worker-protocol-rpnr7Ldd.mjs} +1 -1
  35. package/package.json +1 -1
  36. package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
  37. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  38. package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
  39. package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
  43. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
  44. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  45. package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
  46. package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
  47. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  48. package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
  49. package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
  50. package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
  51. package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
  52. package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
  53. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
  54. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
  55. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
  56. package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
  57. package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
  58. package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
  59. package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
  60. package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
  61. package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
  62. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  63. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  64. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  65. package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  67. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
  70. package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
  71. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  72. package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
  73. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  75. package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
  76. package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
  77. package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
  78. package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
  79. package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
  80. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  81. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  82. package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
  83. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  84. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  85. package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
  86. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  87. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  88. package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
  89. package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
  90. package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
  91. package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
  92. package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
  93. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  94. package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
  95. package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
  96. package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
  97. package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
  98. package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
  99. package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
  100. package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
  101. package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
  102. package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
  103. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  104. package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
  105. package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
  106. package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
  107. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
  108. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
  109. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
  110. package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
  111. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  112. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  113. package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
  114. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  115. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  116. package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
  117. package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
  118. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  119. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  120. package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
  121. package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
  122. package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
  123. 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
@@ -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-CMio-1qq.mjs";
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-Wt81pN5Y.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 { start, end, sampleIndex } = fragmentRangeFor(videoIndex, Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale);
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[sampleIndex]?.timeTicks ?? 0;
4129
- const nextTicks = videoIndex.samples[sampleIndex + 1]?.timeTicks;
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) return {
4894
- data: gop.data,
4895
- gopStartMs: gop.gopStartMs,
4896
- gopDurMs: gop.gopDurMs
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
  }
@@ -11175,6 +11579,23 @@ var RecorderV2Addon = class extends BaseAddon {
11175
11579
  retentionSweeper = null;
11176
11580
  /** Reversible migration pause lease; never reflected in RecordingConfig. */
11177
11581
  storageMigrationLeaseId = null;
11582
+ /**
11583
+ * Call-site attribution for `recorder:recording-configs` / `-exports` /
11584
+ * `-ops-log` row reads (`store/read-census.ts`).
11585
+ *
11586
+ * Disarmed by default; armed for a bounded window through
11587
+ * `debug.storeCensus`. It exists because the live-hub profiler names a
11588
+ * TABLE — `get recorder:recording-configs` ran 581-892 calls/min on
11589
+ * 2026-08-26, finding a row only ~56% of the time — and cannot say WHICH of
11590
+ * `loadDeviceConfig`'s several cap-handler call sites (`getStatus`,
11591
+ * `getDeviceConfig`, `rescanStorage`, `pruneFootage`) is issuing them.
11592
+ * Picking the reader that LOOKS like a loop has cost this repo half a day
11593
+ * twice, wrong both times — this measures instead.
11594
+ */
11595
+ readCensus = new StoreReadCensus();
11596
+ /** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
11597
+ * call shares ONE census instead of each wrapping the raw client afresh. */
11598
+ censusedSettingsStoreClient = null;
11178
11599
  constructor() {
11179
11600
  super({ ...DEFAULT_CONFIG });
11180
11601
  }
@@ -11579,20 +12000,24 @@ var RecorderV2Addon = class extends BaseAddon {
11579
12000
  }
11580
12001
  });
11581
12002
  warnLostBands(this.configStore(), this.ctx.logger);
11582
- return { providers: [
11583
- {
11584
- capability: recordingCapability,
11585
- provider: recordingProvider
11586
- },
11587
- {
11588
- capability: storageEvictableCapability,
11589
- provider: evictableProvider
11590
- },
11591
- {
11592
- capability: recordingExportCapability,
11593
- provider: exportProvider
11594
- }
11595
- ] };
12003
+ return {
12004
+ providers: [
12005
+ {
12006
+ capability: recordingCapability,
12007
+ provider: recordingProvider
12008
+ },
12009
+ {
12010
+ capability: storageEvictableCapability,
12011
+ provider: evictableProvider
12012
+ },
12013
+ {
12014
+ capability: recordingExportCapability,
12015
+ provider: exportProvider
12016
+ }
12017
+ ],
12018
+ customActions: recorderDebugActions,
12019
+ actionHandlers: { "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input) }
12020
+ };
11596
12021
  }
11597
12022
  async onShutdown() {
11598
12023
  this.eventUnsub?.();
@@ -12241,17 +12666,29 @@ var RecorderV2Addon = class extends BaseAddon {
12241
12666
  this.ctx.logger.warn("recorder: retiring the flattened settings keys failed", { meta: { error: errMsg(err) } });
12242
12667
  }
12243
12668
  }
12669
+ /**
12670
+ * `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
12671
+ *
12672
+ * LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
12673
+ * runs, so this cannot be a field initialiser. The wrap is permanent and
12674
+ * cheap when disarmed (one `get` trap and a comparison) — what is armed and
12675
+ * disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
12676
+ */
12677
+ censusedSettingsStore() {
12678
+ this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
12679
+ return this.censusedSettingsStoreClient;
12680
+ }
12244
12681
  configStore() {
12245
- return recordingConfigsState(this.ctx.api.settingsStore, this.ctx.logger);
12682
+ return recordingConfigsState(this.censusedSettingsStore(), this.ctx.logger);
12246
12683
  }
12247
12684
  /** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
12248
12685
  * wrapper as the config store). */
12249
12686
  exportsStore() {
12250
- return recordingExportsState(this.ctx.api.settingsStore, this.ctx.logger);
12687
+ return recordingExportsState(this.censusedSettingsStore(), this.ctx.logger);
12251
12688
  }
12252
12689
  /** Durable handle over the bounded recordings ops-log ring. */
12253
12690
  opsLogStore() {
12254
- return recordingOpsLogState(this.ctx.api.settingsStore, this.ctx.logger);
12691
+ return recordingOpsLogState(this.censusedSettingsStore(), this.ctx.logger);
12255
12692
  }
12256
12693
  /** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
12257
12694
  placementStore() {
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { gt as errMsg } from "./dist-CMio-1qq.mjs";
2
+ import { gt as errMsg } from "./dist-Wt81pN5Y.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,4 +1,4 @@
1
- const require_dist = require("./dist-D9x2_csL.js");
1
+ const require_dist = require("./dist-DOw-izoI.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,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-BmBYCioC.js");
3
- const require_lazy_sharp = require("../lazy-sharp-DrLwLKpg.js");
2
+ const require_worker_protocol = require("../worker-protocol-nAZl_Rsv.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-BbNI2ZD-.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-BHNWB9Ve.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-rpnr7Ldd.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Do2DEtRt.mjs";
1
+ import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs";
2
2
  import { a as p, i as m, n as h, o as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
3
3
  import { n as y, r as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
4
- import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxppPmXr.mjs";
4
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs";
5
5
  import { n as C, t as w } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
6
6
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
7
7
  var T = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), E = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), D = (e) => {
@@ -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.31",
6
+ version: "1.2.32",
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.106",
21
+ version: "1.2.110",
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.72",
36
+ version: "1.2.73",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",