@camstack/addon-pipeline 1.1.53 → 1.1.54

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 +146 -7
  4. package/dist/detection-pipeline/index.mjs +146 -7
  5. package/dist/{dist-DWb62H5U.js → dist-BbaoC680.js} +110 -7
  6. package/dist/{dist-BalxNwOt.mjs → dist-CEcTeu1h.mjs} +110 -7
  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 +114 -5
  10. package/dist/pipeline-runner/index.mjs +114 -5
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{remote-source-plane-BCJW5CvF.mjs → remote-source-plane-CNCqC_XG.mjs} +1 -1
  14. package/dist/{remote-source-plane-CZpzIVro.js → remote-source-plane-vN45KFSm.js} +1 -1
  15. package/dist/session-decode/decode-worker-child.js +319 -35
  16. package/dist/session-decode/decode-worker-child.mjs +319 -35
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DIN3CcRP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-HS0tc6Sa.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3gTdHEh.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-C9fwKMfg.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_v5r5ya.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-CGUEq--X.mjs → hostInit-DgohfEDA.mjs} +3 -3
  22. package/dist/stream-broker/index.js +2 -2
  23. package/dist/stream-broker/index.mjs +2 -2
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/{worker-protocol-pk7qdYXt.mjs → worker-protocol-DGIt_waM.mjs} +6 -0
  26. package/dist/{worker-protocol-BCfO8gUF.js → worker-protocol-DkL6GDxe.js} +6 -0
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CqOe8eYa.js → MaskShapeCanvas-DI4BY7W2-CA2Hd6xx.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CZyLeUnd.js → MotionZonesSettings-NcxxQN8r-DUN1VeC7.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Cn0hGZnu.js → PrivacyMaskSettings-APgPLF7p-BqZIAU5s.js} +1 -1
  30. package/embed-dist/assets/{index-DRl4XYjA.js → index-CMkYypxB.js} +10 -10
  31. package/embed-dist/index.html +1 -1
  32. package/package.json +1 -1
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-o4tu_xuc.mjs +0 -26
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
4627
4627
  return inst;
4628
4628
  }
4629
4629
  //#endregion
4630
- //#region ../types/dist/sleep-b4Jf2n33.mjs
4630
+ //#region ../types/dist/sleep-BC9Yqte7.mjs
4631
4631
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4632
4632
  EventCategory["SystemBoot"] = "system.boot";
4633
4633
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -11890,6 +11890,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(ZoneSchema).read
11890
11890
  auth: "admin"
11891
11891
  }), object({ zones: array(ZoneSchema).readonly() });
11892
11892
  /**
11893
+ * A bounding box in NORMALIZED [0,1] frame coordinates for `getNativeCrop`. The
11894
+ * decode worker resolves it against the RETAINED native frame's real pixel dims,
11895
+ * so the caller supplies only the detection-res bbox divided by the detection
11896
+ * dims — no native resolution to plumb.
11897
+ */
11898
+ var NativeCropBboxSchema = object({
11899
+ x: number(),
11900
+ y: number(),
11901
+ w: number(),
11902
+ h: number()
11903
+ });
11904
+ /** Result of a best-effort native-resolution crop (`getNativeCrop`). */
11905
+ var NativeCropResultSchema = object({
11906
+ /** Packed rgb (24-bit) pixels of the crop. */
11907
+ bytes: _instanceof(Uint8Array),
11908
+ width: number().int().positive(),
11909
+ height: number().int().positive()
11910
+ });
11911
+ /**
11893
11912
  * Per-camera tunable ranges + defaults. Single source of truth used
11894
11913
  * by both the Zod data schema (validation + default fallback) and
11895
11914
  * the device settings UI (slider min/max/step). Touch one place and
@@ -12193,7 +12212,22 @@ var pipelineRunnerCapability = {
12193
12212
  /** All per-camera metrics in one round-trip. */
12194
12213
  getAllCameraMetrics: method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()),
12195
12214
  /** List the deviceIds currently attached to this runner. */
12196
- getLocalCameras: method(_void(), array(number()).readonly())
12215
+ getLocalCameras: method(_void(), array(number()).readonly()),
12216
+ /**
12217
+ * Best-effort NATIVE-resolution crop of a retention-ring frame. Given the
12218
+ * `FrameHandle` that rode an inference-result event and a normalized `bbox`,
12219
+ * the runner asks the decode worker still holding that frame's NATIVE
12220
+ * surface to GPU/CPU-crop ONLY the ROI at native res and download just the
12221
+ * crop. Returns `null` on a miss (handle not registered, or the worker's
12222
+ * tiny native-retention ring already evicted the frame) — the caller falls
12223
+ * back to a detection-frame crop. Routed to the frame's owning node by
12224
+ * `handle.nodeId`; NEVER ships a full native frame.
12225
+ */
12226
+ getNativeCrop: method(object({
12227
+ handle: FrameHandleSchema,
12228
+ bbox: NativeCropBboxSchema,
12229
+ maxWidth: number().int().positive().optional()
12230
+ }), NativeCropResultSchema.nullable())
12197
12231
  }
12198
12232
  };
12199
12233
  object({
@@ -16443,7 +16477,17 @@ var TrackSchema = object({
16443
16477
  /** Cumulative normalized distance travelled (0..1 units = full frame width). */
16444
16478
  totalDistance: number(),
16445
16479
  state: TrackStateSchema,
16446
- active: boolean()
16480
+ active: boolean(),
16481
+ /** Deterministic key-event importance score in [0,1] (server-computed at
16482
+ * track expiry, recomputed on late label). Absent on legacy rows written
16483
+ * before scoring shipped — consumers degrade to absence / compute-on-read. */
16484
+ importance: number().optional(),
16485
+ /** Id of the track's highest-confidence ObjectEvent (its representative
16486
+ * "best" frame). Absent when the track produced no object events. */
16487
+ bestEventId: string().optional(),
16488
+ /** Tag of the importance sub-signal that dominated the score
16489
+ * (identity|dwell|proximity|class|confidence|travel|zone). */
16490
+ importanceReason: string().optional()
16447
16491
  });
16448
16492
  var BaseEventFields = {
16449
16493
  id: string(),
@@ -16508,8 +16552,18 @@ var ObjectEventSchema = object({
16508
16552
  frameHeight: number().optional(),
16509
16553
  /** MediaStore key for the crop attached to this event (if any). */
16510
16554
  mediaKey: string().optional(),
16555
+ /** Design B: MediaStore key of the track's native-resolution key frame (the
16556
+ * best-detection full frame). Resolve via the event-media data-plane
16557
+ * (`/addon/<addonId>/event-media/<keyFrameMediaKey>`) for a detail view that
16558
+ * draws `bbox` over the native frame. Absent on legacy rows / non-decoded
16559
+ * sources — consumers fall back to `mediaKey` (the tight crop). */
16560
+ keyFrameMediaKey: string().optional(),
16511
16561
  /** Populated by B5 (recording playback URL for this event). */
16512
- mediaUrl: string().optional()
16562
+ mediaUrl: string().optional(),
16563
+ /** The parent track's key-event importance [0,1], propagated to every object
16564
+ * event of the track (so an event row can be sorted by importance without a
16565
+ * track join). Absent on legacy rows / before the track was scored. */
16566
+ importance: number().optional()
16513
16567
  });
16514
16568
  var AudioEventSchema = object({
16515
16569
  ...BaseEventFields,
@@ -16533,7 +16587,8 @@ var MediaFileKindEnum = _enum([
16533
16587
  "fullFrame",
16534
16588
  "fullFrameBoxed",
16535
16589
  "faceCrop",
16536
- "plateCrop"
16590
+ "plateCrop",
16591
+ "keyFrame"
16537
16592
  ]);
16538
16593
  var MediaFileSchema = object({
16539
16594
  key: string(),
@@ -16554,6 +16609,32 @@ var DeviceEventQueryInput = object({
16554
16609
  projection: _enum(["full", "slim"]).optional()
16555
16610
  });
16556
16611
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
16612
+ var KeyEventQueryInput = object({
16613
+ deviceId: number(),
16614
+ /** Window lower bound (track firstSeen ≥ since). */
16615
+ since: number(),
16616
+ /** Window upper bound (track firstSeen ≤ until). */
16617
+ until: number(),
16618
+ limit: number().int().min(1).max(200).default(50),
16619
+ /** Drop tracks scoring below this importance. */
16620
+ minImportance: number().min(0).max(1).optional(),
16621
+ /** Restrict to a single class (e.g. 'person'). */
16622
+ classFilter: string().optional()
16623
+ });
16624
+ var KeyEventSchema = object({
16625
+ /** The representative event id (the track's best ObjectEvent, else its trackId). */
16626
+ id: string(),
16627
+ trackId: string(),
16628
+ /** Track start time (firstSeen). */
16629
+ timestamp: number(),
16630
+ className: string(),
16631
+ label: string().optional(),
16632
+ importance: number(),
16633
+ /** Highest-confidence ObjectEvent id for the track (empty when none). */
16634
+ bestEventId: string(),
16635
+ /** Track lifetime in ms (lastSeen - firstSeen). */
16636
+ windowMs: number().optional()
16637
+ });
16557
16638
  var TrackedDetectionSchema = object({
16558
16639
  trackId: string(),
16559
16640
  className: string(),
@@ -16583,7 +16664,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16583
16664
  }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
16584
16665
  kind: "mutation",
16585
16666
  auth: "admin"
16586
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({
16667
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
16587
16668
  deviceId: number(),
16588
16669
  since: number(),
16589
16670
  until: number(),
@@ -18595,7 +18676,17 @@ var FaceInfoSchema = object({
18595
18676
  recognizedIdentityId: string().optional(),
18596
18677
  identityName: string().optional(),
18597
18678
  assigned: boolean(),
18598
- base64: string().optional()
18679
+ base64: string().optional(),
18680
+ /** Design B: the face bbox (pixel space) on the key frame — lets a detail
18681
+ * view draw the box over the native `keyFrameMediaKey` frame. Absent on
18682
+ * legacy rows written before design B. */
18683
+ faceBbox: BoundingBoxSchema.optional(),
18684
+ /** Design B: MediaStore key of the track's native-resolution key frame.
18685
+ * Fetch the native JPEG via the event-media data-plane
18686
+ * (`/addon/<addonId>/event-media/<keyFrameMediaKey>`). Absent when the
18687
+ * track produced no key frame (e.g. native/onboard source) — the UI falls
18688
+ * back to the inline `base64` face crop. */
18689
+ keyFrameMediaKey: string().optional()
18599
18690
  });
18600
18691
  var FaceFilterEnum = _enum([
18601
18692
  "unassigned",
@@ -22747,6 +22838,12 @@ Object.freeze({
22747
22838
  addonId: null,
22748
22839
  access: "view"
22749
22840
  },
22841
+ "pipelineAnalytics.getKeyEvents": {
22842
+ capName: "pipeline-analytics",
22843
+ capScope: "device",
22844
+ addonId: null,
22845
+ access: "view"
22846
+ },
22750
22847
  "pipelineAnalytics.getMotionEvents": {
22751
22848
  capName: "pipeline-analytics",
22752
22849
  capScope: "device",
@@ -23275,6 +23372,12 @@ Object.freeze({
23275
23372
  addonId: null,
23276
23373
  access: "view"
23277
23374
  },
23375
+ "pipelineRunner.getNativeCrop": {
23376
+ capName: "pipeline-runner",
23377
+ capScope: "system",
23378
+ addonId: null,
23379
+ access: "view"
23380
+ },
23278
23381
  "pipelineRunner.reportMotion": {
23279
23382
  capName: "pipeline-runner",
23280
23383
  capScope: "system",
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DWb62H5U.js");
5
+ const require_dist = require("../dist-BbaoC680.js");
6
6
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { B as DeviceType, E as motionDetectionCapability, L as BaseAddon, U as hydrateSchema, x as evaluateZoneRules } from "../dist-BalxNwOt.mjs";
1
+ import { B as DeviceType, E as motionDetectionCapability, L as BaseAddon, U as hydrateSchema, x as evaluateZoneRules } from "../dist-CEcTeu1h.mjs";
2
2
  import { FrameRingReaderCache } from "@camstack/shm-ring";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -2,10 +2,10 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DWb62H5U.js");
6
- const require_remote_source_plane = require("../remote-source-plane-CZpzIVro.js");
5
+ const require_dist = require("../dist-BbaoC680.js");
6
+ const require_remote_source_plane = require("../remote-source-plane-vN45KFSm.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
- const require_worker_protocol = require("../worker-protocol-BCfO8gUF.js");
8
+ const require_worker_protocol = require("../worker-protocol-DkL6GDxe.js");
9
9
  let _camstack_shm_ring = require("@camstack/shm-ring");
10
10
  let node_child_process = require("node:child_process");
11
11
  let node_url = require("node:url");
@@ -1455,6 +1455,9 @@ var SessionDecodeSession = class {
1455
1455
  frameTimeoutMs;
1456
1456
  pendingPull = null;
1457
1457
  pendingToBuffers = /* @__PURE__ */ new Map();
1458
+ /** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
1459
+ pendingNativeCrops = /* @__PURE__ */ new Map();
1460
+ nextNativeCropId = 1;
1458
1461
  exited = false;
1459
1462
  sigkillTimer = null;
1460
1463
  pullTimeoutTimer = null;
@@ -1519,6 +1522,30 @@ var SessionDecodeSession = class {
1519
1522
  }
1520
1523
  });
1521
1524
  }
1525
+ /**
1526
+ * Send `nativeCrop` for `frameId` and resolve with the crop result, or `null`
1527
+ * on any miss (frame aged out, worker error, or worker already exited). Never
1528
+ * rejects — native crops are strictly best-effort with a free fallback.
1529
+ */
1530
+ requestNativeCrop(frameId, bbox, maxWidth) {
1531
+ if (this.exited) return Promise.resolve(null);
1532
+ const requestId = this.nextNativeCropId++;
1533
+ return new Promise((resolve) => {
1534
+ this.pendingNativeCrops.set(requestId, { resolve });
1535
+ try {
1536
+ this.child.send({
1537
+ kind: "nativeCrop",
1538
+ requestId,
1539
+ frameId,
1540
+ bbox,
1541
+ ...maxWidth !== void 0 ? { maxWidth } : {}
1542
+ });
1543
+ } catch {
1544
+ this.pendingNativeCrops.delete(requestId);
1545
+ resolve(null);
1546
+ }
1547
+ });
1548
+ }
1522
1549
  /** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
1523
1550
  teardown() {
1524
1551
  this.clearPullTimeout();
@@ -1551,6 +1578,16 @@ var SessionDecodeSession = class {
1551
1578
  case "buffer":
1552
1579
  this.settleToBuffer(raw.frameId, raw.bytes);
1553
1580
  return;
1581
+ case "nativeCropResult":
1582
+ this.settleNativeCrop(raw.requestId, {
1583
+ bytes: raw.bytes,
1584
+ width: raw.width,
1585
+ height: raw.height
1586
+ });
1587
+ return;
1588
+ case "nativeCropMiss":
1589
+ this.settleNativeCrop(raw.requestId, null);
1590
+ return;
1554
1591
  case "error":
1555
1592
  this.settleError(raw.message, raw.frameId);
1556
1593
  return;
@@ -1570,6 +1607,15 @@ var SessionDecodeSession = class {
1570
1607
  }
1571
1608
  for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
1572
1609
  this.pendingToBuffers.clear();
1610
+ for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(null);
1611
+ this.pendingNativeCrops.clear();
1612
+ }
1613
+ /** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
1614
+ settleNativeCrop(requestId, result) {
1615
+ const waiter = this.pendingNativeCrops.get(requestId);
1616
+ if (!waiter) return;
1617
+ this.pendingNativeCrops.delete(requestId);
1618
+ waiter.resolve(result);
1573
1619
  }
1574
1620
  settlePendingPull(outcome) {
1575
1621
  const waiter = this.pendingPull;
@@ -1701,10 +1747,12 @@ function buildFrameImage(session, reply) {
1701
1747
  width: reply.width,
1702
1748
  height: reply.height,
1703
1749
  format: reply.format,
1750
+ frameId: reply.frameId,
1704
1751
  toBuffer: async (opts) => {
1705
1752
  if (closed) throw new Error(`session-decode-coordinator: toBuffer() called on a closed frame (frameId=${reply.frameId})`);
1706
1753
  return session.requestBuffer(reply.frameId, opts);
1707
1754
  },
1755
+ nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
1708
1756
  close: () => {
1709
1757
  closed = true;
1710
1758
  }
@@ -1845,7 +1893,9 @@ function startSessionDecodePump(deps) {
1845
1893
  }
1846
1894
  try {
1847
1895
  const decoded = await toDecodedFrame(frame, format);
1848
- deps.onDecodedFrame(decoded);
1896
+ const image = frame.image;
1897
+ const nativeCrop = image.nativeCrop ? (bbox, maxWidth) => image.nativeCrop(bbox, maxWidth) : void 0;
1898
+ deps.onDecodedFrame(decoded, nativeCrop);
1849
1899
  } catch (err) {
1850
1900
  deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
1851
1901
  tags: { deviceId: deps.source.deviceId },
@@ -1878,6 +1928,40 @@ function startSessionDecodePump(deps) {
1878
1928
  iterator.return().catch(() => {});
1879
1929
  };
1880
1930
  }
1931
+ var NativeCropRegistry = class {
1932
+ capacity;
1933
+ entries = /* @__PURE__ */ new Map();
1934
+ constructor(capacity = 64) {
1935
+ this.capacity = capacity;
1936
+ }
1937
+ /** Number of registered handles (tests / metrics). */
1938
+ get size() {
1939
+ return this.entries.size;
1940
+ }
1941
+ /** A retention frame is uniquely identified by its segment + slot + committed seq. */
1942
+ key(handle) {
1943
+ return `${handle.shmId}#${handle.slot}#${handle.seq}`;
1944
+ }
1945
+ /** Bind `request` to `handle`; evicts the oldest entry past the cap. */
1946
+ register(handle, request) {
1947
+ const k = this.key(handle);
1948
+ this.entries.delete(k);
1949
+ this.entries.set(k, request);
1950
+ while (this.entries.size > this.capacity) {
1951
+ const oldest = this.entries.keys().next().value;
1952
+ if (oldest === void 0) break;
1953
+ this.entries.delete(oldest);
1954
+ }
1955
+ }
1956
+ /** The requester for `handle`, or `null` if never registered / already evicted. */
1957
+ get(handle) {
1958
+ return this.entries.get(this.key(handle)) ?? null;
1959
+ }
1960
+ /** Drop every entry (node/addon shutdown). */
1961
+ clear() {
1962
+ this.entries.clear();
1963
+ }
1964
+ };
1881
1965
  //#endregion
1882
1966
  //#region src/pipeline-runner/index.ts
1883
1967
  var DEFAULT_CONFIG = {
@@ -2102,6 +2186,13 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2102
2186
  * `resize()`d live in `onConfigChanged`. Null until `onInitialize`.
2103
2187
  */
2104
2188
  spawnGate = null;
2189
+ /**
2190
+ * Node-level map from a retention-ring `FrameHandle` to the decode worker
2191
+ * still holding that frame's NATIVE surface — backs the `getNativeCrop` cap
2192
+ * so post-analysis can cut native-res crops (best-effort; a miss falls back
2193
+ * to the detection-frame crop). See {@link NativeCropRegistry}.
2194
+ */
2195
+ nativeCropRegistry = new NativeCropRegistry();
2105
2196
  attached = /* @__PURE__ */ new Map();
2106
2197
  nodeId = "unknown";
2107
2198
  metricsSnapshotTimer = null;
@@ -2671,6 +2762,23 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2671
2762
  this.runner?.reportMotion(input.deviceId, input.detected, input.source, input.regions);
2672
2763
  return { success: true };
2673
2764
  }
2765
+ /**
2766
+ * Best-effort native-resolution crop for a retention-ring frame (native-crop
2767
+ * feature). Looks the handle up in {@link nativeCropRegistry}; a miss (never
2768
+ * registered, or the worker's tiny native ring already evicted the frame)
2769
+ * returns `null` so the caller falls back to a detection-frame crop. The
2770
+ * worker downloads ONLY the ROI — a full native frame never travels the wire.
2771
+ */
2772
+ async getNativeCrop(input) {
2773
+ const requester = this.nativeCropRegistry.get(input.handle);
2774
+ if (!requester) return null;
2775
+ const result = await requester(input.bbox, input.maxWidth);
2776
+ return result ? {
2777
+ bytes: result.bytes,
2778
+ width: result.width,
2779
+ height: result.height
2780
+ } : null;
2781
+ }
2674
2782
  detachInternal(deviceId) {
2675
2783
  const attachment = this.attached.get(deviceId);
2676
2784
  if (!attachment) return;
@@ -2990,8 +3098,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2990
3098
  format: "rgb"
2991
3099
  },
2992
3100
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
2993
- onDecodedFrame: (frame) => {
3101
+ onDecodedFrame: (frame, nativeCrop) => {
2994
3102
  const handle = retentionSink.write(frame) ?? void 0;
3103
+ if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
2995
3104
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
2996
3105
  }
2997
3106
  });
@@ -1,7 +1,7 @@
1
- import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-BalxNwOt.mjs";
2
- import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-BCJW5CvF.mjs";
1
+ import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-CEcTeu1h.mjs";
2
+ import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-CNCqC_XG.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
- import { t as isWorkerReply } from "../worker-protocol-pk7qdYXt.mjs";
4
+ import { t as isWorkerReply } from "../worker-protocol-DGIt_waM.mjs";
5
5
  import { DecoderFrameRingSink, FrameRingReaderCache, unlinkSegment } from "@camstack/shm-ring";
6
6
  import { fork } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
@@ -1451,6 +1451,9 @@ var SessionDecodeSession = class {
1451
1451
  frameTimeoutMs;
1452
1452
  pendingPull = null;
1453
1453
  pendingToBuffers = /* @__PURE__ */ new Map();
1454
+ /** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
1455
+ pendingNativeCrops = /* @__PURE__ */ new Map();
1456
+ nextNativeCropId = 1;
1454
1457
  exited = false;
1455
1458
  sigkillTimer = null;
1456
1459
  pullTimeoutTimer = null;
@@ -1515,6 +1518,30 @@ var SessionDecodeSession = class {
1515
1518
  }
1516
1519
  });
1517
1520
  }
1521
+ /**
1522
+ * Send `nativeCrop` for `frameId` and resolve with the crop result, or `null`
1523
+ * on any miss (frame aged out, worker error, or worker already exited). Never
1524
+ * rejects — native crops are strictly best-effort with a free fallback.
1525
+ */
1526
+ requestNativeCrop(frameId, bbox, maxWidth) {
1527
+ if (this.exited) return Promise.resolve(null);
1528
+ const requestId = this.nextNativeCropId++;
1529
+ return new Promise((resolve) => {
1530
+ this.pendingNativeCrops.set(requestId, { resolve });
1531
+ try {
1532
+ this.child.send({
1533
+ kind: "nativeCrop",
1534
+ requestId,
1535
+ frameId,
1536
+ bbox,
1537
+ ...maxWidth !== void 0 ? { maxWidth } : {}
1538
+ });
1539
+ } catch {
1540
+ this.pendingNativeCrops.delete(requestId);
1541
+ resolve(null);
1542
+ }
1543
+ });
1544
+ }
1518
1545
  /** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
1519
1546
  teardown() {
1520
1547
  this.clearPullTimeout();
@@ -1547,6 +1574,16 @@ var SessionDecodeSession = class {
1547
1574
  case "buffer":
1548
1575
  this.settleToBuffer(raw.frameId, raw.bytes);
1549
1576
  return;
1577
+ case "nativeCropResult":
1578
+ this.settleNativeCrop(raw.requestId, {
1579
+ bytes: raw.bytes,
1580
+ width: raw.width,
1581
+ height: raw.height
1582
+ });
1583
+ return;
1584
+ case "nativeCropMiss":
1585
+ this.settleNativeCrop(raw.requestId, null);
1586
+ return;
1550
1587
  case "error":
1551
1588
  this.settleError(raw.message, raw.frameId);
1552
1589
  return;
@@ -1566,6 +1603,15 @@ var SessionDecodeSession = class {
1566
1603
  }
1567
1604
  for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
1568
1605
  this.pendingToBuffers.clear();
1606
+ for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(null);
1607
+ this.pendingNativeCrops.clear();
1608
+ }
1609
+ /** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
1610
+ settleNativeCrop(requestId, result) {
1611
+ const waiter = this.pendingNativeCrops.get(requestId);
1612
+ if (!waiter) return;
1613
+ this.pendingNativeCrops.delete(requestId);
1614
+ waiter.resolve(result);
1569
1615
  }
1570
1616
  settlePendingPull(outcome) {
1571
1617
  const waiter = this.pendingPull;
@@ -1697,10 +1743,12 @@ function buildFrameImage(session, reply) {
1697
1743
  width: reply.width,
1698
1744
  height: reply.height,
1699
1745
  format: reply.format,
1746
+ frameId: reply.frameId,
1700
1747
  toBuffer: async (opts) => {
1701
1748
  if (closed) throw new Error(`session-decode-coordinator: toBuffer() called on a closed frame (frameId=${reply.frameId})`);
1702
1749
  return session.requestBuffer(reply.frameId, opts);
1703
1750
  },
1751
+ nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
1704
1752
  close: () => {
1705
1753
  closed = true;
1706
1754
  }
@@ -1841,7 +1889,9 @@ function startSessionDecodePump(deps) {
1841
1889
  }
1842
1890
  try {
1843
1891
  const decoded = await toDecodedFrame(frame, format);
1844
- deps.onDecodedFrame(decoded);
1892
+ const image = frame.image;
1893
+ const nativeCrop = image.nativeCrop ? (bbox, maxWidth) => image.nativeCrop(bbox, maxWidth) : void 0;
1894
+ deps.onDecodedFrame(decoded, nativeCrop);
1845
1895
  } catch (err) {
1846
1896
  deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
1847
1897
  tags: { deviceId: deps.source.deviceId },
@@ -1874,6 +1924,40 @@ function startSessionDecodePump(deps) {
1874
1924
  iterator.return().catch(() => {});
1875
1925
  };
1876
1926
  }
1927
+ var NativeCropRegistry = class {
1928
+ capacity;
1929
+ entries = /* @__PURE__ */ new Map();
1930
+ constructor(capacity = 64) {
1931
+ this.capacity = capacity;
1932
+ }
1933
+ /** Number of registered handles (tests / metrics). */
1934
+ get size() {
1935
+ return this.entries.size;
1936
+ }
1937
+ /** A retention frame is uniquely identified by its segment + slot + committed seq. */
1938
+ key(handle) {
1939
+ return `${handle.shmId}#${handle.slot}#${handle.seq}`;
1940
+ }
1941
+ /** Bind `request` to `handle`; evicts the oldest entry past the cap. */
1942
+ register(handle, request) {
1943
+ const k = this.key(handle);
1944
+ this.entries.delete(k);
1945
+ this.entries.set(k, request);
1946
+ while (this.entries.size > this.capacity) {
1947
+ const oldest = this.entries.keys().next().value;
1948
+ if (oldest === void 0) break;
1949
+ this.entries.delete(oldest);
1950
+ }
1951
+ }
1952
+ /** The requester for `handle`, or `null` if never registered / already evicted. */
1953
+ get(handle) {
1954
+ return this.entries.get(this.key(handle)) ?? null;
1955
+ }
1956
+ /** Drop every entry (node/addon shutdown). */
1957
+ clear() {
1958
+ this.entries.clear();
1959
+ }
1960
+ };
1877
1961
  //#endregion
1878
1962
  //#region src/pipeline-runner/index.ts
1879
1963
  var DEFAULT_CONFIG = {
@@ -2098,6 +2182,13 @@ var PipelineRunnerAddon = class extends BaseAddon {
2098
2182
  * `resize()`d live in `onConfigChanged`. Null until `onInitialize`.
2099
2183
  */
2100
2184
  spawnGate = null;
2185
+ /**
2186
+ * Node-level map from a retention-ring `FrameHandle` to the decode worker
2187
+ * still holding that frame's NATIVE surface — backs the `getNativeCrop` cap
2188
+ * so post-analysis can cut native-res crops (best-effort; a miss falls back
2189
+ * to the detection-frame crop). See {@link NativeCropRegistry}.
2190
+ */
2191
+ nativeCropRegistry = new NativeCropRegistry();
2101
2192
  attached = /* @__PURE__ */ new Map();
2102
2193
  nodeId = "unknown";
2103
2194
  metricsSnapshotTimer = null;
@@ -2667,6 +2758,23 @@ var PipelineRunnerAddon = class extends BaseAddon {
2667
2758
  this.runner?.reportMotion(input.deviceId, input.detected, input.source, input.regions);
2668
2759
  return { success: true };
2669
2760
  }
2761
+ /**
2762
+ * Best-effort native-resolution crop for a retention-ring frame (native-crop
2763
+ * feature). Looks the handle up in {@link nativeCropRegistry}; a miss (never
2764
+ * registered, or the worker's tiny native ring already evicted the frame)
2765
+ * returns `null` so the caller falls back to a detection-frame crop. The
2766
+ * worker downloads ONLY the ROI — a full native frame never travels the wire.
2767
+ */
2768
+ async getNativeCrop(input) {
2769
+ const requester = this.nativeCropRegistry.get(input.handle);
2770
+ if (!requester) return null;
2771
+ const result = await requester(input.bbox, input.maxWidth);
2772
+ return result ? {
2773
+ bytes: result.bytes,
2774
+ width: result.width,
2775
+ height: result.height
2776
+ } : null;
2777
+ }
2670
2778
  detachInternal(deviceId) {
2671
2779
  const attachment = this.attached.get(deviceId);
2672
2780
  if (!attachment) return;
@@ -2986,8 +3094,9 @@ var PipelineRunnerAddon = class extends BaseAddon {
2986
3094
  format: "rgb"
2987
3095
  },
2988
3096
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
2989
- onDecodedFrame: (frame) => {
3097
+ onDecodedFrame: (frame, nativeCrop) => {
2990
3098
  const handle = retentionSink.write(frame) ?? void 0;
3099
+ if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
2991
3100
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
2992
3101
  }
2993
3102
  });
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("../chunk-D6vf50IK.js");
2
- const require_dist = require("../dist-DWb62H5U.js");
2
+ const require_dist = require("../dist-BbaoC680.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-BalxNwOt.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-CEcTeu1h.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
- import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-BalxNwOt.mjs";
1
+ import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-CEcTeu1h.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DWb62H5U.js");
1
+ const require_dist = require("./dist-BbaoC680.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**