@camstack/addon-pipeline 1.2.19 → 1.2.28

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +3 -2
  3. package/dist/chunk-DnnnRqeS.mjs +26 -0
  4. package/dist/detection-pipeline/index.js +2 -2
  5. package/dist/detection-pipeline/index.mjs +4 -3
  6. package/dist/{dist-DZBePRjP.mjs → dist-CsKaXmP8.mjs} +415 -126
  7. package/dist/{dist-DKqOqH5j.js → dist-DUHlZcWd.js} +420 -131
  8. package/dist/{model-download-service-Cp9f4dk6-WO-foD7W.mjs → model-download-service-Cp9f4dk6-CwUb5v3Y.mjs} +1 -26
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +230 -28
  12. package/dist/pipeline-runner/index.mjs +230 -28
  13. package/dist/recorder/index.js +2604 -458
  14. package/dist/recorder/index.mjs +2599 -459
  15. package/dist/{step-definitions-zDq8cP8M.js → step-definitions-Bo1hJQQw.js} +1 -1
  16. package/dist/{step-definitions-BwB6TBMB.mjs → step-definitions-C8A78dBi.mjs} +1 -1
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bwuv7iK_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-tHahN-lK.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1zJmtUe.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-DfnRAN5T.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CSWUWZ8c.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-DC0aArn1.mjs → hostInit-CIwilJUZ.mjs} +2 -2
  22. package/dist/stream-broker/index.js +7374 -250
  23. package/dist/stream-broker/index.mjs +7371 -249
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C2hbIwzB.js → MaskShapeCanvas-DI4BY7W2-B7VdNJlO.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BpJ-foJW.js → MotionZonesSettings-NcxxQN8r-YyB4uLdH.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BjLs9Z3w.js → PrivacyMaskSettings-APgPLF7p-DfqixEad.js} +1 -1
  28. package/embed-dist/assets/{index-Dv5H-32C.css → index-CAe6Dg-X.css} +1 -1
  29. package/embed-dist/assets/index-zVLLVdIS.js +343 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +5 -1
  32. package/dist/fmp4-keyframes-5s8T9Mng.js +0 -6711
  33. package/dist/fmp4-keyframes-CV0X1mGy.mjs +0 -6694
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDeEvhV5.mjs +0 -26
  35. package/embed-dist/assets/index-_BRQTmdE.js +0 -116
@@ -1,8 +1,8 @@
1
- import { B as errMsg, G as createEvent, M as pipelineRunnerCapability, S as defineCustomActions, V as BaseAddon, X as nodePin, at as lazy, b as customAction, ct as object, ft as EventCategory, nt as array, rt as boolean, s as DEVICE_BACKEND_TO_FORMAT, st as number, tt as _enum, ut as string } from "../dist-DZBePRjP.mjs";
1
+ import { B as errMsg, G as createEvent, M as pipelineRunnerCapability, S as defineCustomActions, V as BaseAddon, X as nodePin, at as lazy, b as customAction, ct as object, ft as EventCategory, nt as array, rt as boolean, s as DEVICE_BACKEND_TO_FORMAT, st as number, tt as _enum, ut as string } from "../dist-CsKaXmP8.mjs";
2
2
  import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-D7RzZIla.mjs";
5
- import { a as getStepDefinition } from "../step-definitions-BwB6TBMB.mjs";
5
+ import { a as getStepDefinition } from "../step-definitions-C8A78dBi.mjs";
6
6
  import { fork } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import sharp from "sharp";
@@ -569,6 +569,22 @@ var PipelineTimingSampler = class {
569
569
  //#endregion
570
570
  //#region src/pipeline-runner/runner.ts
571
571
  /**
572
+ * Bound on one `processFrame` call. Chosen well above the slowest legitimate
573
+ * inference seen in production (8 617 ms, GPU contention on the hub) so the
574
+ * timeout only ever fires on a call that is genuinely never coming back.
575
+ */
576
+ var DEFAULT_INFERENCE_TIMEOUT_MS = 2e4;
577
+ /** Grace period before a permit-starved scheduler with queued work warns. */
578
+ var DEFAULT_INFERENCE_STALL_WARN_MS = 6e4;
579
+ /** Re-warn cadence while a stall persists, so a wedge stays visible. */
580
+ var INFERENCE_STALL_REWARN_MS = 6e4;
581
+ /**
582
+ * Sentinel distinguishing "the bound expired" from a legitimate `null`
583
+ * (`processFrame` returning no result). A unique symbol, so it can never
584
+ * collide with a value the executor produces.
585
+ */
586
+ var TIMED_OUT = Symbol("inference-timed-out");
587
+ /**
572
588
  * Clone a {@link DetectionQueueEntry} for the queue's copy-on-retain
573
589
  * boundary: the inner frame's pixel buffer is detached via
574
590
  * {@link ownFrame}, the `FrameHandle` is carried through unchanged (it's
@@ -651,6 +667,10 @@ var PipelineRunner = class {
651
667
  arrivalCounts = /* @__PURE__ */ new Map();
652
668
  lastGovernorRecomputeAt = Date.now();
653
669
  governorIntervalHandle = null;
670
+ /** When the scheduler first found itself with zero permits; null when healthy. */
671
+ inferenceStallSince = null;
672
+ /** Last stall warning, so a persistent wedge re-warns instead of going quiet. */
673
+ inferenceStallWarnedAt = null;
654
674
  constructor(config) {
655
675
  this.config = config;
656
676
  this.logger = config.logger;
@@ -1041,7 +1061,12 @@ var PipelineRunner = class {
1041
1061
  }
1042
1062
  tick() {
1043
1063
  this.drainMotionQueues();
1044
- if (this.semaphore.available <= 0) return;
1064
+ if (this.semaphore.available <= 0) {
1065
+ this.noteInferenceStall();
1066
+ return;
1067
+ }
1068
+ this.inferenceStallSince = null;
1069
+ this.inferenceStallWarnedAt = null;
1045
1070
  const picked = this.pickNextDetectionFrame();
1046
1071
  if (!picked) return;
1047
1072
  const { deviceId, entry, state } = picked;
@@ -1064,11 +1089,18 @@ var PipelineRunner = class {
1064
1089
  const pickedAt = Date.now();
1065
1090
  const { frame, handle } = entry;
1066
1091
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
1067
- const release = await this.semaphore.acquire();
1092
+ const rawRelease = await this.semaphore.acquire();
1093
+ let released = false;
1094
+ const release = () => {
1095
+ if (released) return;
1096
+ released = true;
1097
+ rawRelease();
1098
+ };
1068
1099
  const semAcquiredAt = Date.now();
1069
1100
  const executorHandle = state.detectionQueue.size <= HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH && this.semaphore.available > 0 ? handle : void 0;
1070
1101
  try {
1071
- const result = await this.config.processFrame(deviceId, frameInput, executorHandle);
1102
+ const result = await this.awaitInferenceBounded(deviceId, frameInput, executorHandle, release);
1103
+ if (result === TIMED_OUT) return;
1072
1104
  const inferDoneAt = Date.now();
1073
1105
  const inferenceMs = inferDoneAt - semAcquiredAt;
1074
1106
  state.inferenceTimes.push(inferenceMs);
@@ -1092,6 +1124,73 @@ var PipelineRunner = class {
1092
1124
  release();
1093
1125
  }
1094
1126
  }
1127
+ /**
1128
+ * Await `processFrame` with a hard bound. On expiry the permit is released
1129
+ * immediately (via the caller's release-once closure) and {@link TIMED_OUT}
1130
+ * is returned; the underlying call is left to settle into the void, since
1131
+ * the executor contract offers no cancellation.
1132
+ */
1133
+ async awaitInferenceBounded(deviceId, frameInput, executorHandle, release) {
1134
+ const timeoutMs = this.config.inferenceTimeoutMs ?? DEFAULT_INFERENCE_TIMEOUT_MS;
1135
+ let timer = null;
1136
+ const expiry = new Promise((resolve) => {
1137
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
1138
+ timer.unref?.();
1139
+ });
1140
+ try {
1141
+ const outcome = await Promise.race([this.config.processFrame(deviceId, frameInput, executorHandle), expiry]);
1142
+ if (outcome === TIMED_OUT) {
1143
+ release();
1144
+ this.logger?.warn("inference timed out — permit released, result discarded", {
1145
+ tags: { deviceId },
1146
+ meta: {
1147
+ timeoutMs,
1148
+ availablePermits: this.semaphore.available
1149
+ }
1150
+ });
1151
+ }
1152
+ return outcome;
1153
+ } finally {
1154
+ if (timer !== null) clearTimeout(timer);
1155
+ }
1156
+ }
1157
+ /**
1158
+ * Called on every tick that finds zero available permits. Warns once the
1159
+ * starvation outlives `inferenceStallWarnMs` WITH queued work — queued work
1160
+ * is the discriminator: a runner with no permits and no backlog is simply
1161
+ * busy, while one with a backlog it cannot touch is wedged.
1162
+ */
1163
+ noteInferenceStall() {
1164
+ let queuedFrames = 0;
1165
+ for (const state of this.cameras.values()) queuedFrames += state.detectionQueue.size;
1166
+ if (queuedFrames === 0) {
1167
+ this.inferenceStallSince = null;
1168
+ this.inferenceStallWarnedAt = null;
1169
+ return;
1170
+ }
1171
+ const now = Date.now();
1172
+ if (this.inferenceStallSince === null) {
1173
+ this.inferenceStallSince = now;
1174
+ return;
1175
+ }
1176
+ const stalledMs = now - this.inferenceStallSince;
1177
+ if (stalledMs < (this.config.inferenceStallWarnMs ?? DEFAULT_INFERENCE_STALL_WARN_MS)) return;
1178
+ if (this.inferenceStallWarnedAt !== null && now - this.inferenceStallWarnedAt < INFERENCE_STALL_REWARN_MS) return;
1179
+ this.inferenceStallWarnedAt = now;
1180
+ for (const [deviceId, state] of this.cameras) {
1181
+ if (state.detectionQueue.size === 0) continue;
1182
+ this.logger?.warn("no inference permit available while frames are queued", {
1183
+ tags: { deviceId },
1184
+ meta: {
1185
+ stalledMs,
1186
+ queuedFrames: state.detectionQueue.size,
1187
+ droppedFrames: state.detectionQueue.droppedFrames,
1188
+ phase: state.phase,
1189
+ concurrency: this.semaphore.concurrency
1190
+ }
1191
+ });
1192
+ }
1193
+ }
1095
1194
  async notifyCallbacks(deviceId, frame, result, streamType, handle) {
1096
1195
  for (const callback of this.resultCallbacks) try {
1097
1196
  await callback(deviceId, frame, result, streamType, handle);
@@ -1231,7 +1330,24 @@ function forkDecodeWorker(_source) {
1231
1330
  //#endregion
1232
1331
  //#region src/session-decode/guarded-session-decode.ts
1233
1332
  /**
1234
- * Guards the in-flight restream-acquire against a detach/unsubscribe race.
1333
+ * 2s 5s 15s 30s, then 30s forever.
1334
+ *
1335
+ * Indefinite is deliberate. The failure this recovers from is "the broker has
1336
+ * no profile bound yet" or "the broker call failed right now" — both of which
1337
+ * clear on their own, often minutes later (a source re-dialing, a runner
1338
+ * restarting). A bounded retry would turn a slow recovery back into the
1339
+ * permanent outage this exists to prevent, and teardown already stops it: a
1340
+ * detach aborts the loop, so nothing retries for a camera nobody wants.
1341
+ */
1342
+ var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
1343
+ 2e3,
1344
+ 5e3,
1345
+ 15e3,
1346
+ 3e4
1347
+ ];
1348
+ /**
1349
+ * Guards the in-flight restream-acquire against a detach/unsubscribe race,
1350
+ * and retries an acquire that fails.
1235
1351
  *
1236
1352
  * `acquireSessionDecodeRestream` + forking the decode worker both take real
1237
1353
  * time (a broker round-trip, then a process fork). If the caller's teardown
@@ -1243,29 +1359,71 @@ function forkDecodeWorker(_source) {
1243
1359
  *
1244
1360
  * This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
1245
1361
  * run in the background; an `aborted` flag recorded by the (possibly
1246
- * already-called) teardown is checked right after the acquire resolves —
1247
- * if set, the restream is released immediately and the pump is NEVER
1362
+ * already-called) teardown is checked right after each acquire resolves —
1363
+ * if set, any restream is released immediately and the pump is NEVER
1248
1364
  * started (no orphan worker). Otherwise the pump starts and the teardown,
1249
1365
  * once called, stops the pump then releases the restream.
1366
+ *
1367
+ * **A failed acquire is retried, not swallowed.** The teardown handed back
1368
+ * here is stored by the caller (as `attachment.detectionUnsubscribe`) the
1369
+ * instant this returns, so a `return` on failure left that handle permanently
1370
+ * non-null with no pump behind it: `onEnded` could never fire because nothing
1371
+ * had ever started, the camera's phase stayed `active`, and not one frame
1372
+ * arrived again. The motion path was hardened against this shape by a frame
1373
+ * freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
1374
+ * path's half.
1250
1375
  */
1251
1376
  function startGuardedSessionDecode(deps) {
1252
1377
  let aborted = false;
1253
1378
  let stopPump = null;
1254
1379
  let release = null;
1380
+ let retryTimer = null;
1381
+ const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
1382
+ /** Resolves after the backoff for `attempt`, or immediately once aborted. */
1383
+ const waitBackoff = (attempt) => new Promise((resolve) => {
1384
+ const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
1385
+ retryTimer = setTimeout(() => {
1386
+ retryTimer = null;
1387
+ resolve();
1388
+ }, ms);
1389
+ retryTimer.unref?.();
1390
+ });
1255
1391
  (async () => {
1256
- const acquired = await deps.acquire();
1257
- if (!acquired) return;
1258
- if (aborted) {
1259
- acquired.release();
1392
+ for (let attempt = 0; !aborted; attempt++) {
1393
+ let acquired = null;
1394
+ try {
1395
+ acquired = await deps.acquire();
1396
+ } catch (err) {
1397
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
1398
+ attempt: attempt + 1,
1399
+ error: errMsg(err)
1400
+ } });
1401
+ await waitBackoff(attempt);
1402
+ continue;
1403
+ }
1404
+ if (aborted) {
1405
+ if (acquired) acquired.release();
1406
+ return;
1407
+ }
1408
+ if (!acquired) {
1409
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
1410
+ await waitBackoff(attempt);
1411
+ continue;
1412
+ }
1413
+ if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
1414
+ release = acquired.release;
1415
+ stopPump = deps.startPump(acquired.source);
1260
1416
  return;
1261
1417
  }
1262
- release = acquired.release;
1263
- stopPump = deps.startPump(acquired.source);
1264
1418
  })().catch((err) => {
1265
1419
  deps.logger.warn("session-decode: guarded start failed", { meta: { error: errMsg(err) } });
1266
1420
  });
1267
1421
  return () => {
1268
1422
  aborted = true;
1423
+ if (retryTimer !== null) {
1424
+ clearTimeout(retryTimer);
1425
+ retryTimer = null;
1426
+ }
1269
1427
  stopPump?.();
1270
1428
  if (release) release();
1271
1429
  };
@@ -2057,10 +2215,14 @@ var NativeCropRegistry = class {
2057
2215
  * Bind `request` to `handle`; evicts the oldest entry past the cap, releasing
2058
2216
  * its worker-side RAM lease so a lease never outlives its registry entry.
2059
2217
  */
2060
- register(handle, request) {
2218
+ register(handle, request, deviceId) {
2061
2219
  const k = this.key(handle);
2062
2220
  this.entries.delete(k);
2063
- this.entries.set(k, request);
2221
+ this.entries.set(k, {
2222
+ request,
2223
+ deviceId,
2224
+ registeredAt: Date.now()
2225
+ });
2064
2226
  while (this.entries.size > this.capacity) {
2065
2227
  const oldest = this.entries.keys().next().value;
2066
2228
  if (oldest === void 0) break;
@@ -2069,7 +2231,22 @@ var NativeCropRegistry = class {
2069
2231
  }
2070
2232
  /** The requester for `handle`, or `null` if never registered / already evicted. */
2071
2233
  get(handle) {
2072
- return this.entries.get(this.key(handle)) ?? null;
2234
+ const entry = this.entries.get(this.key(handle));
2235
+ if (entry === void 0) return null;
2236
+ return {
2237
+ request: entry.request,
2238
+ deviceId: entry.deviceId,
2239
+ ageMs: Date.now() - entry.registeredAt
2240
+ };
2241
+ }
2242
+ /**
2243
+ * The deviceId a handle was registered under, even AFTER its entry was
2244
+ * evicted — kept in a small parallel ring so a `no-registry-entry` miss can
2245
+ * still be attributed to a camera (a miss line without `deviceId` cannot
2246
+ * answer "why is 617 worse than 615?").
2247
+ */
2248
+ deviceIdFor(handle) {
2249
+ return this.evictedDeviceIds.get(this.key(handle)) ?? this.entries.get(this.key(handle))?.deviceId ?? null;
2073
2250
  }
2074
2251
  /**
2075
2252
  * Release the worker-side RAM native lease for `handle` and drop its entry —
@@ -2081,15 +2258,27 @@ var NativeCropRegistry = class {
2081
2258
  }
2082
2259
  /** Drop every entry, releasing each frame's worker-side lease (node/addon shutdown). */
2083
2260
  clear() {
2084
- for (const request of this.entries.values()) safeRelease(request);
2261
+ for (const entry of this.entries.values()) safeRelease(entry.request);
2085
2262
  this.entries.clear();
2263
+ this.evictedDeviceIds.clear();
2086
2264
  }
2087
- /** Release + drop one entry by its map key. */
2265
+ /** Release + drop one entry by its map key, remembering its camera. */
2088
2266
  releaseEntry(key) {
2089
- const request = this.entries.get(key);
2090
- if (!request) return;
2267
+ const entry = this.entries.get(key);
2268
+ if (!entry) return;
2091
2269
  this.entries.delete(key);
2092
- safeRelease(request);
2270
+ this.rememberEvicted(key, entry.deviceId);
2271
+ safeRelease(entry.request);
2272
+ }
2273
+ /** Bounded memory of evicted handles → deviceId, for miss attribution. */
2274
+ evictedDeviceIds = /* @__PURE__ */ new Map();
2275
+ rememberEvicted(key, deviceId) {
2276
+ this.evictedDeviceIds.set(key, deviceId);
2277
+ while (this.evictedDeviceIds.size > this.capacity) {
2278
+ const oldest = this.evictedDeviceIds.keys().next().value;
2279
+ if (oldest === void 0) break;
2280
+ this.evictedDeviceIds.delete(oldest);
2281
+ }
2093
2282
  }
2094
2283
  };
2095
2284
  //#endregion
@@ -3295,16 +3484,29 @@ var PipelineRunnerAddon = class extends BaseAddon {
3295
3484
  const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
3296
3485
  const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
3297
3486
  let nativeResult = null;
3487
+ let missReason = null;
3298
3488
  if (!isUnboundedFullFrame) {
3299
- const requester = this.nativeCropRegistry.get(input.handle);
3300
- if (requester) {
3301
- const result = await requester(input.bbox, input.maxWidth);
3489
+ const found = this.nativeCropRegistry.get(input.handle);
3490
+ if (found) {
3491
+ const result = await found.request(input.bbox, input.maxWidth);
3302
3492
  if (result) nativeResult = {
3303
3493
  bytes: result.bytes,
3304
3494
  width: result.width,
3305
3495
  height: result.height
3306
3496
  };
3307
- }
3497
+ else missReason = "worker-lease-gone";
3498
+ } else missReason = "no-registry-entry";
3499
+ if (missReason !== null) this.ctx.logger.debug("native crop miss", {
3500
+ tags: { deviceId: found?.deviceId ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
3501
+ meta: {
3502
+ reason: missReason,
3503
+ handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
3504
+ handleNodeId: input.handle.nodeId,
3505
+ handleAgeMs: found?.ageMs ?? null,
3506
+ registrySize: this.nativeCropRegistry.size,
3507
+ registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
3508
+ }
3509
+ });
3308
3510
  }
3309
3511
  const tier = resolveNativeCropSource(isFullFrame, nativeResult !== null);
3310
3512
  if (tier === "native" && nativeResult) return this.encodeNativeCropResult(nativeResult, tier, input.encodeJpeg);
@@ -3785,7 +3987,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
3785
3987
  onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
3786
3988
  onDecodedFrame: (frame, nativeCrop) => {
3787
3989
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
3788
- if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
3990
+ if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
3789
3991
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
3790
3992
  }
3791
3993
  });
@@ -4298,7 +4500,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
4298
4500
  onDecodedFrame: (frame, nativeCrop) => {
4299
4501
  if (settled) return;
4300
4502
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
4301
- if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
4503
+ if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
4302
4504
  runner.enqueueOccupancyFrame(deviceId, frame, handle);
4303
4505
  count++;
4304
4506
  if (count >= frames) {