@camstack/addon-pipeline 1.2.38 → 1.2.40

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 (90) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +145 -175
  4. package/dist/detection-pipeline/index.mjs +120 -150
  5. package/dist/{dist-Du7_SNiR.mjs → dist-C6p8jcji.mjs} +495 -113
  6. package/dist/{dist-BvFLZyhW.js → dist-DcMBfoCg.js} +495 -113
  7. package/dist/{step-definitions-C8tk9fPJ.mjs → event-loop-stall-monitor-BHqCesa1.mjs} +139 -2
  8. package/dist/{step-definitions-i8O-hxyI.js → event-loop-stall-monitor-oL9xanvX.js} +150 -1
  9. package/dist/lazy-sharp-B-mb8uWx.mjs +20 -0
  10. package/dist/lazy-sharp-CcYuXtsa.js +25 -0
  11. package/dist/motion-wasm/index.js +3 -3
  12. package/dist/motion-wasm/index.mjs +3 -3
  13. package/dist/pipeline-runner/index.js +3316 -2428
  14. package/dist/pipeline-runner/index.mjs +3315 -2428
  15. package/dist/recorder/index.js +102 -664
  16. package/dist/recorder/index.mjs +102 -658
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ByuMjhL3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bt1YkfUW.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DJnh97n9.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsNhZnfZ.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-CfC64ye2.mjs → hostInit-Dyj8Vaql.mjs} +2 -2
  22. package/dist/stream-broker/index.js +723 -214
  23. package/dist/stream-broker/index.mjs +723 -214
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Dx-Vrpyt.js → MaskShapeCanvas-DI4BY7W2-m1RaK_7F.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-9D-BWwQP.js → MotionZonesSettings-NcxxQN8r-C00UIgFo.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-HvAetfA4.js → PrivacyMaskSettings-APgPLF7p-DWYllToj.js} +1 -1
  28. package/embed-dist/assets/index-4fJzjj4H.css +2 -0
  29. package/embed-dist/assets/index-BaUgbxWl.js +112 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +2 -2
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ByHi7ZAe.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CscpvIRR.mjs +0 -26
  34. package/embed-dist/assets/index-9Ob2YrIf.js +0 -343
  35. package/embed-dist/assets/index-CAe6Dg-X.css +0 -2
  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-314.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
  43. package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
  44. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  45. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
  48. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  49. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  50. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  51. package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
  52. package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
  53. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  54. package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
  55. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  56. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  57. package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
  58. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  59. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  60. package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
  61. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  62. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  63. package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
  64. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  65. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  66. package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
  67. package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
  70. package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
  71. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  72. package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
  73. package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
  75. package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
  76. package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
  77. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  78. package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
  79. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
  80. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
  81. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  82. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  83. package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
  84. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  85. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  86. package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
  87. package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
  88. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  89. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  90. package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
@@ -1,9 +1,5 @@
1
- Object.defineProperties(exports, {
2
- __esModule: { value: true },
3
- [Symbol.toStringTag]: { value: "Module" }
4
- });
5
1
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-BvFLZyhW.js");
2
+ const require_dist = require("../dist-DcMBfoCg.js");
7
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
4
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
9
5
  let node_child_process = require("node:child_process");
@@ -92,8 +88,9 @@ async function availabilityProfile(root, deviceId) {
92
88
  }
93
89
  /** The canonical recording profiles — the ONLY `<deviceId>/` subdirectories
94
90
  * that hold segments. The camera dir also hosts per-camera artifacts that are
95
- * NOT profiles (`strips/`, `events/`); a walker that trusts readdir would
96
- * offer them as playable profiles. */
91
+ * NOT profiles (`events/`, plus the `strips/` trees the retired filmstrip tier
92
+ * left on disk); a walker that trusts readdir would offer them as playable
93
+ * profiles. */
97
94
  var KNOWN_PROFILES = new Set([
98
95
  "high",
99
96
  "mid",
@@ -227,7 +224,7 @@ function mergeRanges(segments, gapToleranceMs) {
227
224
  //#endregion
228
225
  //#region src/recorder/addon/calendar-index.ts
229
226
  var DAY_MS$3 = 864e5;
230
- var HOUR_MS$4 = 36e5;
227
+ var HOUR_MS$3 = 36e5;
231
228
  /**
232
229
  * UTC ms → the UTC epoch of LOCAL midnight of the day it falls in.
233
230
  *
@@ -400,7 +397,7 @@ var CalendarIndex = class {
400
397
  }));
401
398
  const days = /* @__PURE__ */ new Set();
402
399
  for (const { day, hours } of perDay) for (const h of hours) {
403
- const at = day + h * HOUR_MS$4;
400
+ const at = day + h * HOUR_MS$3;
404
401
  if (at < fromMs || at >= toMs) continue;
405
402
  days.add(localDayStart(at, tzOffsetMinutes));
406
403
  }
@@ -462,7 +459,7 @@ var CalendarIndex = class {
462
459
  const day = utcDayOf(utcDayMs);
463
460
  for (let h = 0; h < 24; h++) {
464
461
  await this.awaitQuiet();
465
- await this.segmentsOf(deviceId, profile, day + h * HOUR_MS$4, true);
462
+ await this.segmentsOf(deviceId, profile, day + h * HOUR_MS$3, true);
466
463
  }
467
464
  }
468
465
  /**
@@ -483,10 +480,10 @@ var CalendarIndex = class {
483
480
  */
484
481
  async segmentsOfChecked(deviceId, profile, hourStartMs, background = false) {
485
482
  if (!background) this.lastForegroundAt = this.deps.now?.() ?? Date.now();
486
- const hour = Math.floor(hourStartMs / HOUR_MS$4) * HOUR_MS$4;
483
+ const hour = Math.floor(hourStartMs / HOUR_MS$3) * HOUR_MS$3;
487
484
  const key = `${deviceId}:${profile}:${hour}`;
488
485
  const now = this.deps.now?.() ?? Date.now();
489
- const isCurrentHour = hour === Math.floor(now / HOUR_MS$4) * HOUR_MS$4;
486
+ const isCurrentHour = hour === Math.floor(now / HOUR_MS$3) * HOUR_MS$3;
490
487
  const cached = this.hourSegs.get(key);
491
488
  if (cached !== void 0) {
492
489
  if (!isCurrentHour) return {
@@ -550,7 +547,7 @@ var CalendarIndex = class {
550
547
  */
551
548
  async dayRanges(deviceId, profile, utcDayMs) {
552
549
  const day = utcDayOf(utcDayMs);
553
- return mergeRanges((await mapBounded(Array.from({ length: 24 }, (_, h) => day + h * HOUR_MS$4), READ_CONCURRENCY, (hour) => this.segmentsOf(deviceId, profile, hour))).flat(), RANGE_MERGE_GAP_MS$1);
550
+ return mergeRanges((await mapBounded(Array.from({ length: 24 }, (_, h) => day + h * HOUR_MS$3), READ_CONCURRENCY, (hour) => this.segmentsOf(deviceId, profile, hour))).flat(), RANGE_MERGE_GAP_MS$1);
554
551
  }
555
552
  /**
556
553
  * Warm the days either side of the one being viewed, in the BACKGROUND.
@@ -634,7 +631,7 @@ var PLAYBACK_PLANE_PATH = "/addon/recorder/playback";
634
631
  * deployed client ever sees a version it cannot read. */
635
632
  var VERSION = 2;
636
633
  /** LEB128, unsigned. Every value here is a non-negative integer. */
637
- function writeVarint$1(out, value) {
634
+ function writeVarint(out, value) {
638
635
  let v = Math.max(0, Math.trunc(value));
639
636
  while (v >= 128) {
640
637
  out.push(v & 127 | 128);
@@ -655,29 +652,29 @@ function encodeDirectory(payload) {
655
652
  out.push(VERSION);
656
653
  out.push(payload.hydrated ? 1 : 0);
657
654
  const profile = new TextEncoder().encode(payload.profile);
658
- writeVarint$1(out, profile.length);
655
+ writeVarint(out, profile.length);
659
656
  for (const b of profile) out.push(b);
660
- writeVarint$1(out, payload.asOfMs);
661
- writeVarint$1(out, payload.rows.length);
657
+ writeVarint(out, payload.asOfMs);
658
+ writeVarint(out, payload.rows.length);
662
659
  let prevStart = 0;
663
660
  for (const r of payload.rows) {
664
- writeVarint$1(out, r.startMs - prevStart);
665
- writeVarint$1(out, r.durMs);
666
- writeVarint$1(out, r.bytes);
661
+ writeVarint(out, r.startMs - prevStart);
662
+ writeVarint(out, r.durMs);
663
+ writeVarint(out, r.bytes);
667
664
  prevStart = r.startMs;
668
665
  const mfra = r.mfra;
669
666
  if (mfra === void 0 || mfra.samples.length === 0) {
670
- writeVarint$1(out, 0);
667
+ writeVarint(out, 0);
671
668
  continue;
672
669
  }
673
- writeVarint$1(out, mfra.samples.length);
674
- writeVarint$1(out, mfra.trackId);
675
- writeVarint$1(out, mfra.mfraOffset);
670
+ writeVarint(out, mfra.samples.length);
671
+ writeVarint(out, mfra.trackId);
672
+ writeVarint(out, mfra.mfraOffset);
676
673
  let prevTicks = 0;
677
674
  let prevMoof = 0;
678
675
  for (const s of mfra.samples) {
679
- writeVarint$1(out, s.timeTicks - prevTicks);
680
- writeVarint$1(out, s.moofOffset - prevMoof);
676
+ writeVarint(out, s.timeTicks - prevTicks);
677
+ writeVarint(out, s.moofOffset - prevMoof);
681
678
  prevTicks = s.timeTicks;
682
679
  prevMoof = s.moofOffset;
683
680
  }
@@ -686,7 +683,7 @@ function encodeDirectory(payload) {
686
683
  }
687
684
  //#endregion
688
685
  //#region src/recorder/addon/directory-handler.ts
689
- var HOUR_MS$3 = 36e5;
686
+ var HOUR_MS$2 = 36e5;
690
687
  /** A window wider than this is refused: it is a client bug, and answering it
691
688
  * would walk the archive on the read path — the thing this replaces. */
692
689
  var MAX_WINDOW_MS = 32 * 864e5;
@@ -733,9 +730,9 @@ function withDirectoryRoute(deps, next) {
733
730
  return;
734
731
  }
735
732
  const rows = [];
736
- const firstHour = Math.floor(fromMs / HOUR_MS$3) * HOUR_MS$3;
733
+ const firstHour = Math.floor(fromMs / HOUR_MS$2) * HOUR_MS$2;
737
734
  let hydrated = true;
738
- for (let hour = firstHour; hour < toMs; hour += HOUR_MS$3) {
735
+ for (let hour = firstHour; hour < toMs; hour += HOUR_MS$2) {
739
736
  const { segments, complete } = await deps.source.segmentsOfChecked(deviceId, profile, hour);
740
737
  if (!complete) hydrated = false;
741
738
  for (const s of segments) {
@@ -995,12 +992,9 @@ async function readMfraTable(absPath, fileBytes) {
995
992
  try {
996
993
  const idx = parseMfra(tail, fileBytes, info === null ? {} : { trackId: info.videoTrackId });
997
994
  return {
998
- table: {
999
- trackId: idx.trackId,
1000
- mfraOffset: idx.mfraOffset,
1001
- samples: idx.samples
1002
- },
1003
- videoTimescale: info?.videoTimescale ?? null
995
+ trackId: idx.trackId,
996
+ mfraOffset: idx.mfraOffset,
997
+ samples: idx.samples
1004
998
  };
1005
999
  } catch (err) {
1006
1000
  if (err instanceof MfraParseError && err.needBytes && attempt === 0) {
@@ -1014,20 +1008,6 @@ async function readMfraTable(absPath, fileBytes) {
1014
1008
  await fh.close();
1015
1009
  }
1016
1010
  }
1017
- /**
1018
- * Wall-clock instants for a segment's keyframes: `-skip_frame nokey` emits
1019
- * them in decode order, so frame i IS sync sample i. Without a timescale the
1020
- * samples are spread evenly — a degraded position beats a missing thumbnail.
1021
- */
1022
- function keyframeInstants(startMs, durMs, captured) {
1023
- const { samples } = captured.table;
1024
- const first = samples[0]?.timeTicks ?? 0;
1025
- const ts = captured.videoTimescale;
1026
- return samples.map((s, i) => {
1027
- if (ts !== null && ts > 0) return Math.round(startMs + (s.timeTicks - first) / ts * 1e3);
1028
- return Math.round(startMs + i / samples.length * durMs);
1029
- });
1030
- }
1031
1011
  /** How long a table stays served. Directory asks are for recent footage far
1032
1012
  * more often than not, and a bounded window keeps this store O(day), not
1033
1013
  * O(uptime): ~30 bytes a segment ⇒ a day across 20 cameras is a few MB. */
@@ -1076,467 +1056,6 @@ var MfraTableStore = class {
1076
1056
  }
1077
1057
  };
1078
1058
  //#endregion
1079
- //#region src/recorder/strip-format.ts
1080
- /**
1081
- * The thumbnail STRIP — the fast-drag depth of the scrub ladder.
1082
- *
1083
- * One append-only file per camera-hour holds every keyframe of the LOW
1084
- * recording as a JPEG at the low profile's NATIVE resolution. A fast drag
1085
- * crosses an hour in seconds and wants ~20 scattered stills; without the strip
1086
- * that is 20 keyframe reads in 20 different segments under latest-wins —
1087
- * structurally incapable of painting a flow. With it, the positions a drag
1088
- * crosses are a handful of ~25–40 KB ranged reads from ONE file, painted at
1089
- * the capped cadence.
1090
- *
1091
- * TWO wire shapes live here, both halves in one module so they cannot drift:
1092
- *
1093
- * - the STRIP FILE (`CSS1`): [magic][version] then records of
1094
- * [u32 jpegLen][u64 tsMs][jpeg]. Self-describing, so the index is
1095
- * derivable from the file alone after a restart.
1096
- * - the STRIP INDEX (`CSI1`): the varint answer to "what instants does this
1097
- * hour hold, and which bytes paint each one" — served from RAM, consumed by
1098
- * the client to turn a cursor position into one ranged read.
1099
- */
1100
- /** 'CSS1' — camstack strip, v1. */
1101
- var FILE_MAGIC = new Uint8Array([
1102
- 67,
1103
- 83,
1104
- 83,
1105
- 49
1106
- ]);
1107
- var FILE_VERSION = 1;
1108
- var STRIP_FILE_HEADER_BYTES = FILE_MAGIC.length + 1;
1109
- function stripFileHeader() {
1110
- const out = new Uint8Array(STRIP_FILE_HEADER_BYTES);
1111
- out.set(FILE_MAGIC, 0);
1112
- out[FILE_MAGIC.length] = FILE_VERSION;
1113
- return out;
1114
- }
1115
- function encodeStripRecord(tsMs, jpeg) {
1116
- const out = new Uint8Array(12 + jpeg.length);
1117
- const view = new DataView(out.buffer);
1118
- view.setUint32(0, jpeg.length);
1119
- view.setBigUint64(4, BigInt(Math.trunc(tsMs)));
1120
- out.set(jpeg, 12);
1121
- return out;
1122
- }
1123
- /**
1124
- * Walk a strip file's bytes into its entry list. Tolerates a TRUNCATED final
1125
- * record (a crash mid-append) by stopping at it — everything before is intact
1126
- * because records are append-only.
1127
- */
1128
- function parseStripEntries(bytes) {
1129
- if (bytes.length < STRIP_FILE_HEADER_BYTES) return [];
1130
- for (let i = 0; i < FILE_MAGIC.length; i++) if (bytes[i] !== FILE_MAGIC[i]) throw new Error("CSS1: bad magic");
1131
- if (bytes[FILE_MAGIC.length] !== FILE_VERSION) throw new Error(`CSS1: unsupported version ${String(bytes[FILE_MAGIC.length])}`);
1132
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1133
- const entries = [];
1134
- let p = STRIP_FILE_HEADER_BYTES;
1135
- while (p + 12 <= bytes.length) {
1136
- const len = view.getUint32(p);
1137
- const tsMs = Number(view.getBigUint64(p + 4));
1138
- const offset = p + 12;
1139
- if (offset + len > bytes.length) break;
1140
- entries.push({
1141
- tsMs,
1142
- offset,
1143
- bytes: len
1144
- });
1145
- p = offset + len;
1146
- }
1147
- return entries;
1148
- }
1149
- var INDEX_VERSION = 1;
1150
- function writeVarint(out, value) {
1151
- let v = Math.max(0, Math.trunc(value));
1152
- while (v >= 128) {
1153
- out.push(v & 127 | 128);
1154
- v = Math.floor(v / 128);
1155
- }
1156
- out.push(v);
1157
- }
1158
- /** Entries MUST be ascending by tsMs and offset — append order guarantees it. */
1159
- function encodeStripIndex(entries) {
1160
- const out = [];
1161
- out.push(67, 83, 73, 49);
1162
- out.push(INDEX_VERSION);
1163
- writeVarint(out, entries.length);
1164
- let prevTs = 0;
1165
- let prevOffset = 0;
1166
- for (const e of entries) {
1167
- writeVarint(out, e.tsMs - prevTs);
1168
- writeVarint(out, e.offset - prevOffset);
1169
- writeVarint(out, e.bytes);
1170
- prevTs = e.tsMs;
1171
- prevOffset = e.offset;
1172
- }
1173
- return Uint8Array.from(out);
1174
- }
1175
- //#endregion
1176
- //#region src/recorder/strip-store.ts
1177
- /**
1178
- * The strip files on disk: append at finalize, index on demand.
1179
- *
1180
- * Layout: `<location root>/<deviceId>/strips/YYYY/MM/DD/HH.strip` — rooted
1181
- * under the CAMERA directory like every other per-camera artifact (segments,
1182
- * event media). The profile walkers (`profilesOf`, `hydrateWindowFromStorage`)
1183
- * whitelist the canonical profile names, so `strips/` (and `events/`) sitting
1184
- * next to `high|mid|low/` is never offered as a playable profile.
1185
- *
1186
- * Appends are serialized per file (a per-key promise chain): two segments of
1187
- * the same hour finalizing close together must not interleave their records.
1188
- */
1189
- var HOUR_MS$2 = 36e5;
1190
- /** Location-relative path of one camera-hour's strip. */
1191
- function stripRelPath(deviceId, hourStartMs) {
1192
- const d = /* @__PURE__ */ new Date(Math.floor(hourStartMs / HOUR_MS$2) * HOUR_MS$2);
1193
- const p2 = (n) => String(n).padStart(2, "0");
1194
- return node_path.default.join(String(deviceId), "strips", String(d.getUTCFullYear()), p2(d.getUTCMonth() + 1), p2(d.getUTCDate()), `${p2(d.getUTCHours())}.strip`);
1195
- }
1196
- var StripStore = class {
1197
- rootFor;
1198
- /** Per-file append chain — see the header comment. */
1199
- appending = /* @__PURE__ */ new Map();
1200
- /** Parsed entries per file, invalidated when the SIZE moves (append-only
1201
- * files: same size ⟹ same content). */
1202
- indexCache = /* @__PURE__ */ new Map();
1203
- constructor(rootFor) {
1204
- this.rootFor = rootFor;
1205
- }
1206
- /** Absolute path for a camera-hour, or null before locations resolve. */
1207
- absPathFor(deviceId, hourStartMs) {
1208
- const root = this.rootFor();
1209
- if (root === null) return null;
1210
- return node_path.default.join(root, stripRelPath(deviceId, hourStartMs));
1211
- }
1212
- /** Append thumbnails (one segment's keyframes — same hour by construction). */
1213
- async append(deviceId, thumbs) {
1214
- const first = thumbs[0];
1215
- if (first === void 0) return;
1216
- const abs = this.absPathFor(deviceId, first.tsMs);
1217
- if (abs === null) return;
1218
- const run = (this.appending.get(abs) ?? Promise.resolve()).then(async () => {
1219
- await node_fs.promises.mkdir(node_path.default.dirname(abs), { recursive: true });
1220
- let created = false;
1221
- try {
1222
- await node_fs.promises.access(abs);
1223
- } catch {
1224
- created = true;
1225
- }
1226
- const parts = created ? [stripFileHeader()] : [];
1227
- for (const t of thumbs) parts.push(encodeStripRecord(t.tsMs, t.jpeg));
1228
- await node_fs.promises.appendFile(abs, Buffer.concat(parts.map((p) => Buffer.from(p))));
1229
- this.indexCache.delete(abs);
1230
- });
1231
- this.appending.set(abs, run.catch(() => {}));
1232
- try {
1233
- await run;
1234
- } finally {
1235
- if (this.appending.get(abs) === run) this.appending.delete(abs);
1236
- }
1237
- }
1238
- /**
1239
- * The entry list for a camera-hour, or null when the hour has no strip.
1240
- * Cached by file size — append-only files cannot change under a same-size
1241
- * cache.
1242
- */
1243
- async index(deviceId, hourStartMs) {
1244
- const abs = this.absPathFor(deviceId, hourStartMs);
1245
- if (abs === null) return null;
1246
- let size;
1247
- try {
1248
- size = (await node_fs.promises.stat(abs)).size;
1249
- } catch {
1250
- return null;
1251
- }
1252
- const cached = this.indexCache.get(abs);
1253
- if (cached && cached.size === size) return cached.entries;
1254
- const entries = parseStripEntries(new Uint8Array(await node_fs.promises.readFile(abs)));
1255
- this.indexCache.set(abs, {
1256
- size,
1257
- entries
1258
- });
1259
- return entries;
1260
- }
1261
- /** A regeneration clears the device's whole strip tree first. */
1262
- async removeDevice(deviceId) {
1263
- const root = this.rootFor();
1264
- if (root === null) return;
1265
- for (const key of [...this.indexCache.keys()]) if (key.includes(node_path.default.join(String(deviceId), "strips") + node_path.default.sep)) this.indexCache.delete(key);
1266
- await node_fs.promises.rm(node_path.default.join(root, String(deviceId), "strips"), {
1267
- recursive: true,
1268
- force: true
1269
- }).catch(() => {});
1270
- }
1271
- /** Eviction removed an hour's footage — its strip goes with it. */
1272
- async removeHour(deviceId, hourStartMs) {
1273
- const abs = this.absPathFor(deviceId, hourStartMs);
1274
- if (abs === null) return;
1275
- this.indexCache.delete(abs);
1276
- await node_fs.promises.rm(abs, { force: true }).catch(() => {});
1277
- }
1278
- };
1279
- //#endregion
1280
- //#region src/recorder/strip-capture.ts
1281
- /**
1282
- * Keyframe thumbnails from a JUST-FINALIZED low segment — one short-lived
1283
- * ffmpeg per segment, `-skip_frame nokey`, so only the 2–4 keyframes are ever
1284
- * decoded (~15 ms of CPU on a 360p segment still sitting in page cache).
1285
- *
1286
- * WHY NOT A SECOND OUTPUT ON THE RECORDER'S FFMPEG
1287
- * ────────────────────────────────────────────────
1288
- * The write path is the one process that must never be destabilised, and an
1289
- * image pipe has no per-frame PTS — pairing frames to instants would need NUT
1290
- * parsing or stderr scraping. At finalize the mfra capture has ALREADY read
1291
- * the sync-sample table: `-skip_frame nokey` emits keyframes in decode order,
1292
- * so frame i IS sample i and the timestamps come for free. A failed capture
1293
- * costs one hour's drag a thumbnail, nothing else.
1294
- *
1295
- * Bounded: at most `MAX_CONCURRENT` ffmpegs, and a backlog beyond
1296
- * `MAX_QUEUED` drops the OLDEST waiting job — the strip is a best-effort
1297
- * accelerator, and a stampede of finalizes (20 cameras, one segment cadence)
1298
- * must never queue decode work behind the array.
1299
- */
1300
- var MAX_CONCURRENT = 2;
1301
- var MAX_QUEUED = 32;
1302
- /** A one-shot decode of 2–4 keyframes must never wedge a slot. */
1303
- var TIMEOUT_MS = 15e3;
1304
- /** Split an MJPEG byte stream into its JPEGs on SOI markers (FFD8 FFE0/DB…).
1305
- * ffmpeg's image2pipe emits baseline JPEGs back to back; scanning for the
1306
- * next SOI right after an EOI is unambiguous for its output. */
1307
- function splitJpegs(stream) {
1308
- const out = [];
1309
- let start = -1;
1310
- for (let i = 0; i + 1 < stream.length; i++) if (stream[i] === 255 && stream[i + 1] === 216 && (start === -1 || wasEoi(stream, i))) {
1311
- if (start !== -1) out.push(stream.subarray(start, i));
1312
- start = i;
1313
- }
1314
- if (start !== -1) out.push(stream.subarray(start));
1315
- return out;
1316
- }
1317
- /** Whether the two bytes immediately before `i` are an EOI — the only place a
1318
- * new SOI can legitimately begin inside a concatenated stream. */
1319
- function wasEoi(stream, i) {
1320
- return i >= 2 && stream[i - 2] === 255 && stream[i - 1] === 217;
1321
- }
1322
- var StripCapture = class {
1323
- deps;
1324
- queue = [];
1325
- running = 0;
1326
- constructor(deps = {}) {
1327
- this.deps = deps;
1328
- }
1329
- /**
1330
- * The segment's keyframe JPEGs in decode order, or null when the capture was
1331
- * dropped (backlog) or failed. Never throws — the strip is best-effort.
1332
- */
1333
- capture(absPath) {
1334
- return new Promise((resolve) => {
1335
- this.queue.push({
1336
- absPath,
1337
- resolve
1338
- });
1339
- while (this.queue.length > MAX_QUEUED) {
1340
- const dropped = this.queue.shift();
1341
- dropped?.resolve(null);
1342
- this.deps.onLog?.("strip:dropped", { path: dropped?.absPath ?? "" });
1343
- }
1344
- this.pump();
1345
- });
1346
- }
1347
- pump() {
1348
- while (this.running < MAX_CONCURRENT) {
1349
- const job = this.queue.shift();
1350
- if (job === void 0) return;
1351
- this.running++;
1352
- (this.deps.runFfmpeg ?? this.runFfmpeg)(job.absPath).then((stream) => job.resolve(splitJpegs(stream))).catch((err) => {
1353
- this.deps.onLog?.("strip:capture-failed", {
1354
- path: job.absPath,
1355
- error: err instanceof Error ? err.message : String(err)
1356
- });
1357
- job.resolve(null);
1358
- }).finally(() => {
1359
- this.running--;
1360
- this.pump();
1361
- });
1362
- }
1363
- }
1364
- runFfmpeg = (absPath) => {
1365
- return new Promise((resolve, reject) => {
1366
- const child = (0, node_child_process.spawn)(this.deps.ffmpegPath ?? "ffmpeg", [
1367
- "-hide_banner",
1368
- "-loglevel",
1369
- "error",
1370
- "-skip_frame",
1371
- "nokey",
1372
- "-i",
1373
- absPath,
1374
- "-c:v",
1375
- "mjpeg",
1376
- "-q:v",
1377
- "7",
1378
- "-f",
1379
- "image2pipe",
1380
- "-"
1381
- ], { stdio: [
1382
- "ignore",
1383
- "pipe",
1384
- "pipe"
1385
- ] });
1386
- const chunks = [];
1387
- const errChunks = [];
1388
- const timer = setTimeout(() => {
1389
- child.kill("SIGKILL");
1390
- reject(/* @__PURE__ */ new Error(`strip capture timed out after ${TIMEOUT_MS}ms`));
1391
- }, TIMEOUT_MS);
1392
- timer.unref?.();
1393
- child.stdout.on("data", (c) => chunks.push(c));
1394
- child.stderr.on("data", (c) => errChunks.push(c));
1395
- child.on("error", (err) => {
1396
- clearTimeout(timer);
1397
- reject(err);
1398
- });
1399
- child.on("close", (code) => {
1400
- clearTimeout(timer);
1401
- if (code === 0) resolve(new Uint8Array(Buffer.concat(chunks)));
1402
- else reject(/* @__PURE__ */ new Error(`ffmpeg exited ${String(code)}: ${Buffer.concat(errChunks).toString("utf8").slice(0, 300)}`));
1403
- });
1404
- });
1405
- };
1406
- };
1407
- //#endregion
1408
- //#region src/recorder/addon/strip-routes.ts
1409
- /**
1410
- * The strip's two questions, answered on the playback plane:
1411
- *
1412
- * `GET /<deviceId>/strip-index?hour=<hourStartMs>` → `CSI1` (what instants
1413
- * the hour holds, and which bytes paint each one)
1414
- * `GET /<deviceId>/strip?hour=<hourStartMs>` + `Range` → the strip file's
1415
- * bytes (the client asks for exactly the records it needs)
1416
- *
1417
- * Same prefix, same auth, same fetch seam as the segments and the directory —
1418
- * one plane, one credential path. The strip files live under `<deviceId>/strips/`
1419
- * (the per-camera layout; the profile walkers whitelist high|mid|low so the
1420
- * availability pick never misreads it), and these routes are their only door.
1421
- */
1422
- var INDEX_PATH = /^\/(\d+)\/strip-index$/;
1423
- var FILE_PATH = /^\/(\d+)\/strip$/;
1424
- /** `bytes=a-b` → [a, b] clamped to `size`, or null (serve the whole file). */
1425
- function parseRange(header, size) {
1426
- if (!header) return null;
1427
- const m = /^bytes=(\d+)-(\d+)?$/.exec(header);
1428
- if (!m) return null;
1429
- const start = Number(m[1]);
1430
- const end = m[2] === void 0 ? size - 1 : Math.min(Number(m[2]), size - 1);
1431
- if (start > end || start >= size) return null;
1432
- return [start, end];
1433
- }
1434
- function withStripRoutes(deps, next) {
1435
- return async (req, res) => {
1436
- const raw = req.url ?? "/";
1437
- const qs = raw.indexOf("?");
1438
- const pathname = qs === -1 ? raw : raw.slice(0, qs);
1439
- const params = new URLSearchParams(qs === -1 ? "" : raw.slice(qs + 1));
1440
- const asIndex = INDEX_PATH.exec(pathname);
1441
- const asFile = asIndex === null ? FILE_PATH.exec(pathname) : null;
1442
- if (asIndex === null && asFile === null) {
1443
- await next(req, res);
1444
- return;
1445
- }
1446
- const deviceId = Number((asIndex ?? asFile)[1]);
1447
- const hour = Number(params.get("hour"));
1448
- if (!Number.isFinite(hour) || hour <= 0) {
1449
- res.statusCode = 400;
1450
- res.end("strip: hour required");
1451
- return;
1452
- }
1453
- try {
1454
- if (asIndex !== null) {
1455
- const entries = await deps.store.index(deviceId, hour);
1456
- if (entries === null) {
1457
- res.statusCode = 204;
1458
- res.end();
1459
- return;
1460
- }
1461
- const body = encodeStripIndex(entries);
1462
- res.statusCode = 200;
1463
- res.setHeader("content-type", "application/octet-stream");
1464
- res.setHeader("content-length", String(body.length));
1465
- res.setHeader("content-encoding", "identity");
1466
- const immutable = hour + 36e5 < Date.now();
1467
- res.setHeader("cache-control", immutable ? "private, max-age=31536000, immutable" : "private, max-age=2");
1468
- res.end(Buffer.from(body));
1469
- deps.onLog?.("strip:index-served", {
1470
- deviceId,
1471
- hour,
1472
- entries: entries.length
1473
- });
1474
- return;
1475
- }
1476
- const abs = deps.store.absPathFor(deviceId, hour);
1477
- if (abs === null) {
1478
- res.statusCode = 204;
1479
- res.end();
1480
- return;
1481
- }
1482
- let size;
1483
- try {
1484
- size = (await node_fs.promises.stat(abs)).size;
1485
- } catch {
1486
- res.statusCode = 204;
1487
- res.end();
1488
- return;
1489
- }
1490
- const range = parseRange(req.headers.range, size);
1491
- const [start, end] = range ?? [0, size - 1];
1492
- res.statusCode = range === null ? 200 : 206;
1493
- res.setHeader("content-type", "application/octet-stream");
1494
- res.setHeader("content-length", String(end - start + 1));
1495
- res.setHeader("accept-ranges", "bytes");
1496
- res.setHeader("content-encoding", "identity");
1497
- if (range !== null) res.setHeader("content-range", `bytes ${start}-${end}/${size}`);
1498
- const stream = (0, node_fs.createReadStream)(abs, {
1499
- start,
1500
- end
1501
- });
1502
- stream.on("error", () => res.destroy());
1503
- stream.pipe(res);
1504
- } catch (err) {
1505
- deps.onLog?.("strip:failed", {
1506
- deviceId,
1507
- hour,
1508
- error: err instanceof Error ? err.message : String(err)
1509
- });
1510
- res.statusCode = 500;
1511
- res.end("strip: failed");
1512
- }
1513
- };
1514
- }
1515
- //#endregion
1516
- //#region src/recorder/addon/strip-actions.ts
1517
- /**
1518
- * Recorder custom actions: strip regeneration.
1519
- *
1520
- * `regenerateStrips` clears a camera's thumbnail strips and rebuilds them from
1521
- * the low segments already on disk — which is also the BACKFILL for footage
1522
- * recorded before strips existed (or before the operator opted the camera in).
1523
- *
1524
- * A custom action, not a cap method: the generated AppRouter sits at
1525
- * TypeScript's expansion limit, so caps cannot gain methods (docs/roadmap.md).
1526
- */
1527
- var RegenerateStripsInputSchema = require_dist.object({
1528
- deviceId: require_dist.number().int().nonnegative(),
1529
- /** Days back from now to rebuild (default {@link REGEN_DEFAULT_DAYS}). */
1530
- days: require_dist.number().int().positive().max(60).optional()
1531
- });
1532
- var RegenerateStripsResultSchema = require_dist.object({
1533
- /** False when a regeneration for this device is already running. */
1534
- started: require_dist.boolean(),
1535
- /** Segments queued for thumbnail extraction (0 when not started). */
1536
- segments: require_dist.number()
1537
- });
1538
- var recorderStripActions = require_dist.defineCustomActions({ regenerateStrips: require_dist.customAction(RegenerateStripsInputSchema, RegenerateStripsResultSchema, { kind: "mutation" }) });
1539
- //#endregion
1540
1059
  //#region src/recorder/segment-path.ts
1541
1060
  function segmentRelPath(deviceId, profile, startMs, durMs, bytes) {
1542
1061
  if (profile.includes("/")) throw new Error(`segmentRelPath: profile must not contain '/': ${profile}`);
@@ -4261,6 +3780,41 @@ var DEFAULT_WATCH_INTERVAL_MS = 2e3;
4261
3780
  /** Periodic re-evaluation to catch band boundaries crossed with no config change. */
4262
3781
  var TICK_MS = 3e4;
4263
3782
  /**
3783
+ * Bound on the broker lease-release RPC (`releaseStreamWithCodec`) during
3784
+ * teardown. The UDS request default is 60s, but the runner supervisor's
3785
+ * SIGTERM→SIGKILL grace (`$process.stop`/`restart` in the kernel
3786
+ * process-service) is 5s: one release hanging on a restarting broker — the
3787
+ * normal state during a redeploy — blew the grace and the supervisor
3788
+ * SIGKILLed the recorder mid-shutdown (observed 2026-08-01 04:08–04:24,
3789
+ * `signal=SIGKILL state=stopped` × 4). The SIGKILL orphans the ffmpeg
3790
+ * writers (they keep double-writing `.rec-tmp` against the respawned
3791
+ * runner's fresh writers) and releases NO lease at all — strictly worse
3792
+ * than dropping this one RPC. A timed-out release is safe to abandon:
3793
+ * killing the writer drops the source's only RTSP consumer, so the broker
3794
+ * idles the source out on its own.
3795
+ */
3796
+ var RELEASE_RPC_TIMEOUT_MS = 2e3;
3797
+ /**
3798
+ * Race `promise` against a deadline. Handlers stay attached to the losing
3799
+ * promise, so a post-deadline settlement can never surface as a process-level
3800
+ * `unhandledRejection`.
3801
+ */
3802
+ function withDeadline(promise, ms, label) {
3803
+ return new Promise((resolve, reject) => {
3804
+ const timer = setTimeout(() => {
3805
+ reject(/* @__PURE__ */ new Error(`${label} timed out after ${ms}ms`));
3806
+ }, ms);
3807
+ timer.unref?.();
3808
+ promise.then((value) => {
3809
+ clearTimeout(timer);
3810
+ resolve(value);
3811
+ }, (err) => {
3812
+ clearTimeout(timer);
3813
+ reject(err instanceof Error ? err : new Error(String(err)));
3814
+ });
3815
+ });
3816
+ }
3817
+ /**
4264
3818
  * Floor for "this writer is pinned but producing nothing".
4265
3819
  *
4266
3820
  * The give-up path only fires when ffmpeg EXITS. On 2026-07-29 the opposite
@@ -4470,7 +4024,18 @@ var RecordingController = class {
4470
4024
  lastMotionMs: source === "motion" ? atMs : prev.lastMotionMs,
4471
4025
  lastAudioMs: source === "audio" ? atMs : prev.lastAudioMs
4472
4026
  });
4473
- await this.evaluateDevice(deviceId);
4027
+ try {
4028
+ await this.evaluateDevice(deviceId);
4029
+ } catch (err) {
4030
+ this.deps.logger.warn("recorder: trigger attach deferred — stream not available yet (retries on next broker ready/tick)", {
4031
+ tags: { deviceId },
4032
+ meta: {
4033
+ source,
4034
+ atMs,
4035
+ error: require_dist.errMsg(err)
4036
+ }
4037
+ });
4038
+ }
4474
4039
  }
4475
4040
  /**
4476
4041
  * Decide-and-converge for one device: if its active band demands recording now
@@ -4639,9 +4204,9 @@ var RecordingController = class {
4639
4204
  r.writer.stop();
4640
4205
  } catch {}
4641
4206
  try {
4642
- await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey }, require_dist.nodePin(this.deps.ownerNodeId));
4207
+ await withDeadline(this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey }, require_dist.nodePin(this.deps.ownerNodeId)), RELEASE_RPC_TIMEOUT_MS, "releaseStreamWithCodec");
4643
4208
  } catch (err) {
4644
- this.deps.logger.warn("recorder: releaseStreamWithCodec failed", {
4209
+ this.deps.logger.warn("recorder: releaseStreamWithCodec failed — lease dropped unreleased (broker idles the source once the writer is gone)", {
4645
4210
  tags: { deviceId },
4646
4211
  meta: {
4647
4212
  profile: r.profile,
@@ -4657,7 +4222,7 @@ var RecordingController = class {
4657
4222
  if (!recordings) return;
4658
4223
  this.active.delete(deviceId);
4659
4224
  for (const r of recordings) this.lastSegmentAt.delete(idleKey(deviceId, r.profile));
4660
- for (const r of recordings) await this.teardownProfile(deviceId, r);
4225
+ await Promise.all(recordings.map((r) => this.teardownProfile(deviceId, r)));
4661
4226
  this.deps.logger.info("recorder unpinned broker source(s) — recording stopped", {
4662
4227
  tags: { deviceId },
4663
4228
  meta: { pipelineKeys: recordings.map((r) => r.pipelineKey) }
@@ -4672,7 +4237,7 @@ var RecordingController = class {
4672
4237
  }
4673
4238
  this.restore?.stop();
4674
4239
  this.restore = null;
4675
- for (const deviceId of Array.from(this.active.keys())) await this.detachDevice(deviceId);
4240
+ await Promise.all(Array.from(this.active.keys()).map((deviceId) => this.detachDevice(deviceId)));
4676
4241
  }
4677
4242
  };
4678
4243
  /** Build a `stream-broker` readiness scope for one node. */
@@ -4893,7 +4458,7 @@ var RelocateEngine = class {
4893
4458
  fromLocationId: from.id,
4894
4459
  toLocationId: to.id,
4895
4460
  deviceId: input.deviceId ?? null,
4896
- entities: input.entities ? [...input.entities] : ["segments", "strips"],
4461
+ entities: input.entities ? [...input.entities] : ["segments"],
4897
4462
  filesMoved: 0,
4898
4463
  bytesMoved: 0,
4899
4464
  filesTotal: null,
@@ -4911,8 +4476,7 @@ var RelocateEngine = class {
4911
4476
  const bytesPerMs = throttleMbps * 1024 * 1024 / 1e3;
4912
4477
  try {
4913
4478
  const segRows = job.entities.includes("segments") === false ? [] : this.deps.index.segmentsOnLocation(from.id).filter((r) => job.deviceId === null || r.deviceId === job.deviceId).sort((a, b) => a.startMs - b.startMs);
4914
- const stripFiles = job.entities.includes("strips") ? await listStripFiles(from.root, job.deviceId) : [];
4915
- job.filesTotal = segRows.length + stripFiles.length;
4479
+ job.filesTotal = segRows.length;
4916
4480
  for (const row of segRows) {
4917
4481
  if (job.cancelRequested) break;
4918
4482
  await this.moveSegment(row, from, to);
@@ -4921,15 +4485,6 @@ var RelocateEngine = class {
4921
4485
  if (job.filesMoved % PROGRESS_EVERY_FILES === 0) this.deps.onProgress?.(snapshot(job));
4922
4486
  await sleep(row.bytes / bytesPerMs);
4923
4487
  }
4924
- for (const rel of stripFiles) {
4925
- if (job.cancelRequested) break;
4926
- const src = node_path.default.join(from.root, rel);
4927
- const bytes = (await node_fs.promises.stat(src)).size;
4928
- await moveFile(src, node_path.default.join(to.root, rel), bytes);
4929
- job.filesMoved++;
4930
- job.bytesMoved += bytes;
4931
- await sleep(bytes / bytesPerMs);
4932
- }
4933
4488
  job.state = job.cancelRequested ? "cancelled" : "done";
4934
4489
  } catch (err) {
4935
4490
  job.state = "failed";
@@ -4954,29 +4509,6 @@ var RelocateEngine = class {
4954
4509
  });
4955
4510
  }
4956
4511
  };
4957
- /** The location-relative strip files under `<root>/<dev>/strips/…`, optionally
4958
- * scoped to one device. Missing dirs are data (no strips), not failures. */
4959
- async function listStripFiles(root, deviceId) {
4960
- const devices = [];
4961
- if (deviceId !== null) devices.push(String(deviceId));
4962
- else try {
4963
- for (const e of await node_fs.promises.readdir(root)) if (/^\d+$/.test(e)) devices.push(e);
4964
- } catch {
4965
- return [];
4966
- }
4967
- const out = [];
4968
- for (const dev of devices) {
4969
- const stripDir = node_path.default.join(root, dev, "strips");
4970
- let entries;
4971
- try {
4972
- entries = await node_fs.promises.readdir(stripDir, { recursive: true });
4973
- } catch {
4974
- continue;
4975
- }
4976
- for (const e of entries) if (e.endsWith(".strip")) out.push(node_path.default.join(dev, "strips", e.split(node_path.default.sep).join("/")));
4977
- }
4978
- return out;
4979
- }
4980
4512
  //#endregion
4981
4513
  //#region src/recorder/addon/footage-render.ts
4982
4514
  /**
@@ -5429,14 +4961,6 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
5429
4961
  /** Sync-sample tables captured at finalize, served by the directory so a
5430
4962
  * playback seek needs no mfra tail fetch. RAM-only, TTL-bounded. */
5431
4963
  mfraTables = new MfraTableStore();
5432
- /** Keyframe thumbnail strips (fast-drag scrub depth), one file per
5433
- * camera-hour under the LOW recordings root. */
5434
- stripStore = new StripStore(() => {
5435
- const writable = this.resolvedLocations.filter((l) => !l.readOnly);
5436
- return (writable.find((l) => l.type === "recordingsLow") ?? writable[0])?.root ?? null;
5437
- });
5438
- /** Bounded one-shot ffmpeg pool feeding the strip at finalize. */
5439
- stripCapture = new StripCapture({ onLog: (msg, data) => this.ctx.logger.debug(msg, { meta: data }) });
5440
4964
  /** Continuous + events band write-path controller (ffmpeg writers + watchers). */
5441
4965
  controller = null;
5442
4966
  /** Per-device motion/audio markers for playback (B3 events feed the controller). */
@@ -5510,23 +5034,12 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
5510
5034
  },
5511
5035
  onEvicted: (deviceId, atMs) => {
5512
5036
  this.calendar.invalidate(deviceId, atMs);
5513
- this.stripStore.removeHour(deviceId, atMs);
5514
5037
  },
5515
5038
  onIndexed: (row, absPath) => {
5516
- readMfraTable(absPath, row.bytes).then(async (captured) => {
5517
- this.mfraTables.record(row.deviceId, row.profile, row.startMs, captured.table);
5518
- if (row.profile !== "low") return;
5519
- if ((await loadDeviceConfig(this.configStore(), row.deviceId)).stripsEnabled !== true) return;
5520
- const jpegs = await this.stripCapture.capture(absPath);
5521
- if (jpegs === null || jpegs.length === 0) return;
5522
- const instants = keyframeInstants(row.startMs, row.durMs, captured);
5523
- const thumbs = jpegs.slice(0, instants.length).map((jpeg, i) => ({
5524
- tsMs: instants[i] ?? row.startMs,
5525
- jpeg
5526
- }));
5527
- await this.stripStore.append(row.deviceId, thumbs);
5039
+ readMfraTable(absPath, row.bytes).then((table) => {
5040
+ this.mfraTables.record(row.deviceId, row.profile, row.startMs, table);
5528
5041
  }).catch((err) => {
5529
- this.ctx.logger.debug("mfra/strip capture failed", {
5042
+ this.ctx.logger.debug("mfra capture failed", {
5530
5043
  tags: { deviceId: row.deviceId },
5531
5044
  meta: {
5532
5045
  path: row.path,
@@ -5718,14 +5231,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
5718
5231
  downloadUrlFor: (id) => `/addon/${RECORDER_ADDON_ID}/${EXPORTS_PREFIX}/${id}.mp4`
5719
5232
  });
5720
5233
  try {
5721
- const handler = withStripRoutes({
5722
- store: this.stripStore,
5723
- onLog: (msg, meta) => this.ctx.logger.debug(msg, { meta })
5724
- }, withDirectoryRoute({
5234
+ const handler = withDirectoryRoute({
5725
5235
  source: this.calendar,
5726
5236
  mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
5727
5237
  onLog: (msg, meta) => this.ctx.logger.debug(msg, { meta })
5728
- }, require_model_download_service_Cp9f4dk6.createFileDataPlaneHandler({ getRoots: () => this.playbackRoots() })));
5238
+ }, require_model_download_service_Cp9f4dk6.createFileDataPlaneHandler({ getRoots: () => this.playbackRoots() }));
5729
5239
  const served = await this.ctx.dataPlane?.serve({
5730
5240
  prefix: PLAYBACK_PREFIX,
5731
5241
  access: "authenticated",
@@ -5780,24 +5290,20 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
5780
5290
  }
5781
5291
  });
5782
5292
  warnLostBands(this.configStore(), this.ctx.logger);
5783
- return {
5784
- providers: [
5785
- {
5786
- capability: require_dist.recordingCapability,
5787
- provider: recordingProvider
5788
- },
5789
- {
5790
- capability: require_dist.storageEvictableCapability,
5791
- provider: evictableProvider
5792
- },
5793
- {
5794
- capability: require_dist.recordingExportCapability,
5795
- provider: exportProvider
5796
- }
5797
- ],
5798
- customActions: recorderStripActions,
5799
- actionHandlers: { regenerateStrips: async (input) => this.regenerateStrips(RegenerateStripsInputSchema.parse(input)) }
5800
- };
5293
+ return { providers: [
5294
+ {
5295
+ capability: require_dist.recordingCapability,
5296
+ provider: recordingProvider
5297
+ },
5298
+ {
5299
+ capability: require_dist.storageEvictableCapability,
5300
+ provider: evictableProvider
5301
+ },
5302
+ {
5303
+ capability: require_dist.recordingExportCapability,
5304
+ provider: exportProvider
5305
+ }
5306
+ ] };
5801
5307
  }
5802
5308
  async onShutdown() {
5803
5309
  this.eventUnsub?.();
@@ -6166,73 +5672,6 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
6166
5672
  if (loc === void 0) throw new Error(`recorder scrub: unknown recordings location ${row.locationId}`);
6167
5673
  return node_path.default.join(loc.root, row.path);
6168
5674
  }
6169
- /** Devices with a strip regeneration in flight — one at a time per device. */
6170
- stripRegenRunning = /* @__PURE__ */ new Set();
6171
- /**
6172
- * Clear one camera's strips and rebuild them from the low segments on disk —
6173
- * the backfill for footage recorded before strips were enabled. Walks the
6174
- * RECENT window (default {@link REGEN_DEFAULT_DAYS} days), newest hour
6175
- * first, so the part scrubbing actually visits is ready soonest. Background;
6176
- * the one-shot ffmpeg pool bounds the decode concurrency.
6177
- */
6178
- async regenerateStrips(input) {
6179
- const { deviceId } = input;
6180
- if (this.stripRegenRunning.has(deviceId)) return {
6181
- started: false,
6182
- segments: 0
6183
- };
6184
- const days = input.days ?? 3;
6185
- const cutoff = Date.now() - days * 864e5;
6186
- const rows = this.index.segments(deviceId, "low").filter((r) => r.startMs >= cutoff).toSorted((a, b) => b.startMs - a.startMs);
6187
- this.stripRegenRunning.add(deviceId);
6188
- await this.stripStore.removeDevice(deviceId);
6189
- (async () => {
6190
- let done = 0;
6191
- let failed = 0;
6192
- for (const row of rows) {
6193
- try {
6194
- const abs = this.resolveSegmentAbsPath(row);
6195
- const captured = await readMfraTable(abs, row.bytes);
6196
- const jpegs = await this.stripCapture.capture(abs);
6197
- if (jpegs === null || jpegs.length === 0) {
6198
- failed++;
6199
- continue;
6200
- }
6201
- const instants = keyframeInstants(row.startMs, row.durMs, captured);
6202
- const thumbs = jpegs.slice(0, instants.length).map((jpeg, i) => ({
6203
- tsMs: instants[i] ?? row.startMs,
6204
- jpeg
6205
- }));
6206
- await this.stripStore.append(deviceId, thumbs);
6207
- done++;
6208
- } catch {
6209
- failed++;
6210
- }
6211
- if ((done + failed) % 500 === 0) this.ctx.logger.info("strip regeneration progress", {
6212
- tags: { deviceId },
6213
- meta: {
6214
- done,
6215
- failed,
6216
- total: rows.length
6217
- }
6218
- });
6219
- }
6220
- this.stripRegenRunning.delete(deviceId);
6221
- this.ctx.logger.info("strip regeneration complete", {
6222
- tags: { deviceId },
6223
- meta: {
6224
- done,
6225
- failed,
6226
- total: rows.length,
6227
- days
6228
- }
6229
- });
6230
- })();
6231
- return {
6232
- started: true,
6233
- segments: rows.length
6234
- };
6235
- }
6236
5675
  /**
6237
5676
  * Dedicated export MP4 download handler. Resolves `<exportId>.mp4` to a `ready`
6238
5677
  * row's file (traversal-guarded by the id regex + the exact stored path),
@@ -6319,5 +5758,4 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
6319
5758
  }
6320
5759
  };
6321
5760
  //#endregion
6322
- exports.customActions = recorderStripActions;
6323
- exports.default = RecorderV2Addon;
5761
+ module.exports = RecorderV2Addon;