@camstack/addon-pipeline 1.1.54 → 1.1.55

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 (33) 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 +149 -1468
  4. package/dist/detection-pipeline/index.mjs +129 -1448
  5. package/dist/{dist-BbaoC680.js → dist-DI57FC8K.js} +59 -3
  6. package/dist/{dist-CEcTeu1h.mjs → dist-RWGGPwVx.mjs} +59 -3
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +665 -23
  10. package/dist/pipeline-runner/index.mjs +665 -24
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{remote-source-plane-vN45KFSm.js → remote-source-plane-CHgvzzA6.js} +1 -1
  14. package/dist/{remote-source-plane-CNCqC_XG.mjs → remote-source-plane-DU0aRSPv.mjs} +1 -1
  15. package/dist/session-decode/decode-worker-child.js +222 -3
  16. package/dist/session-decode/decode-worker-child.mjs +222 -3
  17. package/dist/step-definitions-CNBFKjZe.js +1514 -0
  18. package/dist/step-definitions-CP9kVSml.mjs +1479 -0
  19. package/dist/stream-broker/_stub.js +1 -1
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-HS0tc6Sa.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-mDCPzmT3.mjs} +3 -3
  21. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_v5r5ya.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dn_pxzP-.mjs} +1 -1
  22. package/dist/stream-broker/{hostInit-DgohfEDA.mjs → hostInit-GM_CI22k.mjs} +3 -3
  23. package/dist/stream-broker/index.js +2 -2
  24. package/dist/stream-broker/index.mjs +2 -2
  25. package/dist/stream-broker/remoteEntry.js +1 -1
  26. package/dist/{worker-protocol-DGIt_waM.mjs → worker-protocol-CyVJTZEO.mjs} +1 -0
  27. package/dist/{worker-protocol-DkL6GDxe.js → worker-protocol-PP4jKHHJ.js} +1 -0
  28. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CA2Hd6xx.js → MaskShapeCanvas-DI4BY7W2-BChW0ntM.js} +1 -1
  29. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DUN1VeC7.js → MotionZonesSettings-NcxxQN8r-DDzqEbSe.js} +1 -1
  30. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BqZIAU5s.js → PrivacyMaskSettings-APgPLF7p-B879GXaf.js} +1 -1
  31. package/embed-dist/assets/{index-CMkYypxB.js → index-CNjQ5rAE.js} +10 -10
  32. package/embed-dist/index.html +1 -1
  33. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("../chunk-D6vf50IK.js");
2
- const require_dist = require("../dist-BbaoC680.js");
2
+ const require_dist = require("../dist-DI57FC8K.js");
3
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
4
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
5
5
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-CEcTeu1h.mjs";
1
+ import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-RWGGPwVx.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import { spawn } from "node:child_process";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BbaoC680.js");
1
+ const require_dist = require("./dist-DI57FC8K.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-CEcTeu1h.mjs";
1
+ import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-RWGGPwVx.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../chunk-D6vf50IK.js");
3
- const require_worker_protocol = require("../worker-protocol-DkL6GDxe.js");
3
+ const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
4
4
  let node_url = require("node:url");
5
5
  let node_fs = require("node:fs");
6
6
  node_fs = require_chunk.__toESM(node_fs);
@@ -237,6 +237,99 @@ var NativeFrameRing = class {
237
237
  }
238
238
  };
239
239
  //#endregion
240
+ //#region src/session-decode/native-lease-store.ts
241
+ /**
242
+ * A hard-capped-by-bytes, TTL-and-release map from worker frameId → retained
243
+ * RAM frame. Insertion-ordered (`Map`), so budget eviction is FIFO by age.
244
+ */
245
+ var NativeLeaseStore = class {
246
+ entries = /* @__PURE__ */ new Map();
247
+ budgetBytes;
248
+ ttlMs;
249
+ now;
250
+ bytes = 0;
251
+ constructor(options) {
252
+ this.budgetBytes = options.budgetBytes;
253
+ this.ttlMs = options.ttlMs;
254
+ this.now = options.now ?? Date.now;
255
+ }
256
+ /** `false` when the store is disabled (`budgetBytes <= 0`). */
257
+ get enabled() {
258
+ return this.budgetBytes > 0;
259
+ }
260
+ /** Number of retained leases (tests / metrics). */
261
+ get size() {
262
+ return this.entries.size;
263
+ }
264
+ /** Total resident bytes across all retained leases (tests / metrics). */
265
+ get totalBytes() {
266
+ return this.bytes;
267
+ }
268
+ /**
269
+ * Take ownership of `frame` under `frameId`. Returns `true` when the store
270
+ * took ownership (caller MUST NOT free), `false` when the store is disabled
271
+ * (`budgetBytes <= 0`) and the frame was freed here — the caller then keeps
272
+ * today's GPU-surface-ring path. Sweeps expired leases first, then evicts the
273
+ * oldest while the total exceeds the budget.
274
+ */
275
+ put(frameId, frame) {
276
+ if (!this.enabled) {
277
+ frame.free();
278
+ return false;
279
+ }
280
+ this.drop(frameId);
281
+ this.sweepExpired();
282
+ this.entries.set(frameId, {
283
+ frame,
284
+ insertedAt: this.now()
285
+ });
286
+ this.bytes += frame.byteLength;
287
+ while (this.bytes > this.budgetBytes && this.entries.size > 0) {
288
+ const oldest = this.entries.keys().next().value;
289
+ if (oldest === void 0) break;
290
+ this.drop(oldest);
291
+ }
292
+ return true;
293
+ }
294
+ /**
295
+ * The retained frame for `frameId`, or `null` if never leased, already
296
+ * released/evicted, OR aged past its TTL (an expired lease is freed + dropped
297
+ * here so a caller that reads it just after expiry does not resurrect it).
298
+ */
299
+ get(frameId) {
300
+ const entry = this.entries.get(frameId);
301
+ if (!entry) return null;
302
+ if (this.now() - entry.insertedAt > this.ttlMs) {
303
+ this.drop(frameId);
304
+ return null;
305
+ }
306
+ return entry.frame;
307
+ }
308
+ /** Free + drop the lease for `frameId` (the prompt release path). Idempotent. */
309
+ release(frameId) {
310
+ this.drop(frameId);
311
+ }
312
+ /** Free + drop every lease older than the TTL. */
313
+ sweepExpired() {
314
+ const cutoff = this.now() - this.ttlMs;
315
+ for (const [frameId, entry] of this.entries) if (entry.insertedAt < cutoff) this.drop(frameId);
316
+ }
317
+ /** Free + drop every retained lease. Idempotent (re-dial + teardown call it). */
318
+ clear() {
319
+ for (const entry of this.entries.values()) entry.frame.free();
320
+ this.entries.clear();
321
+ this.bytes = 0;
322
+ }
323
+ /** Free + remove one entry, keeping the byte tally exact. */
324
+ drop(frameId) {
325
+ const entry = this.entries.get(frameId);
326
+ if (!entry) return;
327
+ this.entries.delete(frameId);
328
+ this.bytes -= entry.frame.byteLength;
329
+ entry.frame.free();
330
+ }
331
+ };
332
+ //#endregion
240
333
  //#region src/session-decode/decode-worker-child.ts
241
334
  /**
242
335
  * Decode-worker CHILD entry point — Epic C P1, Task 2.
@@ -262,6 +355,14 @@ var NativeFrameRing = class {
262
355
  * NOT `$process`)". Raw `process.on('message')` / `process.send` are the
263
356
  * correct, sanctioned mechanism for this specific file only.
264
357
  */
358
+ /** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
359
+ function toLeasedNavFrame(frame) {
360
+ return {
361
+ frame,
362
+ byteLength: Math.ceil(frame.width * frame.height * 1.5),
363
+ free: () => frame.free()
364
+ };
365
+ }
265
366
  /** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
266
367
  var REDIAL_MS = 3e3;
267
368
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
@@ -281,6 +382,32 @@ var NATIVE_RING_CAP = (() => {
281
382
  return Math.min(4, Math.max(0, Math.floor(raw)));
282
383
  })();
283
384
  /**
385
+ * Hard RAM budget (bytes) for the native-frame LEASE store — the primary
386
+ * native-crop survival window. Unlike {@link NATIVE_RING_CAP} (GPU surfaces,
387
+ * leak-prone → tiny), a lease is a downloaded RAM copy, so the window is sized
388
+ * by memory, not a 2-frame count, and the late cross-process crop reliably
389
+ * hits. The primary eviction is the {@link NATIVE_LEASE_TTL_MS} TTL — a 64MB
390
+ * budget held only ~5 native 4K frames (~0.2s), which the late crop still
391
+ * outran; the budget is now a HIGH safety ceiling (default 1024MB) so the TTL
392
+ * is the effective cap and the ~40-200ms crop reliably lands within it.
393
+ * `CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB` (default 1024); `0` DISABLES the
394
+ * lease and falls back to the {@link NATIVE_RING_CAP} GPU ring (today's path).
395
+ */
396
+ var NATIVE_LEASE_BUDGET_BYTES = (() => {
397
+ const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB"]);
398
+ return (Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 1024) * 1024 * 1024;
399
+ })();
400
+ /**
401
+ * TTL (ms) after which a native-frame lease is treated as a miss and reclaimed.
402
+ * The backstop that bounds in-flight RAM even if the explicit `releaseNativeLease`
403
+ * is dropped — comfortably longer than the detection inference + crop round-trip
404
+ * (~40-200ms). `CAMSTACK_SESSION_NATIVE_LEASE_TTL_MS` (default 500).
405
+ */
406
+ var NATIVE_LEASE_TTL_MS = (() => {
407
+ const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_LEASE_TTL_MS"]);
408
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 500;
409
+ })();
410
+ /**
284
411
  * The libav GPU scale filter for a hwaccel backend, or `null` when none is
285
412
  * known here — those backends fall back to the software crop+scale path.
286
413
  * Mirrors `addon-decoder-ffmpeg/src/ffmpeg-args.ts` `gpuScaleFilterForBackend`.
@@ -520,6 +647,29 @@ var DecodeWorkerChild = class {
520
647
  * re-dial + teardown (see {@link closeInput}).
521
648
  */
522
649
  nativeRing = new NativeFrameRing(NATIVE_RING_CAP);
650
+ /**
651
+ * PRIMARY native-frame survival window — a bounded RAM copy of each shipped
652
+ * detection frame, keyed by frameId, held until release / TTL / budget
653
+ * eviction. Replaces the tiny GPU {@link nativeRing} as the default (the ring
654
+ * is used only when the lease is disabled via a 0 budget). Because it holds
655
+ * DOWNLOADED (system-memory) frames, retaining a large window has no VAAPI
656
+ * surface-pool starvation risk. Cleared on every re-dial + teardown.
657
+ */
658
+ leaseStore = new NativeLeaseStore({
659
+ budgetBytes: NATIVE_LEASE_BUDGET_BYTES,
660
+ ttlMs: NATIVE_LEASE_TTL_MS
661
+ });
662
+ /**
663
+ * DEDICATED full-frame GPU→system download filtergraph
664
+ * (`scale_<be>=iw:ih,hwdownload,format=nv12,format=yuv420p`) used ONLY to
665
+ * materialize a HW surface into a leasable software frame at NATIVE
666
+ * resolution (no scale). Kept separate from the detection/native-crop filters
667
+ * (different geometry). Same dispose discipline: closed on every re-dial
668
+ * ({@link closeInput}) + teardown, since it references the decoder's per-dial
669
+ * `hw_frames_ctx`.
670
+ */
671
+ nativeLeaseDownloadFilter = null;
672
+ nativeLeaseDownloadFilterKey = "";
523
673
  /** Throttled native-crop hit/miss counters, emitted with the throughput line. */
524
674
  nativeCropHits = 0;
525
675
  nativeCropMisses = 0;
@@ -555,7 +705,7 @@ var DecodeWorkerChild = class {
555
705
  * the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
556
706
  * node-av. See that file's doc comment for the full semantics.
557
707
  */
558
- frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.nativeRing.retain(frameId, frame) });
708
+ frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.captureNativeFrame(frameId, frame) });
559
709
  /**
560
710
  * Mirrors whether `this.frames` currently has a pull waiting, so
561
711
  * `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
@@ -584,6 +734,9 @@ var DecodeWorkerChild = class {
584
734
  case "nativeCrop":
585
735
  this.handleNativeCrop(message.requestId, message.frameId, message.bbox, message.maxWidth);
586
736
  return;
737
+ case "releaseNativeLease":
738
+ this.leaseStore.release(message.frameId);
739
+ return;
587
740
  case "stop":
588
741
  this.handleStop();
589
742
  return;
@@ -685,7 +838,7 @@ var DecodeWorkerChild = class {
685
838
  * session to software on a GPU-filter error).
686
839
  */
687
840
  handleNativeCrop(requestId, frameId, bbox, maxWidth) {
688
- const frame = this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
841
+ const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
689
842
  if (!frame) {
690
843
  this.nativeCropMisses++;
691
844
  this.send({
@@ -714,6 +867,68 @@ var DecodeWorkerChild = class {
714
867
  });
715
868
  }
716
869
  }
870
+ /** The GPU-ring or reserved-slot frame for `frameId` (lease-disabled / newest paths). */
871
+ resolveRetainedFrame(frameId) {
872
+ return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
873
+ }
874
+ /**
875
+ * Retain a just-superseded DELIVERED frame for later native crops. Transfers
876
+ * ownership from the {@link FrameSlot} (the caller MUST NOT free when this
877
+ * returns `true`).
878
+ *
879
+ * - Lease ENABLED (default): materialize a SOFTWARE RAM copy and put it in the
880
+ * {@link leaseStore}. A HW surface is downloaded to system memory ONCE here
881
+ * (freeing the GPU surface immediately — no VAAPI-pool pin); a software frame
882
+ * is leased directly. A download failure still consumes ownership (freed
883
+ * here) so the slot never double-frees — the crop simply misses → fallback.
884
+ * - Lease DISABLED (`budget 0`): fall back to today's tiny GPU {@link nativeRing}.
885
+ */
886
+ captureNativeFrame(frameId, frame) {
887
+ if (!this.leaseStore.enabled) return this.nativeRing.retain(frameId, frame);
888
+ if (frame.isHwFrame()) {
889
+ const software = this.downloadToSoftware(frame);
890
+ frame.free();
891
+ if (!software) return true;
892
+ this.leaseStore.put(frameId, toLeasedNavFrame(software));
893
+ return true;
894
+ }
895
+ this.leaseStore.put(frameId, toLeasedNavFrame(frame));
896
+ return true;
897
+ }
898
+ /**
899
+ * Download a HW surface to a NATIVE-resolution software YUV420P frame via the
900
+ * dedicated full-frame download filtergraph. Returns the software frame (owned
901
+ * by the caller) or `null` on any failure (the lease is then simply skipped).
902
+ * `processAllSync` only refs the input surface, so the caller frees it after.
903
+ */
904
+ downloadToSoftware(frame) {
905
+ const nav = this.nav;
906
+ const scaleFilter = this.gpuScaleFilter;
907
+ if (!nav || !scaleFilter || !this.hwContext) return null;
908
+ try {
909
+ const outputs = this.ensureLeaseDownloadFilter(nav, scaleFilter, frame.width, frame.height).processAllSync(frame);
910
+ const first = outputs[0] ?? null;
911
+ for (let i = 1; i < outputs.length; i++) outputs[i]?.free();
912
+ return first;
913
+ } catch (err) {
914
+ this.emitStderr(`decode-worker-child: native lease download failed — ${errMessage(err)}\n`);
915
+ return null;
916
+ }
917
+ }
918
+ /** Build-once / reuse the full-frame GPU→system YUV420P download filtergraph. */
919
+ ensureLeaseDownloadFilter(nav, scaleFilter, srcW, srcH) {
920
+ const key = `${srcW}x${srcH}`;
921
+ const cached = this.nativeLeaseDownloadFilter;
922
+ if (cached && key === this.nativeLeaseDownloadFilterKey) return cached;
923
+ this.nativeLeaseDownloadFilter?.close();
924
+ this.nativeLeaseDownloadFilter = null;
925
+ this.nativeLeaseDownloadFilterKey = "";
926
+ const description = `${scaleFilter}=w=iw:h=ih,hwdownload,format=nv12,format=yuv420p`;
927
+ const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
928
+ this.nativeLeaseDownloadFilter = filter;
929
+ this.nativeLeaseDownloadFilterKey = key;
930
+ return filter;
931
+ }
717
932
  /** Map a normalized bbox to an even-aligned pixel crop + (optionally capped) target. */
718
933
  resolveNativeCrop(frame, bbox, maxWidth) {
719
934
  return resolveNativeCropGeometry(frame.width, frame.height, bbox, maxWidth);
@@ -1168,6 +1383,10 @@ var DecodeWorkerChild = class {
1168
1383
  */
1169
1384
  closeInput() {
1170
1385
  this.nativeRing.clear();
1386
+ this.leaseStore.clear();
1387
+ this.nativeLeaseDownloadFilter?.close();
1388
+ this.nativeLeaseDownloadFilter = null;
1389
+ this.nativeLeaseDownloadFilterKey = "";
1171
1390
  this.nativeCropHwFilter?.close();
1172
1391
  this.nativeCropHwFilter = null;
1173
1392
  this.nativeCropHwFilterKey = "";
@@ -1,4 +1,4 @@
1
- import { n as isWorkerRequest } from "../worker-protocol-DGIt_waM.mjs";
1
+ import { n as isWorkerRequest } from "../worker-protocol-CyVJTZEO.mjs";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -233,6 +233,99 @@ var NativeFrameRing = class {
233
233
  }
234
234
  };
235
235
  //#endregion
236
+ //#region src/session-decode/native-lease-store.ts
237
+ /**
238
+ * A hard-capped-by-bytes, TTL-and-release map from worker frameId → retained
239
+ * RAM frame. Insertion-ordered (`Map`), so budget eviction is FIFO by age.
240
+ */
241
+ var NativeLeaseStore = class {
242
+ entries = /* @__PURE__ */ new Map();
243
+ budgetBytes;
244
+ ttlMs;
245
+ now;
246
+ bytes = 0;
247
+ constructor(options) {
248
+ this.budgetBytes = options.budgetBytes;
249
+ this.ttlMs = options.ttlMs;
250
+ this.now = options.now ?? Date.now;
251
+ }
252
+ /** `false` when the store is disabled (`budgetBytes <= 0`). */
253
+ get enabled() {
254
+ return this.budgetBytes > 0;
255
+ }
256
+ /** Number of retained leases (tests / metrics). */
257
+ get size() {
258
+ return this.entries.size;
259
+ }
260
+ /** Total resident bytes across all retained leases (tests / metrics). */
261
+ get totalBytes() {
262
+ return this.bytes;
263
+ }
264
+ /**
265
+ * Take ownership of `frame` under `frameId`. Returns `true` when the store
266
+ * took ownership (caller MUST NOT free), `false` when the store is disabled
267
+ * (`budgetBytes <= 0`) and the frame was freed here — the caller then keeps
268
+ * today's GPU-surface-ring path. Sweeps expired leases first, then evicts the
269
+ * oldest while the total exceeds the budget.
270
+ */
271
+ put(frameId, frame) {
272
+ if (!this.enabled) {
273
+ frame.free();
274
+ return false;
275
+ }
276
+ this.drop(frameId);
277
+ this.sweepExpired();
278
+ this.entries.set(frameId, {
279
+ frame,
280
+ insertedAt: this.now()
281
+ });
282
+ this.bytes += frame.byteLength;
283
+ while (this.bytes > this.budgetBytes && this.entries.size > 0) {
284
+ const oldest = this.entries.keys().next().value;
285
+ if (oldest === void 0) break;
286
+ this.drop(oldest);
287
+ }
288
+ return true;
289
+ }
290
+ /**
291
+ * The retained frame for `frameId`, or `null` if never leased, already
292
+ * released/evicted, OR aged past its TTL (an expired lease is freed + dropped
293
+ * here so a caller that reads it just after expiry does not resurrect it).
294
+ */
295
+ get(frameId) {
296
+ const entry = this.entries.get(frameId);
297
+ if (!entry) return null;
298
+ if (this.now() - entry.insertedAt > this.ttlMs) {
299
+ this.drop(frameId);
300
+ return null;
301
+ }
302
+ return entry.frame;
303
+ }
304
+ /** Free + drop the lease for `frameId` (the prompt release path). Idempotent. */
305
+ release(frameId) {
306
+ this.drop(frameId);
307
+ }
308
+ /** Free + drop every lease older than the TTL. */
309
+ sweepExpired() {
310
+ const cutoff = this.now() - this.ttlMs;
311
+ for (const [frameId, entry] of this.entries) if (entry.insertedAt < cutoff) this.drop(frameId);
312
+ }
313
+ /** Free + drop every retained lease. Idempotent (re-dial + teardown call it). */
314
+ clear() {
315
+ for (const entry of this.entries.values()) entry.frame.free();
316
+ this.entries.clear();
317
+ this.bytes = 0;
318
+ }
319
+ /** Free + remove one entry, keeping the byte tally exact. */
320
+ drop(frameId) {
321
+ const entry = this.entries.get(frameId);
322
+ if (!entry) return;
323
+ this.entries.delete(frameId);
324
+ this.bytes -= entry.frame.byteLength;
325
+ entry.frame.free();
326
+ }
327
+ };
328
+ //#endregion
236
329
  //#region src/session-decode/decode-worker-child.ts
237
330
  /**
238
331
  * Decode-worker CHILD entry point — Epic C P1, Task 2.
@@ -258,6 +351,14 @@ var NativeFrameRing = class {
258
351
  * NOT `$process`)". Raw `process.on('message')` / `process.send` are the
259
352
  * correct, sanctioned mechanism for this specific file only.
260
353
  */
354
+ /** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
355
+ function toLeasedNavFrame(frame) {
356
+ return {
357
+ frame,
358
+ byteLength: Math.ceil(frame.width * frame.height * 1.5),
359
+ free: () => frame.free()
360
+ };
361
+ }
261
362
  /** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
262
363
  var REDIAL_MS = 3e3;
263
364
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
@@ -277,6 +378,32 @@ var NATIVE_RING_CAP = (() => {
277
378
  return Math.min(4, Math.max(0, Math.floor(raw)));
278
379
  })();
279
380
  /**
381
+ * Hard RAM budget (bytes) for the native-frame LEASE store — the primary
382
+ * native-crop survival window. Unlike {@link NATIVE_RING_CAP} (GPU surfaces,
383
+ * leak-prone → tiny), a lease is a downloaded RAM copy, so the window is sized
384
+ * by memory, not a 2-frame count, and the late cross-process crop reliably
385
+ * hits. The primary eviction is the {@link NATIVE_LEASE_TTL_MS} TTL — a 64MB
386
+ * budget held only ~5 native 4K frames (~0.2s), which the late crop still
387
+ * outran; the budget is now a HIGH safety ceiling (default 1024MB) so the TTL
388
+ * is the effective cap and the ~40-200ms crop reliably lands within it.
389
+ * `CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB` (default 1024); `0` DISABLES the
390
+ * lease and falls back to the {@link NATIVE_RING_CAP} GPU ring (today's path).
391
+ */
392
+ var NATIVE_LEASE_BUDGET_BYTES = (() => {
393
+ const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB"]);
394
+ return (Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 1024) * 1024 * 1024;
395
+ })();
396
+ /**
397
+ * TTL (ms) after which a native-frame lease is treated as a miss and reclaimed.
398
+ * The backstop that bounds in-flight RAM even if the explicit `releaseNativeLease`
399
+ * is dropped — comfortably longer than the detection inference + crop round-trip
400
+ * (~40-200ms). `CAMSTACK_SESSION_NATIVE_LEASE_TTL_MS` (default 500).
401
+ */
402
+ var NATIVE_LEASE_TTL_MS = (() => {
403
+ const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_LEASE_TTL_MS"]);
404
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 500;
405
+ })();
406
+ /**
280
407
  * The libav GPU scale filter for a hwaccel backend, or `null` when none is
281
408
  * known here — those backends fall back to the software crop+scale path.
282
409
  * Mirrors `addon-decoder-ffmpeg/src/ffmpeg-args.ts` `gpuScaleFilterForBackend`.
@@ -516,6 +643,29 @@ var DecodeWorkerChild = class {
516
643
  * re-dial + teardown (see {@link closeInput}).
517
644
  */
518
645
  nativeRing = new NativeFrameRing(NATIVE_RING_CAP);
646
+ /**
647
+ * PRIMARY native-frame survival window — a bounded RAM copy of each shipped
648
+ * detection frame, keyed by frameId, held until release / TTL / budget
649
+ * eviction. Replaces the tiny GPU {@link nativeRing} as the default (the ring
650
+ * is used only when the lease is disabled via a 0 budget). Because it holds
651
+ * DOWNLOADED (system-memory) frames, retaining a large window has no VAAPI
652
+ * surface-pool starvation risk. Cleared on every re-dial + teardown.
653
+ */
654
+ leaseStore = new NativeLeaseStore({
655
+ budgetBytes: NATIVE_LEASE_BUDGET_BYTES,
656
+ ttlMs: NATIVE_LEASE_TTL_MS
657
+ });
658
+ /**
659
+ * DEDICATED full-frame GPU→system download filtergraph
660
+ * (`scale_<be>=iw:ih,hwdownload,format=nv12,format=yuv420p`) used ONLY to
661
+ * materialize a HW surface into a leasable software frame at NATIVE
662
+ * resolution (no scale). Kept separate from the detection/native-crop filters
663
+ * (different geometry). Same dispose discipline: closed on every re-dial
664
+ * ({@link closeInput}) + teardown, since it references the decoder's per-dial
665
+ * `hw_frames_ctx`.
666
+ */
667
+ nativeLeaseDownloadFilter = null;
668
+ nativeLeaseDownloadFilterKey = "";
519
669
  /** Throttled native-crop hit/miss counters, emitted with the throughput line. */
520
670
  nativeCropHits = 0;
521
671
  nativeCropMisses = 0;
@@ -551,7 +701,7 @@ var DecodeWorkerChild = class {
551
701
  * the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
552
702
  * node-av. See that file's doc comment for the full semantics.
553
703
  */
554
- frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.nativeRing.retain(frameId, frame) });
704
+ frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.captureNativeFrame(frameId, frame) });
555
705
  /**
556
706
  * Mirrors whether `this.frames` currently has a pull waiting, so
557
707
  * `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
@@ -580,6 +730,9 @@ var DecodeWorkerChild = class {
580
730
  case "nativeCrop":
581
731
  this.handleNativeCrop(message.requestId, message.frameId, message.bbox, message.maxWidth);
582
732
  return;
733
+ case "releaseNativeLease":
734
+ this.leaseStore.release(message.frameId);
735
+ return;
583
736
  case "stop":
584
737
  this.handleStop();
585
738
  return;
@@ -681,7 +834,7 @@ var DecodeWorkerChild = class {
681
834
  * session to software on a GPU-filter error).
682
835
  */
683
836
  handleNativeCrop(requestId, frameId, bbox, maxWidth) {
684
- const frame = this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
837
+ const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
685
838
  if (!frame) {
686
839
  this.nativeCropMisses++;
687
840
  this.send({
@@ -710,6 +863,68 @@ var DecodeWorkerChild = class {
710
863
  });
711
864
  }
712
865
  }
866
+ /** The GPU-ring or reserved-slot frame for `frameId` (lease-disabled / newest paths). */
867
+ resolveRetainedFrame(frameId) {
868
+ return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
869
+ }
870
+ /**
871
+ * Retain a just-superseded DELIVERED frame for later native crops. Transfers
872
+ * ownership from the {@link FrameSlot} (the caller MUST NOT free when this
873
+ * returns `true`).
874
+ *
875
+ * - Lease ENABLED (default): materialize a SOFTWARE RAM copy and put it in the
876
+ * {@link leaseStore}. A HW surface is downloaded to system memory ONCE here
877
+ * (freeing the GPU surface immediately — no VAAPI-pool pin); a software frame
878
+ * is leased directly. A download failure still consumes ownership (freed
879
+ * here) so the slot never double-frees — the crop simply misses → fallback.
880
+ * - Lease DISABLED (`budget 0`): fall back to today's tiny GPU {@link nativeRing}.
881
+ */
882
+ captureNativeFrame(frameId, frame) {
883
+ if (!this.leaseStore.enabled) return this.nativeRing.retain(frameId, frame);
884
+ if (frame.isHwFrame()) {
885
+ const software = this.downloadToSoftware(frame);
886
+ frame.free();
887
+ if (!software) return true;
888
+ this.leaseStore.put(frameId, toLeasedNavFrame(software));
889
+ return true;
890
+ }
891
+ this.leaseStore.put(frameId, toLeasedNavFrame(frame));
892
+ return true;
893
+ }
894
+ /**
895
+ * Download a HW surface to a NATIVE-resolution software YUV420P frame via the
896
+ * dedicated full-frame download filtergraph. Returns the software frame (owned
897
+ * by the caller) or `null` on any failure (the lease is then simply skipped).
898
+ * `processAllSync` only refs the input surface, so the caller frees it after.
899
+ */
900
+ downloadToSoftware(frame) {
901
+ const nav = this.nav;
902
+ const scaleFilter = this.gpuScaleFilter;
903
+ if (!nav || !scaleFilter || !this.hwContext) return null;
904
+ try {
905
+ const outputs = this.ensureLeaseDownloadFilter(nav, scaleFilter, frame.width, frame.height).processAllSync(frame);
906
+ const first = outputs[0] ?? null;
907
+ for (let i = 1; i < outputs.length; i++) outputs[i]?.free();
908
+ return first;
909
+ } catch (err) {
910
+ this.emitStderr(`decode-worker-child: native lease download failed — ${errMessage(err)}\n`);
911
+ return null;
912
+ }
913
+ }
914
+ /** Build-once / reuse the full-frame GPU→system YUV420P download filtergraph. */
915
+ ensureLeaseDownloadFilter(nav, scaleFilter, srcW, srcH) {
916
+ const key = `${srcW}x${srcH}`;
917
+ const cached = this.nativeLeaseDownloadFilter;
918
+ if (cached && key === this.nativeLeaseDownloadFilterKey) return cached;
919
+ this.nativeLeaseDownloadFilter?.close();
920
+ this.nativeLeaseDownloadFilter = null;
921
+ this.nativeLeaseDownloadFilterKey = "";
922
+ const description = `${scaleFilter}=w=iw:h=ih,hwdownload,format=nv12,format=yuv420p`;
923
+ const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
924
+ this.nativeLeaseDownloadFilter = filter;
925
+ this.nativeLeaseDownloadFilterKey = key;
926
+ return filter;
927
+ }
713
928
  /** Map a normalized bbox to an even-aligned pixel crop + (optionally capped) target. */
714
929
  resolveNativeCrop(frame, bbox, maxWidth) {
715
930
  return resolveNativeCropGeometry(frame.width, frame.height, bbox, maxWidth);
@@ -1164,6 +1379,10 @@ var DecodeWorkerChild = class {
1164
1379
  */
1165
1380
  closeInput() {
1166
1381
  this.nativeRing.clear();
1382
+ this.leaseStore.clear();
1383
+ this.nativeLeaseDownloadFilter?.close();
1384
+ this.nativeLeaseDownloadFilter = null;
1385
+ this.nativeLeaseDownloadFilterKey = "";
1167
1386
  this.nativeCropHwFilter?.close();
1168
1387
  this.nativeCropHwFilter = null;
1169
1388
  this.nativeCropHwFilterKey = "";