@camstack/addon-pipeline 1.2.1 → 1.2.2

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 +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-E0K373T1.js → dist-CVjqvU8G.js} +66 -6
  6. package/dist/{dist-C3cDjL49.mjs → dist-ClyAFBZk.mjs} +66 -6
  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 +198 -21
  10. package/dist/pipeline-runner/index.mjs +198 -21
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/session-decode/decode-worker-child.js +55 -7
  14. package/dist/session-decode/decode-worker-child.mjs +55 -7
  15. package/dist/{step-definitions-DMne6GHq.js → step-definitions-D76MMwbo.js} +1 -1
  16. package/dist/{step-definitions-ty97QrzE.mjs → step-definitions-DMOw4fzn.mjs} +1 -1
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B2g-vEQt.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-7jRy3Uzr.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-UGa9yj7_.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQC_f4Yv.mjs} +1 -1
  20. package/dist/stream-broker/{hostInit-C3C5oZGy.mjs → hostInit-4pxims1d.mjs} +3 -3
  21. package/dist/stream-broker/index.js +1 -1
  22. package/dist/stream-broker/index.mjs +1 -1
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/dist/{worker-protocol-CyVJTZEO.mjs → worker-protocol-D7RzZIla.mjs} +16 -2
  25. package/dist/{worker-protocol-PP4jKHHJ.js → worker-protocol-DextwlTX.js} +21 -1
  26. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BM5dXUL6.js → MaskShapeCanvas-DI4BY7W2-BPRZW9TX.js} +1 -1
  27. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-C7QpBII3.js → MotionZonesSettings-NcxxQN8r-Bw6Nlg-Q.js} +1 -1
  28. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DgvSlqrq.js → PrivacyMaskSettings-APgPLF7p-BLF9QKG8.js} +1 -1
  29. package/embed-dist/assets/{index-C3bYXV0t.js → index-DiUhKfSk.js} +13 -13
  30. package/embed-dist/assets/index-Dv5H-32C.css +2 -0
  31. package/embed-dist/index.html +2 -2
  32. package/package.json +1 -1
  33. package/embed-dist/assets/index-D4MGuECB.css +0 -2
@@ -1,8 +1,8 @@
1
- import { H as errMsg, N as pipelineRunnerCapability, S as defineCustomActions, U as BaseAddon, b as customAction, ct as number, dt as string, it as boolean, j as nodePin, lt as object, nt as _enum, ot as lazy, pt as EventCategory, q as createEvent, rt as array } from "../dist-C3cDjL49.mjs";
1
+ import { H as errMsg, N as pipelineRunnerCapability, S as defineCustomActions, U as BaseAddon, b as customAction, ct as number, dt as string, it as boolean, j as nodePin, lt as object, nt as _enum, ot as lazy, pt as EventCategory, q as createEvent, rt as array, s as DEVICE_BACKEND_TO_FORMAT } from "../dist-ClyAFBZk.mjs";
2
2
  import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
3
3
  import { r as resolveHubHostname } from "../sheet-geometry-DcpuGTCm.mjs";
4
- import { t as isWorkerReply } from "../worker-protocol-CyVJTZEO.mjs";
5
- import { a as getStepDefinition } from "../step-definitions-ty97QrzE.mjs";
4
+ import { t as isWorkerReply } from "../worker-protocol-D7RzZIla.mjs";
5
+ import { a as getStepDefinition } from "../step-definitions-DMOw4fzn.mjs";
6
6
  import { fork } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import sharp from "sharp";
@@ -1674,11 +1674,18 @@ var SessionDecodeSession = class {
1674
1674
  });
1675
1675
  return;
1676
1676
  case "nativeCropMiss":
1677
+ if (raw.reason !== void 0) this.logger.debug("session-decode: native crop miss", { meta: {
1678
+ requestId: raw.requestId,
1679
+ reason: raw.reason
1680
+ } });
1677
1681
  this.settleNativeCrop(raw.requestId, null);
1678
1682
  return;
1679
1683
  case "error":
1680
1684
  this.settleError(raw.message, raw.frameId);
1681
1685
  return;
1686
+ case "log":
1687
+ this.logger[raw.level](raw.message);
1688
+ return;
1682
1689
  }
1683
1690
  }
1684
1691
  handleExit(info) {
@@ -2247,13 +2254,93 @@ async function runDetailSubtree(deps, input) {
2247
2254
  if (!crop) return null;
2248
2255
  const details = [];
2249
2256
  for (const step of steps) {
2250
- const result = await deps.runPipeline([step], crop.jpeg, input.deviceId);
2257
+ const deviceKey = deps.resolveStepDevice?.(input.deviceId, step);
2258
+ const result = await deps.runPipeline([step], crop.jpeg, input.deviceId, deviceKey);
2251
2259
  if (!result) continue;
2252
2260
  for (const detection of result.detections) details.push(toDetailResult(detection, step.addonId, crop));
2253
2261
  }
2254
2262
  return { details };
2255
2263
  }
2256
2264
  //#endregion
2265
+ //#region src/pipeline-runner/device-jump.ts
2266
+ /**
2267
+ * Step-tree device jump (same-node, phase 1) — the PURE resolution core.
2268
+ *
2269
+ * Given the camera's elected ("effective") inference device and the node's
2270
+ * enabled-device roster, decide which device ONE enrichment step should run on:
2271
+ *
2272
+ * 1. A manual `jumpDeviceKey` override wins when it names a candidate that can
2273
+ * run the step (bypasses the auto choice; operator intent).
2274
+ * 2. Otherwise, if the effective device's format can run the step's model →
2275
+ * NO jump (the step inherits the effective device — the common case).
2276
+ * 3. Otherwise AUTO-jump: among the node's OTHER enabled devices whose format
2277
+ * can run the step (and that are under their `maxSessions` cap), pick the
2278
+ * weighted-least-loaded one — mirroring the balancer's per-device core.
2279
+ * 4. If nothing on the node can run it → `no-candidate` (the caller emits the
2280
+ * loud once-per-(camera,step) warn that replaces today's silent degrade).
2281
+ *
2282
+ * Framework-free + side-effect-free so the whole decision table is unit-testable
2283
+ * without a live runner/catalog. The `canRun(format)` predicate is injected by
2284
+ * the runner (which owns the model catalog); this module never imports it.
2285
+ */
2286
+ /** The `ModelFormat` of a `<backend>:<device>` key, or `undefined` for an unknown backend. */
2287
+ function formatOf(deviceKey) {
2288
+ const colon = deviceKey.indexOf(":");
2289
+ return DEVICE_BACKEND_TO_FORMAT[colon >= 0 ? deviceKey.slice(0, colon) : deviceKey];
2290
+ }
2291
+ /** True when the step's model has a build for `deviceKey`'s format. */
2292
+ function canRunOn(deviceKey, canRun) {
2293
+ const format = formatOf(deviceKey);
2294
+ return format !== void 0 && canRun(format);
2295
+ }
2296
+ /** Effective weighted load of a device (session count ÷ weight); lower = preferred. */
2297
+ function weightedLoad(d) {
2298
+ return d.attachedCameras / (d.weight > 0 ? d.weight : 1);
2299
+ }
2300
+ /**
2301
+ * Weighted-least-loaded pick with a deterministic `deviceKey` tie-break —
2302
+ * mirrors the node/device balancer's decision core (least `load/weight`, ties
2303
+ * broken stably). `devices` is assumed non-empty.
2304
+ */
2305
+ function pickWeightedLeastLoaded(devices) {
2306
+ return devices.reduce((best, d) => {
2307
+ const dScore = weightedLoad(d);
2308
+ const bScore = weightedLoad(best);
2309
+ if (dScore < bScore) return d;
2310
+ if (dScore > bScore) return best;
2311
+ return d.deviceKey < best.deviceKey ? d : best;
2312
+ });
2313
+ }
2314
+ /**
2315
+ * Resolve the device one enrichment step runs on. See the module header for the
2316
+ * decision table. Pure — never throws, no I/O.
2317
+ */
2318
+ function resolveStepDevice(input) {
2319
+ const { effectiveDeviceKey, manualJumpDeviceKey, canRun } = input;
2320
+ if (manualJumpDeviceKey !== void 0 && manualJumpDeviceKey !== effectiveDeviceKey) {
2321
+ const target = input.devices.find((d) => d.deviceKey === manualJumpDeviceKey);
2322
+ if (target && canRunOn(target.deviceKey, canRun)) return {
2323
+ kind: "jumped",
2324
+ deviceKey: target.deviceKey,
2325
+ reason: "manual"
2326
+ };
2327
+ }
2328
+ if (canRunOn(effectiveDeviceKey, canRun)) return {
2329
+ kind: "no-jump",
2330
+ deviceKey: effectiveDeviceKey
2331
+ };
2332
+ const runnable = input.devices.filter((d) => d.deviceKey !== effectiveDeviceKey && (d.maxSessions === null || d.attachedCameras < d.maxSessions) && canRunOn(d.deviceKey, canRun));
2333
+ if (runnable.length === 0) return {
2334
+ kind: "no-candidate",
2335
+ deviceKey: effectiveDeviceKey
2336
+ };
2337
+ return {
2338
+ kind: "jumped",
2339
+ deviceKey: pickWeightedLeastLoaded(runnable).deviceKey,
2340
+ reason: "auto"
2341
+ };
2342
+ }
2343
+ //#endregion
2257
2344
  //#region src/pipeline-runner/index.ts
2258
2345
  var DEFAULT_CONFIG = {
2259
2346
  maxQueueDepth: 30,
@@ -3218,18 +3305,48 @@ var PipelineRunnerAddon = class extends BaseAddon {
3218
3305
  }
3219
3306
  }
3220
3307
  const tier = resolveNativeCropSource(isFullFrame, nativeResult !== null);
3221
- if (tier === "native" && nativeResult) return {
3222
- ...nativeResult,
3223
- tier
3224
- };
3308
+ if (tier === "native" && nativeResult) return this.encodeNativeCropResult(nativeResult, tier, input.encodeJpeg);
3225
3309
  if (tier !== "ram-fullframe") return null;
3226
3310
  const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
3227
- return crop ? {
3311
+ if (!crop) return null;
3312
+ return this.encodeNativeCropResult({
3313
+ bytes: crop.bytes,
3314
+ width: crop.width,
3315
+ height: crop.height
3316
+ }, tier, input.encodeJpeg);
3317
+ }
3318
+ /**
3319
+ * Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
3320
+ * return the zero-copy `bytes` — same-node callers encode locally. Compressed
3321
+ * path (`encodeJpeg`, the CROSS-NODE case): encode to JPEG in-process, on the
3322
+ * OWNING node, and return `jpeg` (base64) with NO `bytes` — the payload then
3323
+ * fits Moleculer's `maxPacketSize` and no raw pixels cross the process
3324
+ * boundary (CLAUDE.md invariant #21). On an encode failure the crop is a miss
3325
+ * (`null`) — the caller retries — never a silent raw-RGB cross-node fallback.
3326
+ */
3327
+ async encodeNativeCropResult(crop, tier, encodeJpeg) {
3328
+ if (encodeJpeg !== true) return {
3228
3329
  bytes: crop.bytes,
3229
3330
  width: crop.width,
3230
3331
  height: crop.height,
3231
3332
  tier
3232
- } : null;
3333
+ };
3334
+ try {
3335
+ const sharp = (await import("sharp")).default;
3336
+ return {
3337
+ jpeg: (await sharp(Buffer.from(crop.bytes), { raw: {
3338
+ width: crop.width,
3339
+ height: crop.height,
3340
+ channels: 3
3341
+ } }).jpeg({ quality: 90 }).toBuffer()).toString("base64"),
3342
+ width: crop.width,
3343
+ height: crop.height,
3344
+ tier
3345
+ };
3346
+ } catch (err) {
3347
+ this.ctx.logger.warn("getNativeCrop: cross-node JPEG encode failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
3348
+ return null;
3349
+ }
3233
3350
  }
3234
3351
  /**
3235
3352
  * Two-plane design: run the DETAIL subtree (crop children) for a single
@@ -3244,10 +3361,72 @@ var PipelineRunnerAddon = class extends BaseAddon {
3244
3361
  return runDetailSubtree({
3245
3362
  resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
3246
3363
  getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
3247
- runPipeline: (steps, imageJpeg, deviceId) => this.runDetailPipeline(steps, imageJpeg, deviceId)
3364
+ runPipeline: (steps, imageJpeg, deviceId, deviceKey) => this.runDetailPipeline(steps, imageJpeg, deviceId, deviceKey),
3365
+ resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step)
3248
3366
  }, input);
3249
3367
  }
3250
3368
  /**
3369
+ * Step-tree device jump (phase 1) — pick which same-node inference device an
3370
+ * enrichment `step` runs on. AUTO-jumps only when the camera's elected device
3371
+ * cannot run the step's model (its format has no build); an operator
3372
+ * `step.jumpDeviceKey` overrides. Returns the resolved `<backend>:<device>`
3373
+ * key, or `undefined` (⇒ the runner's per-camera default pool) when there is
3374
+ * nothing to route: no elected device, or no jump roster on the attach.
3375
+ *
3376
+ * When NO enabled device on the node can run the step, logs a LOUD warn once
3377
+ * per (camera, step) — killing the historical SILENT per-child degrade (an
3378
+ * `edgetpu` root would otherwise drop faces/plates/clip with no signal).
3379
+ */
3380
+ resolveDetailStepDevice(deviceId, step) {
3381
+ const config = this.attached.get(deviceId)?.config;
3382
+ const effectiveDeviceKey = config?.deviceKey;
3383
+ if (!effectiveDeviceKey) return void 0;
3384
+ const roster = config?.inferenceDevices;
3385
+ if (!roster || roster.length < 2) return effectiveDeviceKey;
3386
+ const attachedByKey = this.perDeviceAttachedCounts();
3387
+ const devices = roster.map((d) => ({
3388
+ deviceKey: d.deviceKey,
3389
+ weight: d.weight,
3390
+ maxSessions: d.maxSessions,
3391
+ attachedCameras: attachedByKey.get(d.deviceKey) ?? 0
3392
+ }));
3393
+ const def = tryGetStepDefinition(step.addonId);
3394
+ const canRun = (format) => def === null || def.models.some((m) => m.formats[format]);
3395
+ const result = resolveStepDevice({
3396
+ effectiveDeviceKey,
3397
+ devices,
3398
+ ...step.jumpDeviceKey !== void 0 ? { manualJumpDeviceKey: step.jumpDeviceKey } : {},
3399
+ canRun
3400
+ });
3401
+ if (result.kind === "no-candidate") this.warnStepHasNoRunnableDevice(deviceId, step.addonId, effectiveDeviceKey);
3402
+ return result.deviceKey;
3403
+ }
3404
+ /** Live attached-camera count per inference `deviceKey` on this runner. */
3405
+ perDeviceAttachedCounts() {
3406
+ const counts = /* @__PURE__ */ new Map();
3407
+ for (const cam of this.attached.values()) {
3408
+ const key = cam.config.deviceKey;
3409
+ if (key === void 0) continue;
3410
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3411
+ }
3412
+ return counts;
3413
+ }
3414
+ /** `${deviceId}:${addonId}` pairs already warned about a no-runnable-device step. */
3415
+ warnedNoJumpDevice = /* @__PURE__ */ new Set();
3416
+ warnStepHasNoRunnableDevice(deviceId, addonId, effectiveDeviceKey) {
3417
+ const key = `${deviceId}:${addonId}`;
3418
+ if (this.warnedNoJumpDevice.has(key)) return;
3419
+ this.warnedNoJumpDevice.add(key);
3420
+ this.ctx?.logger.warn("enrichment step has no runnable inference device on this node — producing NO results for it", {
3421
+ tags: { deviceId },
3422
+ meta: {
3423
+ step: addonId,
3424
+ electedDeviceKey: effectiveDeviceKey,
3425
+ hint: "the elected device format has no build of this step, and no enabled same-node device can run it — enable a compatible accelerator or add a jumpDeviceKey"
3426
+ }
3427
+ });
3428
+ }
3429
+ /**
3251
3430
  * Real `resolveChildSteps` dep: `null` when the camera isn't attached
3252
3431
  * (matches the cap doc — "camera not attached" is a distinct case from
3253
3432
  * "attached but no matching child steps", which returns an empty array
@@ -3269,16 +3448,12 @@ var PipelineRunnerAddon = class extends BaseAddon {
3269
3448
  async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
3270
3449
  const native = await this.getNativeCrop({
3271
3450
  handle,
3272
- bbox: paddedBbox
3451
+ bbox: paddedBbox,
3452
+ encodeJpeg: true
3273
3453
  });
3274
- if (!native) return null;
3275
- const sharp = (await import("sharp")).default;
3454
+ if (!native?.jpeg) return null;
3276
3455
  return {
3277
- jpeg: await sharp(Buffer.from(native.bytes), { raw: {
3278
- width: native.width,
3279
- height: native.height,
3280
- channels: 3
3281
- } }).jpeg({ quality: 90 }).toBuffer(),
3456
+ jpeg: Buffer.from(native.jpeg, "base64"),
3282
3457
  width: native.width,
3283
3458
  height: native.height,
3284
3459
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
@@ -3338,10 +3513,10 @@ var PipelineRunnerAddon = class extends BaseAddon {
3338
3513
  })();
3339
3514
  }
3340
3515
  /** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
3341
- async runDetailPipeline(steps, imageJpeg, deviceId) {
3516
+ async runDetailPipeline(steps, imageJpeg, deviceId, stepDeviceKey) {
3342
3517
  const api = this.ctxIfReady?.api;
3343
3518
  if (!api) return null;
3344
- const detailDeviceKey = this.attached.get(deviceId)?.config?.deviceKey;
3519
+ const detailDeviceKey = stepDeviceKey ?? this.attached.get(deviceId)?.config?.deviceKey;
3345
3520
  try {
3346
3521
  return await api.pipelineExecutor.runPipeline.mutate({
3347
3522
  steps: [...steps],
@@ -3418,6 +3593,8 @@ var PipelineRunnerAddon = class extends BaseAddon {
3418
3593
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
3419
3594
  this.thumbSampler?.forget(deviceId);
3420
3595
  this.packageGovernor.forget(deviceId);
3596
+ const warnPrefix = `${deviceId}:`;
3597
+ for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
3421
3598
  this.attached.delete(deviceId);
3422
3599
  this.lastMotionAt.delete(deviceId);
3423
3600
  this.lastEmittedCameraMetrics.delete(deviceId);
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("../chunk-D6vf50IK.js");
2
- const require_dist = require("../dist-E0K373T1.js");
2
+ const require_dist = require("../dist-CVjqvU8G.js");
3
3
  const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
4
4
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
5
5
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { F as recordingExportCapability, H as errMsg, I as resolveScrubThumbnailGeometry, J as hydrateSchema, K as DeviceType, P as recordingCapability, R as storageEvictableCapability, U as BaseAddon, Y as isEvent, c as EVENT_PAD_MS, dt as string, et as selectAssignedProfileSlots, f as OpsLogEntrySchema, j as nodePin, k as migrateConfigToBands, p as RecordingConfigSchema, pt as EventCategory, rt as array, u as ExportRecordSchema, ut as record } from "../dist-C3cDjL49.mjs";
1
+ import { F as recordingExportCapability, H as errMsg, I as resolveScrubThumbnailGeometry, J as hydrateSchema, K as DeviceType, P as recordingCapability, R as storageEvictableCapability, U as BaseAddon, Y as isEvent, c as EVENT_PAD_MS, dt as string, et as selectAssignedProfileSlots, f as OpsLogEntrySchema, j as nodePin, k as migrateConfigToBands, p as RecordingConfigSchema, pt as EventCategory, rt as array, u as ExportRecordSchema, ut as record } from "../dist-ClyAFBZk.mjs";
2
2
  import { n as computeSheetGeometry, r as resolveHubHostname, t as allTilePlacements } from "../sheet-geometry-DcpuGTCm.mjs";
3
3
  import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
4
4
  import { spawn } from "node:child_process";
@@ -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-PP4jKHHJ.js");
3
+ const require_worker_protocol = require("../worker-protocol-DextwlTX.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);
@@ -597,6 +597,44 @@ function retainedFrameBytes(width, height, rgb24) {
597
597
  function rgbCropSourceOffset(top, left, stride) {
598
598
  return top * stride + left * 3;
599
599
  }
600
+ /**
601
+ * Resolve the SOURCE the RGB native-crop scaler reads.
602
+ *
603
+ * The fast path is a zero-copy view: offset the packed RGB24 plane to the crop
604
+ * origin and let the scaler stride over it ({@link rgbCropSourceOffset}). That
605
+ * view is safe ONLY while it still holds `stride * height + slack` bytes — the
606
+ * conservative span libswscale reads (full rows + a SIMD overshoot on the last
607
+ * row). For a ROI whose bottom edge sits at (or near) the frame bottom AND whose
608
+ * left offset is > 0, the tail of the plane is `left*3` bytes short of that span,
609
+ * so the scaler reads past the allocation and THROWS — the crop is reported as a
610
+ * native miss. A FULL-frame crop (left=0, full height) is always exactly sized,
611
+ * which is why full-frame keyFrames landed while small offset ROIs missed
612
+ * (the subject-ROI-vs-full-frame asymmetry).
613
+ *
614
+ * When the zero-copy view is too short, repack the ROI into a tightly-packed
615
+ * buffer (stride = `width*3`) with the SIMD tail slack — identical pixels, but
616
+ * guaranteed in-bounds for every ROI position. Pure + exported so the bounds
617
+ * math is unit-testable without node-av.
618
+ */
619
+ function resolveRgbCropSource(plane, planeStride, region) {
620
+ const offset = rgbCropSourceOffset(region.top, region.left, planeStride);
621
+ const rowBytes = region.width * 3;
622
+ const requiredSpan = planeStride * region.height + 64;
623
+ if (plane.length - offset >= requiredSpan) return {
624
+ src: plane.subarray(offset),
625
+ stride: planeStride
626
+ };
627
+ const tightStride = rowBytes;
628
+ const packed = new Uint8Array(tightStride * region.height + 64);
629
+ for (let row = 0; row < region.height; row++) {
630
+ const srcRowStart = offset + row * planeStride;
631
+ packed.set(plane.subarray(srcRowStart, srcRowStart + rowBytes), row * tightStride);
632
+ }
633
+ return {
634
+ src: packed,
635
+ stride: tightStride
636
+ };
637
+ }
600
638
  //#endregion
601
639
  //#region src/session-decode/decode-worker-child.ts
602
640
  /**
@@ -1201,7 +1239,8 @@ var DecodeWorkerChild = class {
1201
1239
  this.nativeCropMisses++;
1202
1240
  this.send({
1203
1241
  kind: "nativeCropMiss",
1204
- requestId
1242
+ requestId,
1243
+ reason: `frame ${frameId} not retained (lease/ring evicted before crop)`
1205
1244
  });
1206
1245
  return;
1207
1246
  }
@@ -1218,10 +1257,12 @@ var DecodeWorkerChild = class {
1218
1257
  });
1219
1258
  } catch (err) {
1220
1259
  this.nativeCropMisses++;
1221
- this.emitStderr(`decode-worker-child: native crop failed (requestId ${requestId}) — ${errMessage(err)}\n`);
1260
+ const reason = errMessage(err);
1261
+ this.emitStderr(`decode-worker-child: native crop failed (requestId ${requestId}) — ${reason}\n`);
1222
1262
  this.send({
1223
1263
  kind: "nativeCropMiss",
1224
- requestId
1264
+ requestId,
1265
+ reason: `crop failed — ${reason}`
1225
1266
  });
1226
1267
  }
1227
1268
  }
@@ -1389,13 +1430,13 @@ var DecodeWorkerChild = class {
1389
1430
  const { region, target } = resolved;
1390
1431
  const plane = frame.data?.[0];
1391
1432
  if (!plane) throw new Error("decode-worker-child: retained RGB frame missing packed plane data");
1392
- const srcStride = frame.linesize[0] ?? 0;
1393
- const srcSlice = plane.subarray(rgbCropSourceOffset(region.top, region.left, srcStride));
1433
+ const { src: srcRgb, stride: srcSliceStride } = resolveRgbCropSource(plane, frame.linesize[0] ?? 0, region);
1434
+ const srcSlice = Buffer.from(srcRgb.buffer, srcRgb.byteOffset, srcRgb.byteLength);
1394
1435
  const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height, C.AV_PIX_FMT_RGB24);
1395
1436
  const tightStride = target.width * 3;
1396
1437
  const dstStride = tightStride + 63 & -64;
1397
1438
  const dstBuffer = Buffer.allocUnsafe(dstStride * target.height + 64);
1398
- const scaledHeight = scaler.scaleSync([srcSlice], [srcStride], 0, region.height, [dstBuffer], [dstStride]);
1439
+ const scaledHeight = scaler.scaleSync([srcSlice], [srcSliceStride], 0, region.height, [dstBuffer], [dstStride]);
1399
1440
  if (scaledHeight < 0) throw new Error(`native rgb crop sws_scale failed: ${scaledHeight}`);
1400
1441
  if (scaledHeight !== target.height) throw new Error(`native rgb crop scaler produced ${scaledHeight} of ${target.height} rows`);
1401
1442
  if (dstStride === tightStride) return dstBuffer.subarray(0, tightStride * target.height);
@@ -1857,6 +1898,13 @@ var DecodeWorkerChild = class {
1857
1898
  emitStderr(line) {
1858
1899
  const tag = this.deviceId === null ? "[dev:?]" : `[dev:${this.deviceId}]`;
1859
1900
  process.stderr.write(`${tag} ${line}`);
1901
+ try {
1902
+ this.send({
1903
+ kind: "log",
1904
+ level: require_worker_protocol.logLevelForLine(line),
1905
+ message: `${tag} ${line.trimEnd()}`
1906
+ });
1907
+ } catch {}
1860
1908
  }
1861
1909
  sendError(message, frameId) {
1862
1910
  this.emitStderr(`${message}\n`);
@@ -1,4 +1,4 @@
1
- import { n as isWorkerRequest } from "../worker-protocol-CyVJTZEO.mjs";
1
+ import { n as isWorkerRequest, r as logLevelForLine } from "../worker-protocol-D7RzZIla.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";
@@ -593,6 +593,44 @@ function retainedFrameBytes(width, height, rgb24) {
593
593
  function rgbCropSourceOffset(top, left, stride) {
594
594
  return top * stride + left * 3;
595
595
  }
596
+ /**
597
+ * Resolve the SOURCE the RGB native-crop scaler reads.
598
+ *
599
+ * The fast path is a zero-copy view: offset the packed RGB24 plane to the crop
600
+ * origin and let the scaler stride over it ({@link rgbCropSourceOffset}). That
601
+ * view is safe ONLY while it still holds `stride * height + slack` bytes — the
602
+ * conservative span libswscale reads (full rows + a SIMD overshoot on the last
603
+ * row). For a ROI whose bottom edge sits at (or near) the frame bottom AND whose
604
+ * left offset is > 0, the tail of the plane is `left*3` bytes short of that span,
605
+ * so the scaler reads past the allocation and THROWS — the crop is reported as a
606
+ * native miss. A FULL-frame crop (left=0, full height) is always exactly sized,
607
+ * which is why full-frame keyFrames landed while small offset ROIs missed
608
+ * (the subject-ROI-vs-full-frame asymmetry).
609
+ *
610
+ * When the zero-copy view is too short, repack the ROI into a tightly-packed
611
+ * buffer (stride = `width*3`) with the SIMD tail slack — identical pixels, but
612
+ * guaranteed in-bounds for every ROI position. Pure + exported so the bounds
613
+ * math is unit-testable without node-av.
614
+ */
615
+ function resolveRgbCropSource(plane, planeStride, region) {
616
+ const offset = rgbCropSourceOffset(region.top, region.left, planeStride);
617
+ const rowBytes = region.width * 3;
618
+ const requiredSpan = planeStride * region.height + 64;
619
+ if (plane.length - offset >= requiredSpan) return {
620
+ src: plane.subarray(offset),
621
+ stride: planeStride
622
+ };
623
+ const tightStride = rowBytes;
624
+ const packed = new Uint8Array(tightStride * region.height + 64);
625
+ for (let row = 0; row < region.height; row++) {
626
+ const srcRowStart = offset + row * planeStride;
627
+ packed.set(plane.subarray(srcRowStart, srcRowStart + rowBytes), row * tightStride);
628
+ }
629
+ return {
630
+ src: packed,
631
+ stride: tightStride
632
+ };
633
+ }
596
634
  //#endregion
597
635
  //#region src/session-decode/decode-worker-child.ts
598
636
  /**
@@ -1197,7 +1235,8 @@ var DecodeWorkerChild = class {
1197
1235
  this.nativeCropMisses++;
1198
1236
  this.send({
1199
1237
  kind: "nativeCropMiss",
1200
- requestId
1238
+ requestId,
1239
+ reason: `frame ${frameId} not retained (lease/ring evicted before crop)`
1201
1240
  });
1202
1241
  return;
1203
1242
  }
@@ -1214,10 +1253,12 @@ var DecodeWorkerChild = class {
1214
1253
  });
1215
1254
  } catch (err) {
1216
1255
  this.nativeCropMisses++;
1217
- this.emitStderr(`decode-worker-child: native crop failed (requestId ${requestId}) — ${errMessage(err)}\n`);
1256
+ const reason = errMessage(err);
1257
+ this.emitStderr(`decode-worker-child: native crop failed (requestId ${requestId}) — ${reason}\n`);
1218
1258
  this.send({
1219
1259
  kind: "nativeCropMiss",
1220
- requestId
1260
+ requestId,
1261
+ reason: `crop failed — ${reason}`
1221
1262
  });
1222
1263
  }
1223
1264
  }
@@ -1385,13 +1426,13 @@ var DecodeWorkerChild = class {
1385
1426
  const { region, target } = resolved;
1386
1427
  const plane = frame.data?.[0];
1387
1428
  if (!plane) throw new Error("decode-worker-child: retained RGB frame missing packed plane data");
1388
- const srcStride = frame.linesize[0] ?? 0;
1389
- const srcSlice = plane.subarray(rgbCropSourceOffset(region.top, region.left, srcStride));
1429
+ const { src: srcRgb, stride: srcSliceStride } = resolveRgbCropSource(plane, frame.linesize[0] ?? 0, region);
1430
+ const srcSlice = Buffer.from(srcRgb.buffer, srcRgb.byteOffset, srcRgb.byteLength);
1390
1431
  const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height, C.AV_PIX_FMT_RGB24);
1391
1432
  const tightStride = target.width * 3;
1392
1433
  const dstStride = tightStride + 63 & -64;
1393
1434
  const dstBuffer = Buffer.allocUnsafe(dstStride * target.height + 64);
1394
- const scaledHeight = scaler.scaleSync([srcSlice], [srcStride], 0, region.height, [dstBuffer], [dstStride]);
1435
+ const scaledHeight = scaler.scaleSync([srcSlice], [srcSliceStride], 0, region.height, [dstBuffer], [dstStride]);
1395
1436
  if (scaledHeight < 0) throw new Error(`native rgb crop sws_scale failed: ${scaledHeight}`);
1396
1437
  if (scaledHeight !== target.height) throw new Error(`native rgb crop scaler produced ${scaledHeight} of ${target.height} rows`);
1397
1438
  if (dstStride === tightStride) return dstBuffer.subarray(0, tightStride * target.height);
@@ -1853,6 +1894,13 @@ var DecodeWorkerChild = class {
1853
1894
  emitStderr(line) {
1854
1895
  const tag = this.deviceId === null ? "[dev:?]" : `[dev:${this.deviceId}]`;
1855
1896
  process.stderr.write(`${tag} ${line}`);
1897
+ try {
1898
+ this.send({
1899
+ kind: "log",
1900
+ level: logLevelForLine(line),
1901
+ message: `${tag} ${line.trimEnd()}`
1902
+ });
1903
+ } catch {}
1856
1904
  }
1857
1905
  sendError(message, frameId) {
1858
1906
  this.emitStderr(`${message}\n`);
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-E0K373T1.js");
1
+ const require_dist = require("./dist-CVjqvU8G.js");
2
2
  //#region src/detection-pipeline/registry/model-catalogs.ts
3
3
  var HF_REPO = "camstack/camstack-models";
4
4
  var HF_SCRYPTED = "scrypted/plugin-models";
@@ -1,4 +1,4 @@
1
- import { E as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-C3cDjL49.mjs";
1
+ import { E as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-ClyAFBZk.mjs";
2
2
  //#region src/detection-pipeline/registry/model-catalogs.ts
3
3
  var HF_REPO = "camstack/camstack-models";
4
4
  var HF_SCRYPTED = "scrypted/plugin-models";
@@ -2,7 +2,7 @@ import { a as e, c as t, d as n, i as r, l as i, n as a, o, r as s, s as c, t as
2
2
  import { a as d, i as f, n as p, o as m, r as h, t as g } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
3
3
  import { n as _, r as v, t as y } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
4
4
  import { n as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
5
- import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-UGa9yj7_.mjs";
5
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQC_f4Yv.mjs";
6
6
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
7
7
  var C = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), w = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), T = (e) => {
8
8
  let t = w(e);
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.1",
6
+ version: "1.2.2",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.1",
21
+ version: "1.2.2",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.1",
36
+ version: "1.2.2",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",