@camstack/addon-pipeline 1.2.132 → 1.2.133

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 (36) hide show
  1. package/dist/{addon-utils-DzCn6z7v.js → addon-utils-FV2O25id.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-BZYUUBUx.mjs → dist-7Uc6NdFm.mjs} +653 -65
  7. package/dist/{dist-DBP47QOz.js → dist-DsnZUYTs.js} +670 -64
  8. package/dist/{event-loop-stall-monitor-CiH_MHfO.js → event-loop-stall-monitor-D86J6d2c.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-IACE22fK.mjs → event-loop-stall-monitor-XzDMVU9d.mjs} +1 -1
  10. package/dist/{lazy-sharp-B_kETjyQ.js → lazy-sharp-UzEUL79V.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-C7rsraCR.mjs → process-memory-Ci90-zF_.mjs} +1 -1
  16. package/dist/{process-memory-CtXQAA3d.js → process-memory-Co59RQIK.js} +1 -1
  17. package/dist/recorder/index.js +446 -86
  18. package/dist/recorder/index.mjs +445 -85
  19. package/dist/{segment-demux-js-BL1UDdiI.mjs → segment-demux-js-0NZ9TXro.mjs} +1 -1
  20. package/dist/{segment-demux-js-wTzFi5UW.js → segment-demux-js-DKcc0bxA.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +195 -13
  22. package/dist/session-decode/decode-worker-child.mjs +194 -12
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DlKJZ-_M.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dqj2f3H7.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-CVBnNONy.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-BUw_aTfK.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-77rGxv1J.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-kDKgfUjp.mjs → hostInit-DnwyBL4I.mjs} +3 -3
  30. package/dist/stream-broker/index.js +2 -2
  31. package/dist/stream-broker/index.mjs +2 -2
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-Cut8984B.mjs → worker-protocol-CMDUAqvX.mjs} +12 -6
  34. package/dist/{worker-protocol-C3zaz0Jv.js → worker-protocol-DzRUHIcM.js} +12 -6
  35. package/package.json +1 -1
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DXKTNEae.mjs +0 -26
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { gt as errMsg } from "./dist-BZYUUBUx.mjs";
2
+ import { yt as errMsg } from "./dist-7Uc6NdFm.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-DBP47QOz.js");
1
+ const require_dist = require("./dist-DsnZUYTs.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-C3zaz0Jv.js");
3
- const require_lazy_sharp = require("../lazy-sharp-B_kETjyQ.js");
2
+ const require_worker_protocol = require("../worker-protocol-DzRUHIcM.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-UzEUL79V.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.
@@ -792,11 +792,24 @@ var NativeTileStore = class {
792
792
  ttlMs;
793
793
  now;
794
794
  bytes = 0;
795
+ /**
796
+ * Frames whose tiles were dropped by the BUDGET before their TTL ran out.
797
+ *
798
+ * Counted, not silent: budget eviction is the one path here that destroys
799
+ * retained work, and on the scene store a dropped entry is a key frame that
800
+ * can never be captured again. `0` means the TTL is what bounds this store —
801
+ * anything else is the signal that its budget is the binding limit.
802
+ */
803
+ evicted = 0;
795
804
  constructor(options) {
796
805
  this.budgetBytes = options.budgetBytes;
797
806
  this.ttlMs = options.ttlMs;
798
807
  this.now = options.now ?? Date.now;
799
808
  }
809
+ /** Frames dropped by the byte budget rather than by the TTL (tests / metrics). */
810
+ get evictedCount() {
811
+ return this.evicted;
812
+ }
800
813
  /** `false` when tiles are disabled (`budgetBytes <= 0`). */
801
814
  get enabled() {
802
815
  return this.budgetBytes > 0;
@@ -837,6 +850,7 @@ var NativeTileStore = class {
837
850
  const oldest = this.entries.keys().next().value;
838
851
  if (oldest === void 0 || oldest === frameId) break;
839
852
  this.drop(oldest);
853
+ this.evicted += 1;
840
854
  }
841
855
  return true;
842
856
  }
@@ -1078,6 +1092,107 @@ function resolveRgbCropSource(plane, planeStride, region) {
1078
1092
  };
1079
1093
  }
1080
1094
  //#endregion
1095
+ //#region src/session-decode/scene-tile.ts
1096
+ /**
1097
+ * The whole frame, in the same normalized space every other cut uses. Identical
1098
+ * to the bbox `getNativeCrop`'s full-frame callers send, which is the point —
1099
+ * the rectangle asked for and the rectangle retained are the same one.
1100
+ */
1101
+ var SCENE_TILE_BBOX = {
1102
+ x: 0,
1103
+ y: 0,
1104
+ w: 1,
1105
+ h: 1
1106
+ };
1107
+ /**
1108
+ * Plan one frame's cut.
1109
+ *
1110
+ * The scene rectangle is appended to the SAME cut rather than taken separately,
1111
+ * because `subject-tile-cut.ts` reads every rectangle in one synchronous turn:
1112
+ * a second read after the first cut's encodes have awaited is exactly the
1113
+ * eviction window that once cut subject 1 of each frame and lost the rest.
1114
+ *
1115
+ * A frame that detected NOTHING gets nothing, scene tile included. That
1116
+ * asymmetry is what keeps this tier affordable — the frames a key-frame capture
1117
+ * can ever name are the frames that carried a best detection, and on this
1118
+ * cluster those are ~0.4/s per camera against ~2.1/s delivered.
1119
+ */
1120
+ function planFrameCuts(subjects, conditions) {
1121
+ if (!conditions.sceneEnabled || !conditions.hasDetections) return {
1122
+ bboxes: subjects,
1123
+ scene: false
1124
+ };
1125
+ return {
1126
+ bboxes: [...subjects, SCENE_TILE_BBOX],
1127
+ scene: true
1128
+ };
1129
+ }
1130
+ /**
1131
+ * Does this region cover the whole frame?
1132
+ *
1133
+ * Judged on the RESOLVED region rather than on the position in the cut list,
1134
+ * because `cutSubjectTiles` drops a rectangle whose read or encode failed and
1135
+ * positional identity does not survive that. Unknown frame dimensions (a freed
1136
+ * node-av frame reports 0×0) are `false` — classifying against those would file
1137
+ * every subject tile as the scene.
1138
+ */
1139
+ function isSceneRegion(region, frameWidth, frameHeight) {
1140
+ if (frameWidth <= 0 || frameHeight <= 0) return false;
1141
+ return region.left <= 0 && region.top <= 0 && region.width >= frameWidth && region.height >= frameHeight;
1142
+ }
1143
+ /**
1144
+ * Route each cut tile to the store that owns it.
1145
+ *
1146
+ * The FIRST frame-spanning tile is the scene; any later one stays a subject. A
1147
+ * padded subject that already covers the frame genuinely IS the scene, and both
1148
+ * stores are consulted on a serve, so claiming one of them costs no hit and
1149
+ * keeps the two stores from holding the same pixels twice.
1150
+ */
1151
+ function partitionSceneTile(tiles, frameWidth, frameHeight) {
1152
+ const subjects = [];
1153
+ let scene = null;
1154
+ for (const tile of tiles) {
1155
+ if (scene === null && isSceneRegion(tile.region, frameWidth, frameHeight)) {
1156
+ scene = tile;
1157
+ continue;
1158
+ }
1159
+ subjects.push(tile);
1160
+ }
1161
+ return {
1162
+ subjects,
1163
+ scene
1164
+ };
1165
+ }
1166
+ /**
1167
+ * Pick the retained tile that CONTAINS `rect`, subject tiles first.
1168
+ *
1169
+ * The order is the whole of the policy and it is not cosmetic. Both rungs are
1170
+ * native-resolution, so either would be honest — but a subject tile is
1171
+ * ~60-120 KB against a scene tile's ~1.5-2.5 MB, and the extract runs on
1172
+ * whatever it is handed. Asking the subject store first keeps an ordinary
1173
+ * face/plate crop off the 20×-larger raster; the scene tile is consulted only
1174
+ * for the rectangles no subject tile could ever contain, which is exactly the
1175
+ * set that had no rung at all before it existed.
1176
+ *
1177
+ * `null` means no retained rung contains the rectangle — a MISS, never a clamp
1178
+ * into the nearest tile ([D52](../../../../docs/decisions/adr-0052.md)).
1179
+ */
1180
+ function selectRetainedTile(subjects, scenes, rect) {
1181
+ const fromSubject = selectTileForRect(subjects, rect);
1182
+ if (fromSubject) return {
1183
+ tile: fromSubject.tile,
1184
+ local: fromSubject.local,
1185
+ source: "subject"
1186
+ };
1187
+ const fromScene = selectTileForRect(scenes, rect);
1188
+ if (fromScene) return {
1189
+ tile: fromScene.tile,
1190
+ local: fromScene.local,
1191
+ source: "scene"
1192
+ };
1193
+ return null;
1194
+ }
1195
+ //#endregion
1081
1196
  //#region src/session-decode/subject-tile-cut.ts
1082
1197
  function failureReason(err) {
1083
1198
  return err instanceof Error ? err.message : String(err);
@@ -1226,6 +1341,14 @@ var NATIVE_LEASE_HOLD_FRAMES = NATIVE_LEASE_KNOBS.values.holdFrames;
1226
1341
  */
1227
1342
  var NATIVE_TILE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.tileBudgetMb * 1024 * 1024;
1228
1343
  /**
1344
+ * Hard RAM budget (bytes) for the SCENE TILE store — one JPEG of the whole
1345
+ * native frame per detection-bearing frame, which is the only thing that can
1346
+ * answer a FULL-FRAME native request after the hold ends. Separate from the
1347
+ * subject budget so a key frame can never evict a face tile. `0` disables the
1348
+ * rung. See `scene-tile.ts` for the measurements that sized it.
1349
+ */
1350
+ var NATIVE_SCENE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.sceneBudgetMb * 1024 * 1024;
1351
+ /**
1229
1352
  * How long a subject tile is served. A TTL is honest HERE and dishonest on the
1230
1353
  * hold: nothing is waiting on a tile, it is speculative retention against a
1231
1354
  * request that may never come. Sized an order of magnitude above the measured
@@ -1563,11 +1686,33 @@ var DecodeWorkerChild = class {
1563
1686
  budgetBytes: NATIVE_TILE_BUDGET_BYTES,
1564
1687
  ttlMs: NATIVE_TILE_TTL_MS
1565
1688
  });
1689
+ /**
1690
+ * What survives a held frame for a FULL-FRAME request: one JPEG of the whole
1691
+ * native raster, cut in the SAME synchronous read phase as the subject tiles.
1692
+ *
1693
+ * A separate store, not a fifth entry in {@link tileStore}, because a scene
1694
+ * tile is ~20× a subject tile: one shared budget would let a busy camera's
1695
+ * key frames evict the face/plate tiles the recognisers live on. Two stores
1696
+ * make starving that tier impossible rather than unlikely.
1697
+ */
1698
+ sceneStore = new NativeTileStore({
1699
+ budgetBytes: NATIVE_SCENE_BUDGET_BYTES,
1700
+ ttlMs: NATIVE_TILE_TTL_MS
1701
+ });
1566
1702
  /** Tiles cut / served / refused-for-containment, on the throughput line. */
1567
1703
  tilesCut = 0;
1568
1704
  tileHits = 0;
1569
1705
  tileMisses = 0;
1570
1706
  tileCutFailures = 0;
1707
+ /** Scene tiles cut / served, on the throughput line. */
1708
+ scenesCut = 0;
1709
+ sceneHits = 0;
1710
+ /**
1711
+ * Full-frame cuts NOT taken because the rung is switched off
1712
+ * (`sceneBudgetMb: 0`). A branch that drops work counts it — this one drops a
1713
+ * key frame that can never be captured again.
1714
+ */
1715
+ sceneRefused = 0;
1571
1716
  /**
1572
1717
  * Native frame dimensions the retained tiles were cut from. A session's
1573
1718
  * resolution does not change (a re-dial clears the store), so ONE pair is
@@ -1653,6 +1798,9 @@ var DecodeWorkerChild = class {
1653
1798
  * un-guarded version a log flood rather than a diagnosis.
1654
1799
  */
1655
1800
  warnedTileEncoderUnavailable = false;
1801
+ /** One-shot: the scene rung is off by configuration, which cannot change under
1802
+ * a running worker. The VOLUME lives on `sceneRefused`. */
1803
+ warnedSceneDisabled = false;
1656
1804
  /**
1657
1805
  * The last subject-tile-cut failure REASON logged — see
1658
1806
  * {@link reportTileCutFailure}. Compared, not counted: the count lives on the
@@ -1781,7 +1929,7 @@ var DecodeWorkerChild = class {
1781
1929
  const skipped = this.framesSkipped + this.frames.droppedCount;
1782
1930
  const rssMb = Math.round(process.memoryUsage().rss / 1048576);
1783
1931
  const ageS = Math.round((now - this.startedAt) / 1e3);
1784
- this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
1932
+ this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
1785
1933
  this.lastMetricsAt = now;
1786
1934
  this.lastDeliveredSnapshot = this.framesDelivered;
1787
1935
  if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
@@ -1923,7 +2071,7 @@ var DecodeWorkerChild = class {
1923
2071
  this.send({
1924
2072
  kind: "nativeCropMiss",
1925
2073
  requestId,
1926
- reason: this.tileStore.enabled ? `frame ${frameId} released and no retained subject tile contains the requested rectangle` : `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`
2074
+ reason: this.tileMissReason(frameId)
1927
2075
  });
1928
2076
  } catch (err) {
1929
2077
  this.tileMisses += 1;
@@ -1937,6 +2085,18 @@ var DecodeWorkerChild = class {
1937
2085
  });
1938
2086
  }
1939
2087
  }
2088
+ /**
2089
+ * Why nothing retained could answer — named per RUNG, because they have
2090
+ * different fixes. "Subject tiles are off" and "the scene rung is off" and
2091
+ * "the rectangle is outside every container" are three different sentences,
2092
+ * and one "not retained" hid all of them.
2093
+ */
2094
+ tileMissReason(frameId) {
2095
+ if (!this.tileStore.enabled && !this.sceneStore.enabled) return `frame ${frameId} released and retention is DISABLED (tileBudgetMb 0, sceneBudgetMb 0)`;
2096
+ if (!this.sceneStore.enabled) return `frame ${frameId} released and no retained subject tile contains the requested rectangle; scene tiles are DISABLED (sceneBudgetMb 0), so a full-frame request cannot be served at all`;
2097
+ if (!this.tileStore.enabled) return `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`;
2098
+ return `frame ${frameId} released and no retained subject or scene tile contains the requested rectangle`;
2099
+ }
1940
2100
  /** The GPU-ring or reserved-slot frame for `frameId` (hold-disabled / newest paths). */
1941
2101
  resolveRetainedFrame(frameId) {
1942
2102
  return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
@@ -1963,8 +2123,8 @@ var DecodeWorkerChild = class {
1963
2123
  */
1964
2124
  async handleCutTiles(frameId, bboxes) {
1965
2125
  try {
1966
- if (!this.tileStore.enabled || bboxes.length === 0) return;
1967
- const wanted = bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME);
2126
+ if (!this.tileStore.enabled && !this.sceneStore.enabled || bboxes.length === 0) return;
2127
+ const wanted = this.tileStore.enabled ? bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME) : [];
1968
2128
  if (bboxes.length > wanted.length) this.emitStderr(`decode-worker-child: tile cut capped at ${NATIVE_TILE_MAX_PER_FRAME} of ${bboxes.length} subjects on frame ${frameId} — the rest fall back to the ≤640 tier\n`);
1969
2129
  const sharp = await this.tileEncoder();
1970
2130
  const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
@@ -1983,7 +2143,18 @@ var DecodeWorkerChild = class {
1983
2143
  }
1984
2144
  this.tileFrameWidth = frame.width;
1985
2145
  this.tileFrameHeight = frame.height;
1986
- const tiles = await cutSubjectTiles(frame, wanted, {
2146
+ const plan = planFrameCuts(wanted, {
2147
+ sceneEnabled: this.sceneStore.enabled,
2148
+ hasDetections: bboxes.length > 0
2149
+ });
2150
+ if (!plan.scene) {
2151
+ this.sceneRefused += 1;
2152
+ if (!this.warnedSceneDisabled) {
2153
+ this.warnedSceneDisabled = true;
2154
+ this.emitStderr("decode-worker-child: scene tiles DISABLED (sceneBudgetMb 0) — every late FULL-FRAME request (native keyFrame, detail frameJpeg, gallery fallback) will miss once this frame is released; refusals are counted as sceneRefused\n");
2155
+ }
2156
+ }
2157
+ const cut = await cutSubjectTiles(frame, plan.bboxes, {
1987
2158
  isLive: (f) => this.isRetainedFrameLive(f),
1988
2159
  cutRaw: (f, bbox) => {
1989
2160
  const { region } = resolveNativeTileRegion(f.width, f.height, bbox, NATIVE_TILE_PADDING_RATIO);
@@ -2017,10 +2188,18 @@ var DecodeWorkerChild = class {
2017
2188
  this.reportTileCutFailure(frameId, reason);
2018
2189
  }
2019
2190
  });
2020
- this.tileCutFailures += wanted.length - tiles.length;
2021
- if (tiles.length > 0) {
2022
- this.tilesCut += tiles.length;
2023
- this.tileStore.put(frameId, tiles);
2191
+ this.tileCutFailures += plan.bboxes.length - cut.length;
2192
+ const { subjects, scene } = plan.scene ? partitionSceneTile(cut, frame.width, frame.height) : {
2193
+ subjects: cut,
2194
+ scene: null
2195
+ };
2196
+ if (subjects.length > 0) {
2197
+ this.tilesCut += subjects.length;
2198
+ this.tileStore.put(frameId, subjects);
2199
+ }
2200
+ if (scene !== null) {
2201
+ this.scenesCut += 1;
2202
+ this.sceneStore.put(frameId, [scene]);
2024
2203
  }
2025
2204
  } finally {
2026
2205
  this.leaseStore.release(frameId);
@@ -2103,14 +2282,16 @@ var DecodeWorkerChild = class {
2103
2282
  * which would enrich a different part of the scene and error nowhere.
2104
2283
  */
2105
2284
  async serveFromTile(frameId, bbox, maxWidth) {
2285
+ const scenes = this.sceneStore.get(frameId);
2106
2286
  const tiles = this.tileStore.get(frameId);
2107
- if (tiles.length === 0) return null;
2287
+ if (tiles.length === 0 && scenes.length === 0) return null;
2108
2288
  const width = this.tileFrameWidth;
2109
2289
  const height = this.tileFrameHeight;
2110
2290
  if (width <= 0 || height <= 0) return null;
2111
2291
  const { region, target } = resolveNativeCropGeometry(width, height, bbox, maxWidth);
2112
- const served = selectTileForRect(tiles, region);
2292
+ const served = selectRetainedTile(tiles, scenes, region);
2113
2293
  if (!served) return null;
2294
+ if (served.source === "scene") this.sceneHits += 1;
2114
2295
  let pipeline = (await require_lazy_sharp.getSharp())(served.tile.bytes).extract({
2115
2296
  left: served.local.left,
2116
2297
  top: served.local.top,
@@ -2767,6 +2948,7 @@ var DecodeWorkerChild = class {
2767
2948
  this.nativeRing.clear();
2768
2949
  this.leaseStore.clear();
2769
2950
  this.tileStore.clear();
2951
+ this.sceneStore.clear();
2770
2952
  this.tileFrameWidth = 0;
2771
2953
  this.tileFrameHeight = 0;
2772
2954
  this.nativeLeaseDownloadFilter?.close();
@@ -1,4 +1,4 @@
1
- import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-Cut8984B.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-CMDUAqvX.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
  /**
@@ -791,11 +791,24 @@ var NativeTileStore = class {
791
791
  ttlMs;
792
792
  now;
793
793
  bytes = 0;
794
+ /**
795
+ * Frames whose tiles were dropped by the BUDGET before their TTL ran out.
796
+ *
797
+ * Counted, not silent: budget eviction is the one path here that destroys
798
+ * retained work, and on the scene store a dropped entry is a key frame that
799
+ * can never be captured again. `0` means the TTL is what bounds this store —
800
+ * anything else is the signal that its budget is the binding limit.
801
+ */
802
+ evicted = 0;
794
803
  constructor(options) {
795
804
  this.budgetBytes = options.budgetBytes;
796
805
  this.ttlMs = options.ttlMs;
797
806
  this.now = options.now ?? Date.now;
798
807
  }
808
+ /** Frames dropped by the byte budget rather than by the TTL (tests / metrics). */
809
+ get evictedCount() {
810
+ return this.evicted;
811
+ }
799
812
  /** `false` when tiles are disabled (`budgetBytes <= 0`). */
800
813
  get enabled() {
801
814
  return this.budgetBytes > 0;
@@ -836,6 +849,7 @@ var NativeTileStore = class {
836
849
  const oldest = this.entries.keys().next().value;
837
850
  if (oldest === void 0 || oldest === frameId) break;
838
851
  this.drop(oldest);
852
+ this.evicted += 1;
839
853
  }
840
854
  return true;
841
855
  }
@@ -1077,6 +1091,107 @@ function resolveRgbCropSource(plane, planeStride, region) {
1077
1091
  };
1078
1092
  }
1079
1093
  //#endregion
1094
+ //#region src/session-decode/scene-tile.ts
1095
+ /**
1096
+ * The whole frame, in the same normalized space every other cut uses. Identical
1097
+ * to the bbox `getNativeCrop`'s full-frame callers send, which is the point —
1098
+ * the rectangle asked for and the rectangle retained are the same one.
1099
+ */
1100
+ var SCENE_TILE_BBOX = {
1101
+ x: 0,
1102
+ y: 0,
1103
+ w: 1,
1104
+ h: 1
1105
+ };
1106
+ /**
1107
+ * Plan one frame's cut.
1108
+ *
1109
+ * The scene rectangle is appended to the SAME cut rather than taken separately,
1110
+ * because `subject-tile-cut.ts` reads every rectangle in one synchronous turn:
1111
+ * a second read after the first cut's encodes have awaited is exactly the
1112
+ * eviction window that once cut subject 1 of each frame and lost the rest.
1113
+ *
1114
+ * A frame that detected NOTHING gets nothing, scene tile included. That
1115
+ * asymmetry is what keeps this tier affordable — the frames a key-frame capture
1116
+ * can ever name are the frames that carried a best detection, and on this
1117
+ * cluster those are ~0.4/s per camera against ~2.1/s delivered.
1118
+ */
1119
+ function planFrameCuts(subjects, conditions) {
1120
+ if (!conditions.sceneEnabled || !conditions.hasDetections) return {
1121
+ bboxes: subjects,
1122
+ scene: false
1123
+ };
1124
+ return {
1125
+ bboxes: [...subjects, SCENE_TILE_BBOX],
1126
+ scene: true
1127
+ };
1128
+ }
1129
+ /**
1130
+ * Does this region cover the whole frame?
1131
+ *
1132
+ * Judged on the RESOLVED region rather than on the position in the cut list,
1133
+ * because `cutSubjectTiles` drops a rectangle whose read or encode failed and
1134
+ * positional identity does not survive that. Unknown frame dimensions (a freed
1135
+ * node-av frame reports 0×0) are `false` — classifying against those would file
1136
+ * every subject tile as the scene.
1137
+ */
1138
+ function isSceneRegion(region, frameWidth, frameHeight) {
1139
+ if (frameWidth <= 0 || frameHeight <= 0) return false;
1140
+ return region.left <= 0 && region.top <= 0 && region.width >= frameWidth && region.height >= frameHeight;
1141
+ }
1142
+ /**
1143
+ * Route each cut tile to the store that owns it.
1144
+ *
1145
+ * The FIRST frame-spanning tile is the scene; any later one stays a subject. A
1146
+ * padded subject that already covers the frame genuinely IS the scene, and both
1147
+ * stores are consulted on a serve, so claiming one of them costs no hit and
1148
+ * keeps the two stores from holding the same pixels twice.
1149
+ */
1150
+ function partitionSceneTile(tiles, frameWidth, frameHeight) {
1151
+ const subjects = [];
1152
+ let scene = null;
1153
+ for (const tile of tiles) {
1154
+ if (scene === null && isSceneRegion(tile.region, frameWidth, frameHeight)) {
1155
+ scene = tile;
1156
+ continue;
1157
+ }
1158
+ subjects.push(tile);
1159
+ }
1160
+ return {
1161
+ subjects,
1162
+ scene
1163
+ };
1164
+ }
1165
+ /**
1166
+ * Pick the retained tile that CONTAINS `rect`, subject tiles first.
1167
+ *
1168
+ * The order is the whole of the policy and it is not cosmetic. Both rungs are
1169
+ * native-resolution, so either would be honest — but a subject tile is
1170
+ * ~60-120 KB against a scene tile's ~1.5-2.5 MB, and the extract runs on
1171
+ * whatever it is handed. Asking the subject store first keeps an ordinary
1172
+ * face/plate crop off the 20×-larger raster; the scene tile is consulted only
1173
+ * for the rectangles no subject tile could ever contain, which is exactly the
1174
+ * set that had no rung at all before it existed.
1175
+ *
1176
+ * `null` means no retained rung contains the rectangle — a MISS, never a clamp
1177
+ * into the nearest tile ([D52](../../../../docs/decisions/adr-0052.md)).
1178
+ */
1179
+ function selectRetainedTile(subjects, scenes, rect) {
1180
+ const fromSubject = selectTileForRect(subjects, rect);
1181
+ if (fromSubject) return {
1182
+ tile: fromSubject.tile,
1183
+ local: fromSubject.local,
1184
+ source: "subject"
1185
+ };
1186
+ const fromScene = selectTileForRect(scenes, rect);
1187
+ if (fromScene) return {
1188
+ tile: fromScene.tile,
1189
+ local: fromScene.local,
1190
+ source: "scene"
1191
+ };
1192
+ return null;
1193
+ }
1194
+ //#endregion
1080
1195
  //#region src/session-decode/subject-tile-cut.ts
1081
1196
  function failureReason(err) {
1082
1197
  return err instanceof Error ? err.message : String(err);
@@ -1225,6 +1340,14 @@ var NATIVE_LEASE_HOLD_FRAMES = NATIVE_LEASE_KNOBS.values.holdFrames;
1225
1340
  */
1226
1341
  var NATIVE_TILE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.tileBudgetMb * 1024 * 1024;
1227
1342
  /**
1343
+ * Hard RAM budget (bytes) for the SCENE TILE store — one JPEG of the whole
1344
+ * native frame per detection-bearing frame, which is the only thing that can
1345
+ * answer a FULL-FRAME native request after the hold ends. Separate from the
1346
+ * subject budget so a key frame can never evict a face tile. `0` disables the
1347
+ * rung. See `scene-tile.ts` for the measurements that sized it.
1348
+ */
1349
+ var NATIVE_SCENE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.sceneBudgetMb * 1024 * 1024;
1350
+ /**
1228
1351
  * How long a subject tile is served. A TTL is honest HERE and dishonest on the
1229
1352
  * hold: nothing is waiting on a tile, it is speculative retention against a
1230
1353
  * request that may never come. Sized an order of magnitude above the measured
@@ -1562,11 +1685,33 @@ var DecodeWorkerChild = class {
1562
1685
  budgetBytes: NATIVE_TILE_BUDGET_BYTES,
1563
1686
  ttlMs: NATIVE_TILE_TTL_MS
1564
1687
  });
1688
+ /**
1689
+ * What survives a held frame for a FULL-FRAME request: one JPEG of the whole
1690
+ * native raster, cut in the SAME synchronous read phase as the subject tiles.
1691
+ *
1692
+ * A separate store, not a fifth entry in {@link tileStore}, because a scene
1693
+ * tile is ~20× a subject tile: one shared budget would let a busy camera's
1694
+ * key frames evict the face/plate tiles the recognisers live on. Two stores
1695
+ * make starving that tier impossible rather than unlikely.
1696
+ */
1697
+ sceneStore = new NativeTileStore({
1698
+ budgetBytes: NATIVE_SCENE_BUDGET_BYTES,
1699
+ ttlMs: NATIVE_TILE_TTL_MS
1700
+ });
1565
1701
  /** Tiles cut / served / refused-for-containment, on the throughput line. */
1566
1702
  tilesCut = 0;
1567
1703
  tileHits = 0;
1568
1704
  tileMisses = 0;
1569
1705
  tileCutFailures = 0;
1706
+ /** Scene tiles cut / served, on the throughput line. */
1707
+ scenesCut = 0;
1708
+ sceneHits = 0;
1709
+ /**
1710
+ * Full-frame cuts NOT taken because the rung is switched off
1711
+ * (`sceneBudgetMb: 0`). A branch that drops work counts it — this one drops a
1712
+ * key frame that can never be captured again.
1713
+ */
1714
+ sceneRefused = 0;
1570
1715
  /**
1571
1716
  * Native frame dimensions the retained tiles were cut from. A session's
1572
1717
  * resolution does not change (a re-dial clears the store), so ONE pair is
@@ -1652,6 +1797,9 @@ var DecodeWorkerChild = class {
1652
1797
  * un-guarded version a log flood rather than a diagnosis.
1653
1798
  */
1654
1799
  warnedTileEncoderUnavailable = false;
1800
+ /** One-shot: the scene rung is off by configuration, which cannot change under
1801
+ * a running worker. The VOLUME lives on `sceneRefused`. */
1802
+ warnedSceneDisabled = false;
1655
1803
  /**
1656
1804
  * The last subject-tile-cut failure REASON logged — see
1657
1805
  * {@link reportTileCutFailure}. Compared, not counted: the count lives on the
@@ -1780,7 +1928,7 @@ var DecodeWorkerChild = class {
1780
1928
  const skipped = this.framesSkipped + this.frames.droppedCount;
1781
1929
  const rssMb = Math.round(process.memoryUsage().rss / 1048576);
1782
1930
  const ageS = Math.round((now - this.startedAt) / 1e3);
1783
- this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
1931
+ this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
1784
1932
  this.lastMetricsAt = now;
1785
1933
  this.lastDeliveredSnapshot = this.framesDelivered;
1786
1934
  if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
@@ -1922,7 +2070,7 @@ var DecodeWorkerChild = class {
1922
2070
  this.send({
1923
2071
  kind: "nativeCropMiss",
1924
2072
  requestId,
1925
- reason: this.tileStore.enabled ? `frame ${frameId} released and no retained subject tile contains the requested rectangle` : `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`
2073
+ reason: this.tileMissReason(frameId)
1926
2074
  });
1927
2075
  } catch (err) {
1928
2076
  this.tileMisses += 1;
@@ -1936,6 +2084,18 @@ var DecodeWorkerChild = class {
1936
2084
  });
1937
2085
  }
1938
2086
  }
2087
+ /**
2088
+ * Why nothing retained could answer — named per RUNG, because they have
2089
+ * different fixes. "Subject tiles are off" and "the scene rung is off" and
2090
+ * "the rectangle is outside every container" are three different sentences,
2091
+ * and one "not retained" hid all of them.
2092
+ */
2093
+ tileMissReason(frameId) {
2094
+ if (!this.tileStore.enabled && !this.sceneStore.enabled) return `frame ${frameId} released and retention is DISABLED (tileBudgetMb 0, sceneBudgetMb 0)`;
2095
+ if (!this.sceneStore.enabled) return `frame ${frameId} released and no retained subject tile contains the requested rectangle; scene tiles are DISABLED (sceneBudgetMb 0), so a full-frame request cannot be served at all`;
2096
+ if (!this.tileStore.enabled) return `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`;
2097
+ return `frame ${frameId} released and no retained subject or scene tile contains the requested rectangle`;
2098
+ }
1939
2099
  /** The GPU-ring or reserved-slot frame for `frameId` (hold-disabled / newest paths). */
1940
2100
  resolveRetainedFrame(frameId) {
1941
2101
  return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
@@ -1962,8 +2122,8 @@ var DecodeWorkerChild = class {
1962
2122
  */
1963
2123
  async handleCutTiles(frameId, bboxes) {
1964
2124
  try {
1965
- if (!this.tileStore.enabled || bboxes.length === 0) return;
1966
- const wanted = bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME);
2125
+ if (!this.tileStore.enabled && !this.sceneStore.enabled || bboxes.length === 0) return;
2126
+ const wanted = this.tileStore.enabled ? bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME) : [];
1967
2127
  if (bboxes.length > wanted.length) this.emitStderr(`decode-worker-child: tile cut capped at ${NATIVE_TILE_MAX_PER_FRAME} of ${bboxes.length} subjects on frame ${frameId} — the rest fall back to the ≤640 tier\n`);
1968
2128
  const sharp = await this.tileEncoder();
1969
2129
  const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
@@ -1982,7 +2142,18 @@ var DecodeWorkerChild = class {
1982
2142
  }
1983
2143
  this.tileFrameWidth = frame.width;
1984
2144
  this.tileFrameHeight = frame.height;
1985
- const tiles = await cutSubjectTiles(frame, wanted, {
2145
+ const plan = planFrameCuts(wanted, {
2146
+ sceneEnabled: this.sceneStore.enabled,
2147
+ hasDetections: bboxes.length > 0
2148
+ });
2149
+ if (!plan.scene) {
2150
+ this.sceneRefused += 1;
2151
+ if (!this.warnedSceneDisabled) {
2152
+ this.warnedSceneDisabled = true;
2153
+ this.emitStderr("decode-worker-child: scene tiles DISABLED (sceneBudgetMb 0) — every late FULL-FRAME request (native keyFrame, detail frameJpeg, gallery fallback) will miss once this frame is released; refusals are counted as sceneRefused\n");
2154
+ }
2155
+ }
2156
+ const cut = await cutSubjectTiles(frame, plan.bboxes, {
1986
2157
  isLive: (f) => this.isRetainedFrameLive(f),
1987
2158
  cutRaw: (f, bbox) => {
1988
2159
  const { region } = resolveNativeTileRegion(f.width, f.height, bbox, NATIVE_TILE_PADDING_RATIO);
@@ -2016,10 +2187,18 @@ var DecodeWorkerChild = class {
2016
2187
  this.reportTileCutFailure(frameId, reason);
2017
2188
  }
2018
2189
  });
2019
- this.tileCutFailures += wanted.length - tiles.length;
2020
- if (tiles.length > 0) {
2021
- this.tilesCut += tiles.length;
2022
- this.tileStore.put(frameId, tiles);
2190
+ this.tileCutFailures += plan.bboxes.length - cut.length;
2191
+ const { subjects, scene } = plan.scene ? partitionSceneTile(cut, frame.width, frame.height) : {
2192
+ subjects: cut,
2193
+ scene: null
2194
+ };
2195
+ if (subjects.length > 0) {
2196
+ this.tilesCut += subjects.length;
2197
+ this.tileStore.put(frameId, subjects);
2198
+ }
2199
+ if (scene !== null) {
2200
+ this.scenesCut += 1;
2201
+ this.sceneStore.put(frameId, [scene]);
2023
2202
  }
2024
2203
  } finally {
2025
2204
  this.leaseStore.release(frameId);
@@ -2102,14 +2281,16 @@ var DecodeWorkerChild = class {
2102
2281
  * which would enrich a different part of the scene and error nowhere.
2103
2282
  */
2104
2283
  async serveFromTile(frameId, bbox, maxWidth) {
2284
+ const scenes = this.sceneStore.get(frameId);
2105
2285
  const tiles = this.tileStore.get(frameId);
2106
- if (tiles.length === 0) return null;
2286
+ if (tiles.length === 0 && scenes.length === 0) return null;
2107
2287
  const width = this.tileFrameWidth;
2108
2288
  const height = this.tileFrameHeight;
2109
2289
  if (width <= 0 || height <= 0) return null;
2110
2290
  const { region, target } = resolveNativeCropGeometry(width, height, bbox, maxWidth);
2111
- const served = selectTileForRect(tiles, region);
2291
+ const served = selectRetainedTile(tiles, scenes, region);
2112
2292
  if (!served) return null;
2293
+ if (served.source === "scene") this.sceneHits += 1;
2113
2294
  let pipeline = (await getSharp())(served.tile.bytes).extract({
2114
2295
  left: served.local.left,
2115
2296
  top: served.local.top,
@@ -2766,6 +2947,7 @@ var DecodeWorkerChild = class {
2766
2947
  this.nativeRing.clear();
2767
2948
  this.leaseStore.clear();
2768
2949
  this.tileStore.clear();
2950
+ this.sceneStore.clear();
2769
2951
  this.tileFrameWidth = 0;
2770
2952
  this.tileFrameHeight = 0;
2771
2953
  this.nativeLeaseDownloadFilter?.close();
@@ -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-BUw_aTfK.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-77rGxv1J.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-DXKTNEae.mjs";
4
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CVBnNONy.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) => {