@camstack/addon-pipeline 1.2.74 → 1.2.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/{addon-utils-CQs-AjTJ.js → addon-utils-a_36HH_N.js} +4 -4
  2. package/dist/audio-analyzer/index.js +5 -6
  3. package/dist/audio-analyzer/index.mjs +3 -3
  4. package/dist/detection-pipeline/index.js +9 -10
  5. package/dist/detection-pipeline/index.mjs +5 -5
  6. package/dist/{dist-CgJ8MRAy.js → dist-6ZJCV0jY.js} +1900 -27
  7. package/dist/{dist-CKY0efl3.mjs → dist-T_8jMw7d.mjs} +1865 -27
  8. package/dist/{event-loop-stall-monitor-Dx-zXcNg.js → event-loop-stall-monitor-Cjof8vA1.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-CIapFIez.mjs → event-loop-stall-monitor-XeGmL4xr.mjs} +1 -1
  10. package/dist/lazy-sharp-6oymT_yf.mjs +341 -0
  11. package/dist/lazy-sharp-DvwPafeP.js +360 -0
  12. package/dist/motion-wasm/index.js +3 -3
  13. package/dist/motion-wasm/index.mjs +3 -3
  14. package/dist/pipeline-runner/index.js +6 -7
  15. package/dist/pipeline-runner/index.mjs +4 -4
  16. package/dist/recorder/index.js +138 -32
  17. package/dist/recorder/index.mjs +137 -30
  18. package/dist/session-decode/decode-worker-child.js +311 -126
  19. package/dist/session-decode/decode-worker-child.mjs +311 -123
  20. package/dist/stream-broker/_stub.js +94 -94
  21. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DVJ4gKkk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-H2OTmgR6.mjs} +2 -2
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-RYTOS6B-.mjs +26 -0
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfsLsn2e.mjs +26 -0
  24. package/dist/stream-broker/{hostInit-5SFdfUyc.mjs → hostInit-4XDN_Nt0.mjs} +2 -2
  25. package/dist/stream-broker/index.js +12 -13
  26. package/dist/stream-broker/index.mjs +3 -3
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/dist/{worker-protocol-D44DCkcs.js → worker-protocol-B0cbTXyw.js} +1 -1
  29. package/dist/{worker-protocol-BzAQPUR-.mjs → worker-protocol-SqED7Y9i.mjs} +1 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-emK7D4bc.js +0 -35
  32. package/dist/lazy-sharp-B-mb8uWx.mjs +0 -20
  33. package/dist/lazy-sharp-CcYuXtsa.js +0 -25
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C_plRUtD.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CExO0piw.mjs +0 -26
  36. package/dist/{addon-utils-CZ2xo67g.mjs → addon-utils-A2S9D7pu.mjs} +2 -2
@@ -1,12 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_chunk = require("../chunk-emK7D4bc.js");
3
- const require_worker_protocol = require("../worker-protocol-D44DCkcs.js");
4
- const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
5
- let node_url = require("node:url");
6
- let node_fs = require("node:fs");
7
- node_fs = require_chunk.__toESM(node_fs);
8
- let node_path = require("node:path");
9
- node_path = require_chunk.__toESM(node_path);
2
+ const require_worker_protocol = require("../worker-protocol-B0cbTXyw.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-DvwPafeP.js");
10
4
  //#region src/session-decode/color-conversion.ts
11
5
  /**
12
6
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -879,6 +873,82 @@ var NativeTileStore = class {
879
873
  }
880
874
  };
881
875
  //#endregion
876
+ //#region src/session-decode/planar-crop-source.ts
877
+ /** How many planes each layout must present before the offsets mean anything. */
878
+ var PLANE_COUNT = {
879
+ yuv420p: 3,
880
+ nv12: 2
881
+ };
882
+ /**
883
+ * Cache tag for a native-crop scaler's SOURCE format. `yuv420p` is the empty
884
+ * string on purpose: it keeps the historical, unprefixed cache key byte-for-byte
885
+ * (`320x240->160x120`), so adding formats cannot silently move an existing
886
+ * session onto a different scaler.
887
+ */
888
+ function nativeCropSourceTag(format) {
889
+ switch (format) {
890
+ case "yuv420p": return "";
891
+ case "nv12": return "nv12:";
892
+ case "rgb24": return "rgb:";
893
+ }
894
+ }
895
+ /** A plane view starting at `offset`, or `null` when the plane is too short. */
896
+ function viewAt(plane, offset, requiredSpan) {
897
+ if (!plane) return null;
898
+ if (offset < 0 || plane.length - offset < requiredSpan) return null;
899
+ return plane.subarray(offset);
900
+ }
901
+ /**
902
+ * Resolve the plane views a crop of `region` reads, for `kind`.
903
+ *
904
+ * Returns `null` — never a partial or clamped view — when the frame does not
905
+ * present the planes the layout requires, or when the region would run past a
906
+ * plane's end. The caller counts that as a crop miss and says so ONCE; a short
907
+ * view would hand libswscale a buffer to read past, which is how this path
908
+ * previously produced heap corruption rather than an error.
909
+ *
910
+ * An odd origin is floored onto the chroma grid rather than rejected: the
911
+ * geometry helper already aligns to even, and flooring reads the chroma sample
912
+ * that CONTAINS the origin instead of inventing a half sample.
913
+ */
914
+ function resolvePlanarCropSource(kind, planes, strides, region) {
915
+ if (!planes || planes.length < PLANE_COUNT[kind]) return null;
916
+ const yStride = strides?.[0] ?? 0;
917
+ const cStride = strides?.[1] ?? 0;
918
+ if (yStride <= 0 || cStride <= 0) return null;
919
+ const top = Math.max(0, region.top);
920
+ const left = Math.max(0, region.left);
921
+ const chromaRow = Math.floor(top / 2);
922
+ const chromaHeight = Math.ceil(region.height / 2);
923
+ const y = viewAt(planes[0], top * yStride + left, yStride * region.height);
924
+ if (kind === "nv12") {
925
+ const uvOffset = chromaRow * cStride + Math.floor(left / 2) * 2;
926
+ const uv = viewAt(planes[1], uvOffset, cStride * chromaHeight);
927
+ if (!y || !uv) return null;
928
+ return {
929
+ planes: [y, uv],
930
+ strides: [yStride, cStride]
931
+ };
932
+ }
933
+ const vStride = strides?.[2] ?? cStride;
934
+ const chromaOffset = chromaRow * cStride + Math.floor(left / 2);
935
+ const u = viewAt(planes[1], chromaOffset, cStride * chromaHeight);
936
+ const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), vStride * chromaHeight);
937
+ if (!y || !u || !v) return null;
938
+ return {
939
+ planes: [
940
+ y,
941
+ u,
942
+ v
943
+ ],
944
+ strides: [
945
+ yStride,
946
+ cStride,
947
+ vStride
948
+ ]
949
+ };
950
+ }
951
+ //#endregion
882
952
  //#region src/session-decode/pinned-rgb-crop.ts
883
953
  /**
884
954
  * Pure decision/plumbing helpers for the `pinnedRgbCrop` native-crop color fix
@@ -934,12 +1004,13 @@ function leaseDownloadFilterKey(srcW, srcH, color) {
934
1004
  /**
935
1005
  * Cache key for the dedicated native-crop software scaler. YUV420P (flag OFF /
936
1006
  * degrade) returns the historical `${srcW}x${srcH}->${dstW}x${dstH}` key
937
- * byte-for-byte; the RGB24 (flag ON) scaler carries an `rgb:` prefix so an RGB
938
- * scaler and a YUV scaler of identical geometry never alias the same cache slot.
1007
+ * byte-for-byte; every other source layout carries its own prefix
1008
+ * ({@link nativeCropSourceTag}) so two scalers of identical geometry but
1009
+ * different SOURCE format never alias the same cache slot — a scaler built for
1010
+ * three planes handed two reads garbage, silently.
939
1011
  */
940
- function nativeCropScalerKey(srcW, srcH, dstW, dstH, rgb24) {
941
- const base = `${srcW}x${srcH}->${dstW}x${dstH}`;
942
- return rgb24 ? `rgb:${base}` : base;
1012
+ function nativeCropScalerKey(srcW, srcH, dstW, dstH, srcFormat) {
1013
+ return `${nativeCropSourceTag(srcFormat)}${srcW}x${srcH}->${dstW}x${dstH}`;
943
1014
  }
944
1015
  /**
945
1016
  * Approximate resident bytes of a retained software frame, for the lease
@@ -998,31 +1069,60 @@ function resolveRgbCropSource(plane, planeStride, region) {
998
1069
  };
999
1070
  }
1000
1071
  //#endregion
1001
- //#region src/session-decode/decode-worker-child.ts
1072
+ //#region src/session-decode/subject-tile-cut.ts
1073
+ function failureReason(err) {
1074
+ return err instanceof Error ? err.message : String(err);
1075
+ }
1002
1076
  /**
1003
- * Decode-worker CHILD entry point Epic C P1, Task 2.
1077
+ * Copy every subject's pixels out of `frame`. SYNCHRONOUS on purpose — see the
1078
+ * module docblock; adding an `await` here reopens the race this closes.
1004
1079
  *
1005
- * This file is the whole program run by a per-session `child_process.fork`
1006
- * that the pipeline-runner's session-decode coordinator (Task 3) spawns and
1007
- * tears down on session end, so the OS reclaims the VAAPI/GPU decode pool by
1008
- * process exit (Task 0 live-validated that reclaim-by-exit works — node-av
1009
- * does not release the hw_frames_ctx pool on `dispose()` alone). It decodes
1010
- * with node-av in-process and hands decoded frames to its parent as a
1011
- * request/response PULL over its own dedicated fork IPC channel. Frames
1012
- * NEVER touch `/dev/shm` no shm sink is ever constructed here.
1080
+ * Liveness is re-checked before EVERY read and the loop STOPS at the first
1081
+ * failed check: once a frame has been freed, the remaining subjects are lost
1082
+ * regardless, and there is nothing to gain from asking again per subject.
1083
+ * `onFrameLost` therefore fires at most once.
1084
+ */
1085
+ function extractSubjectTiles(frame, bboxes, ports) {
1086
+ const raws = [];
1087
+ for (const bbox of bboxes) {
1088
+ if (!ports.isLive(frame)) {
1089
+ ports.onFrameLost();
1090
+ return raws;
1091
+ }
1092
+ try {
1093
+ raws.push(ports.cutRaw(frame, bbox));
1094
+ } catch (err) {
1095
+ ports.onCutFailed(failureReason(err));
1096
+ }
1097
+ }
1098
+ return raws;
1099
+ }
1100
+ /**
1101
+ * Cut and encode one frame's subject tiles.
1102
+ *
1103
+ * Two phases, and the boundary between them is the whole point: extraction
1104
+ * (synchronous, touches the frame) then encoding (async, touches only copies).
1013
1105
  *
1014
- * SANCTIONED RAW IPC READ BEFORE "FIXING" THIS FILE
1015
- * CLAUDE.md rule #8 forbids `process.send`/`process.on('message')` for
1016
- * BUSINESS LOGIC that rule governs cross-ADDON traffic, which must travel
1017
- * over the UDS cap/event channel (`LocalChildRegistry`/`LocalChildClient`)
1018
- * instead of raw IPC. This file is NOT a second addon: it is a private
1019
- * worker directly forked by the pipeline-runner addon, exactly like the
1020
- * Python inference pool's own subprocess. The Epic C P1 plan's Global
1021
- * Constraints explicitly carve this out — "a directly-forked worker child
1022
- * talks to its parent over its own IPC channel (this is NOT a cap call and
1023
- * NOT `$process`)". Raw `process.on('message')` / `process.send` are the
1024
- * correct, sanctioned mechanism for this specific file only.
1106
+ * The hold is deliberately NOT ended at that boundary, tempting as it looks: the
1107
+ * tiles are not stored until the encodes finish, so releasing the raster early
1108
+ * would open a window in which a crop request for this frame can be served by
1109
+ * neither the lease nor a tile. That window is small and the RAM it would buy
1110
+ * back is one frame for a few milliseconds a bad trade against a miss, which
1111
+ * is the failure this whole tier exists to remove. The caller releases once,
1112
+ * after the tiles are in the store.
1025
1113
  */
1114
+ async function cutSubjectTiles(frame, bboxes, ports) {
1115
+ const raws = extractSubjectTiles(frame, bboxes, ports);
1116
+ const tiles = [];
1117
+ for (const raw of raws) try {
1118
+ tiles.push(await ports.encode(raw));
1119
+ } catch (err) {
1120
+ ports.onCutFailed(failureReason(err));
1121
+ }
1122
+ return tiles;
1123
+ }
1124
+ //#endregion
1125
+ //#region src/session-decode/decode-worker-child.ts
1026
1126
  /** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
1027
1127
  function toLeasedNavFrame(frame) {
1028
1128
  return {
@@ -1214,68 +1314,64 @@ function gpuScaleFilterForBackend(backend) {
1214
1314
  function pixelFilterName(format) {
1215
1315
  return format === "gray" ? "gray" : "rgb24";
1216
1316
  }
1217
- var _nav = null;
1218
- var _consts = null;
1219
1317
  /**
1220
- * Candidate file URLs for a node-av entry (`.` or `./constants`) in the
1221
- * module locations an AGENT lays node-av out in.
1318
+ * The crop layout a retained frame's `AV_PIX_FMT_*` maps to, or `null` when this
1319
+ * path cannot read it.
1222
1320
  *
1223
- * WHY: this worker runs as a plain `child_process.fork` (see `fork-decode-
1224
- * worker.ts`) which does NOT inherit the addon-runner's module resolution
1225
- * (NODE_PATH / the framework ESM hook). On the HUB the fork inherits that hook,
1226
- * so the bare `import('node-av')` resolves. On an AGENT it does not, so the
1227
- * bare import throws `Cannot find package 'node-av'` and we resolve node-av
1228
- * explicitly from the agent's known layout:
1229
- * • DOCKER agent — the framework overlay `CAMSTACK_FRAMEWORK_DIR`
1230
- * (`/data/framework/node_modules/node-av`).
1231
- * • ELECTRON agent — the app bundle beside `CAMSTACK_BUNDLED_ADDONS_DIR`
1232
- * (`<resources>/addons`): `<resources>/node_modules/node-av` (+ the
1233
- * `app.asar.unpacked` mirror).
1234
- * Both env vars point at real dirs only on the respective platform; a missing
1235
- * dir is skipped, and `[]` (dev / hub with the bare import already working)
1236
- * keeps the bare specifier the only path.
1321
+ * Only the formats the lease can actually hold are listed, and deliberately so:
1322
+ * a format nobody has seen must surface as a NAMED failure the next session can
1323
+ * act on, not as a guess that silently mis-reads chroma. `AV_PIX_FMT_NV12` is
1324
+ * here because a videotoolbox-era retained frame arrives as two planes the
1325
+ * shape that made every subject tile on dev:617 fail.
1237
1326
  */
1238
- function fallbackNodeAvUrls(subpath) {
1239
- const rel = subpath === "." ? "dist/index.js" : "dist/constants/index.js";
1240
- const dirs = [];
1241
- const frameworkDir = process.env["CAMSTACK_FRAMEWORK_DIR"];
1242
- if (frameworkDir !== void 0 && frameworkDir.length > 0) dirs.push(node_path.join(frameworkDir, "node_modules", "node-av"));
1243
- const bundledAddons = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
1244
- if (bundledAddons !== void 0 && bundledAddons.length > 0) {
1245
- const resourcesRoot = node_path.dirname(bundledAddons);
1246
- dirs.push(node_path.join(resourcesRoot, "node_modules", "node-av"));
1247
- dirs.push(node_path.join(resourcesRoot, "app.asar.unpacked", "node_modules", "node-av"));
1248
- }
1249
- const urls = [];
1250
- for (const dir of dirs) {
1251
- const entry = node_path.join(dir, rel);
1252
- if (node_fs.existsSync(entry)) urls.push((0, node_url.pathToFileURL)(entry).href);
1253
- }
1254
- return urls;
1327
+ function planarCropKindFor(format, C) {
1328
+ if (format === C.AV_PIX_FMT_YUV420P) return "yuv420p";
1329
+ if (format === C.AV_PIX_FMT_NV12) return "nv12";
1330
+ return null;
1331
+ }
1332
+ /** The `AV_PIX_FMT_*` a native-crop scaler is built with for a source layout. */
1333
+ function navPixelFormatFor(format, C) {
1334
+ switch (format) {
1335
+ case "yuv420p": return C.AV_PIX_FMT_YUV420P;
1336
+ case "nv12": return C.AV_PIX_FMT_NV12;
1337
+ case "rgb24": return C.AV_PIX_FMT_RGB24;
1338
+ }
1255
1339
  }
1340
+ var _nav = null;
1341
+ var _consts = null;
1256
1342
  /**
1257
1343
  * Import a node-av entry, falling back to the agent's explicit node-av path
1258
- * when the bare specifier can't be resolved (see {@link fallbackNodeAvUrls}).
1344
+ * when the bare specifier can't be resolved.
1345
+ *
1346
+ * WHY: this worker runs as a plain `child_process.fork` (see
1347
+ * `fork-decode-worker.ts`) which does NOT inherit the addon-runner's module
1348
+ * resolution (the framework ESM hook). On the HUB the fork inherits that hook,
1349
+ * so the bare `import('node-av')` resolves; on an AGENT it does not and the bare
1350
+ * import throws `Cannot find package 'node-av'`. The candidate layouts, and why
1351
+ * the resolver hook is not simply registered in the child instead, live in
1352
+ * `shared/host-external-import.ts`. `sharp` needs the same treatment and gets it
1353
+ * through `shared/lazy-sharp.ts` — one mechanism, two callers.
1354
+ *
1259
1355
  * Each candidate is tried in turn so a copy whose native binary fails to load
1260
1356
  * doesn't mask a working one. A dynamic `import(url)` is typed `any`, so the
1261
1357
  * caller's declared module type applies without a cast.
1262
1358
  */
1263
- async function importNodeAv(bareSpecifier, subpath) {
1359
+ async function importNodeAv(bareSpecifier) {
1264
1360
  try {
1265
1361
  return await import(bareSpecifier);
1266
1362
  } catch (bareErr) {
1267
- for (const url of fallbackNodeAvUrls(subpath)) try {
1363
+ for (const url of require_lazy_sharp.hostExternalEntryUrls(bareSpecifier)) try {
1268
1364
  return await import(url);
1269
1365
  } catch {}
1270
1366
  throw bareErr;
1271
1367
  }
1272
1368
  }
1273
1369
  async function getNodeAv() {
1274
- if (!_nav) _nav = await importNodeAv("node-av", ".");
1370
+ if (!_nav) _nav = await importNodeAv("node-av");
1275
1371
  return _nav;
1276
1372
  }
1277
1373
  async function getConstants() {
1278
- if (!_consts) _consts = await importNodeAv("node-av/constants", "./constants");
1374
+ if (!_consts) _consts = await importNodeAv("node-av/constants");
1279
1375
  return _consts;
1280
1376
  }
1281
1377
  function errMessage(err) {
@@ -1542,6 +1638,18 @@ var DecodeWorkerChild = class {
1542
1638
  /** One-shot, so a legitimately long session does not repeat the warning
1543
1639
  * every ten seconds. */
1544
1640
  warnedLarge = false;
1641
+ /**
1642
+ * One-shot for "there is no JPEG encoder here" — see {@link tileEncoder}. The
1643
+ * branch it guards runs once per subject per frame, which is what made the
1644
+ * un-guarded version a log flood rather than a diagnosis.
1645
+ */
1646
+ warnedTileEncoderUnavailable = false;
1647
+ /**
1648
+ * The last subject-tile-cut failure REASON logged — see
1649
+ * {@link reportTileCutFailure}. Compared, not counted: the count lives on the
1650
+ * metrics line where it can be read per device.
1651
+ */
1652
+ lastTileCutFailureReason = "";
1545
1653
  lastDeliveredSnapshot = 0;
1546
1654
  metricsTimer = null;
1547
1655
  demuxer = null;
@@ -1827,41 +1935,60 @@ var DecodeWorkerChild = class {
1827
1935
  async handleCutTiles(frameId, bboxes) {
1828
1936
  try {
1829
1937
  if (!this.tileStore.enabled || bboxes.length === 0) return;
1938
+ const wanted = bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME);
1939
+ if (bboxes.length > wanted.length) this.emitStderr(`decode-worker-child: tile cut capped at ${NATIVE_TILE_MAX_PER_FRAME} of ${bboxes.length} subjects on frame ${frameId} — the rest fall back to the ≤640 tier\n`);
1940
+ const sharp = await this.tileEncoder();
1830
1941
  const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
1831
1942
  if (!frame) {
1832
1943
  this.tileCutFailures += 1;
1833
1944
  return;
1834
1945
  }
1946
+ if (sharp === null) {
1947
+ this.tileCutFailures += wanted.length;
1948
+ return;
1949
+ }
1950
+ if (!this.isRetainedFrameLive(frame)) {
1951
+ this.tileCutFailures += wanted.length;
1952
+ this.reportTileCutFailure(frameId, "the retained frame was released before the cut");
1953
+ return;
1954
+ }
1835
1955
  this.tileFrameWidth = frame.width;
1836
1956
  this.tileFrameHeight = frame.height;
1837
- const wanted = bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME);
1838
- if (bboxes.length > wanted.length) this.emitStderr(`decode-worker-child: tile cut capped at ${NATIVE_TILE_MAX_PER_FRAME} of ${bboxes.length} subjects on frame ${frameId} — the rest fall back to the ≤640 tier\n`);
1839
- const tiles = [];
1840
- for (const bbox of wanted) {
1841
- const { region } = resolveNativeTileRegion(frame.width, frame.height, bbox, NATIVE_TILE_PADDING_RATIO);
1842
- const resolved = {
1843
- region,
1844
- target: {
1845
- width: region.width,
1846
- height: region.height
1847
- }
1848
- };
1849
- try {
1850
- const raw = frame.isHwFrame() ? this.nativeHwCropToBuffer(frame, resolved) : this.nativeScaleCropToBuffer(frame, resolved);
1851
- const jpeg = await (await require_lazy_sharp.getSharp())(raw, { raw: {
1852
- width: region.width,
1853
- height: region.height,
1957
+ const tiles = await cutSubjectTiles(frame, wanted, {
1958
+ isLive: (f) => this.isRetainedFrameLive(f),
1959
+ cutRaw: (f, bbox) => {
1960
+ const { region } = resolveNativeTileRegion(f.width, f.height, bbox, NATIVE_TILE_PADDING_RATIO);
1961
+ const resolved = {
1962
+ region,
1963
+ target: {
1964
+ width: region.width,
1965
+ height: region.height
1966
+ }
1967
+ };
1968
+ return {
1969
+ region,
1970
+ raw: f.isHwFrame() ? this.nativeHwCropToBuffer(f, resolved) : this.nativeScaleCropToBuffer(f, resolved)
1971
+ };
1972
+ },
1973
+ encode: async (tile) => {
1974
+ const jpeg = await sharp(tile.raw, { raw: {
1975
+ width: tile.region.width,
1976
+ height: tile.region.height,
1854
1977
  channels: 3
1855
1978
  } }).jpeg({ quality: NATIVE_TILE_JPEG_QUALITY }).toBuffer();
1856
- tiles.push({
1857
- region,
1979
+ return {
1980
+ region: tile.region,
1858
1981
  bytes: new Uint8Array(jpeg)
1859
- });
1860
- } catch (err) {
1861
- this.tileCutFailures += 1;
1862
- this.emitStderr(`decode-worker-child: subject tile cut FAILED on frame ${frameId} ${errMessage(err)}\n`);
1982
+ };
1983
+ },
1984
+ onFrameLost: () => {
1985
+ this.reportTileCutFailure(frameId, "the retained frame was released mid-cut");
1986
+ },
1987
+ onCutFailed: (reason) => {
1988
+ this.reportTileCutFailure(frameId, reason);
1863
1989
  }
1864
- }
1990
+ });
1991
+ this.tileCutFailures += wanted.length - tiles.length;
1865
1992
  if (tiles.length > 0) {
1866
1993
  this.tilesCut += tiles.length;
1867
1994
  this.tileStore.put(frameId, tiles);
@@ -1871,6 +1998,73 @@ var DecodeWorkerChild = class {
1871
1998
  }
1872
1999
  }
1873
2000
  /**
2001
+ * Is this retained frame still backed by pixels?
2002
+ *
2003
+ * A node-av frame whose `free()` has run reports `AV_PIX_FMT_NONE` and no
2004
+ * planes — that is what dev:1443 logged (`pixel format -1 (planes=0,
2005
+ * hw=false)`) when an eviction landed mid-cut. A HW frame legitimately has no
2006
+ * `data` planes, so residency is judged there by format and dimensions.
2007
+ *
2008
+ * This is an ASSERTION of the invariant `subject-tile-cut.ts` establishes by
2009
+ * ORDERING, not the mechanism that enforces it: within one synchronous turn
2010
+ * nothing can free the frame, so the check can only fail for a frame that was
2011
+ * already dead when the cut arrived. It is kept, and consulted before every
2012
+ * read, so that an edit which reintroduces an await inside the loop degrades
2013
+ * to a counted miss instead of a read of recycled memory.
2014
+ */
2015
+ isRetainedFrameLive(frame) {
2016
+ const C = this.consts;
2017
+ if (!C) return false;
2018
+ if (frame.format === C.AV_PIX_FMT_NONE) return false;
2019
+ if (frame.width <= 0 || frame.height <= 0) return false;
2020
+ if (frame.isHwFrame()) return true;
2021
+ return (frame.data?.length ?? 0) > 0;
2022
+ }
2023
+ /**
2024
+ * Report a failed subject-tile cut — on TRANSITION, not per subject.
2025
+ *
2026
+ * Every failure is COUNTED (`tileCutFailures`, on the periodic metrics line
2027
+ * per device), so the volume is never hidden; what is suppressed is only the
2028
+ * repetition of a cause already stated. A tile cut fails once per subject per
2029
+ * frame, so a standing cause — a retained format this path cannot read, which
2030
+ * is precisely what dev:617 had — writes one line per subject per frame: 533
2031
+ * identical lines that said the same thing 533 times.
2032
+ *
2033
+ * Transition rather than one-shot, mirroring {@link logColorProvenance}: a
2034
+ * SECOND, different cause is worth a line, and a cause that comes back after
2035
+ * another one intervened is a real signal. The frame id is in the message but
2036
+ * deliberately not in the comparison key — otherwise every frame is a
2037
+ * "transition" and nothing is suppressed.
2038
+ */
2039
+ reportTileCutFailure(frameId, reason) {
2040
+ if (reason === this.lastTileCutFailureReason) return;
2041
+ this.lastTileCutFailureReason = reason;
2042
+ this.emitStderr(`decode-worker-child: subject tile cut FAILED on frame ${frameId} — ${reason} (further identical failures are counted in tileCutFailures, not logged)\n`);
2043
+ }
2044
+ /**
2045
+ * The JPEG encoder for subject tiles, or `null` when `sharp` cannot be
2046
+ * resolved in this process at all.
2047
+ *
2048
+ * `getSharp()` memoizes an unresolvable `sharp` (see `shared/lazy-sharp.ts`),
2049
+ * so this costs one module-loader round trip for the worker's whole life and a
2050
+ * rejected promise thereafter. The diagnostic is one-shot for the same reason
2051
+ * the memo is: the cause cannot change under a running worker, and the failing
2052
+ * branch is per subject per frame. The loader emits its own line naming every
2053
+ * path it tried; this one names the CONSEQUENCE, which is what an operator
2054
+ * looking at a camera with no detail crops needs to read.
2055
+ */
2056
+ async tileEncoder() {
2057
+ try {
2058
+ return await require_lazy_sharp.getSharp();
2059
+ } catch (err) {
2060
+ if (!this.warnedTileEncoderUnavailable) {
2061
+ this.warnedTileEncoderUnavailable = true;
2062
+ this.emitStderr(`decode-worker-child: subject tile tier DISABLED — no JPEG encoder: ${errMessage(err)}\n`);
2063
+ }
2064
+ return null;
2065
+ }
2066
+ }
2067
+ /**
1874
2068
  * Serve a native crop from a retained subject TILE — the rung that covers the
1875
2069
  * ordinary case, where the request arrives seconds after the frame is gone.
1876
2070
  *
@@ -2073,30 +2267,16 @@ var DecodeWorkerChild = class {
2073
2267
  if (frame.format === C.AV_PIX_FMT_RGB24) return this.nativeRgbCropToBuffer(nav, C, frame, resolved);
2074
2268
  this.logColorProvenance("sw-crop", this.resolveFrameColor(frame));
2075
2269
  const { region, target } = resolved;
2076
- const planes = frame.data;
2077
- const strides = frame.linesize;
2078
- const yPlane = planes?.[0];
2079
- const uPlane = planes?.[1];
2080
- const vPlane = planes?.[2];
2081
- if (!yPlane || !uPlane || !vPlane) throw new Error("decode-worker-child: retained frame missing planar YUV420P data");
2082
- const yStride = strides[0] ?? 0;
2083
- const cStride = strides[1] ?? 0;
2084
- const ySlice = yPlane.subarray(region.top * yStride + region.left);
2085
- const uSlice = uPlane.subarray(region.top / 2 * cStride + region.left / 2);
2086
- const vSlice = vPlane.subarray(region.top / 2 * cStride + region.left / 2);
2087
- const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height, C.AV_PIX_FMT_YUV420P);
2270
+ const kind = planarCropKindFor(frame.format, C);
2271
+ if (kind === null) throw new Error(`decode-worker-child: retained frame in unsupported pixel format ${frame.format} (planes=${frame.data?.length ?? 0}, hw=${frame.isHwFrame()}) — cannot crop`);
2272
+ const source = resolvePlanarCropSource(kind, frame.data, frame.linesize, region);
2273
+ if (!source) throw new Error(`decode-worker-child: retained ${kind} frame missing plane data for ${region.width}x${region.height}+${region.left}+${region.top} (planes=${frame.data?.length ?? 0})`);
2274
+ const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height, kind);
2088
2275
  const tightStride = target.width * 3;
2089
2276
  const dstStride = tightStride + 63 & -64;
2090
2277
  const dstBuffer = Buffer.allocUnsafe(dstStride * target.height + 64);
2091
- const scaledHeight = scaler.scaleSync([
2092
- ySlice,
2093
- uSlice,
2094
- vSlice
2095
- ], [
2096
- yStride,
2097
- cStride,
2098
- cStride
2099
- ], 0, region.height, [dstBuffer], [dstStride]);
2278
+ const srcPlanes = source.planes.map((plane) => Buffer.from(plane.buffer, plane.byteOffset, plane.byteLength));
2279
+ const scaledHeight = scaler.scaleSync(srcPlanes, [...source.strides], 0, region.height, [dstBuffer], [dstStride]);
2100
2280
  if (scaledHeight < 0) throw new Error(`native crop sws_scale failed: ${scaledHeight}`);
2101
2281
  if (scaledHeight !== target.height) throw new Error(`native crop scaler produced ${scaledHeight} of ${target.height} rows`);
2102
2282
  if (dstStride === tightStride) return dstBuffer.subarray(0, tightStride * target.height);
@@ -2117,7 +2297,7 @@ var DecodeWorkerChild = class {
2117
2297
  if (!plane) throw new Error("decode-worker-child: retained RGB frame missing packed plane data");
2118
2298
  const { src: srcRgb, stride: srcSliceStride } = resolveRgbCropSource(plane, frame.linesize[0] ?? 0, region);
2119
2299
  const srcSlice = Buffer.from(srcRgb.buffer, srcRgb.byteOffset, srcRgb.byteLength);
2120
- const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height, C.AV_PIX_FMT_RGB24);
2300
+ const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height, "rgb24");
2121
2301
  const tightStride = target.width * 3;
2122
2302
  const dstStride = tightStride + 63 & -64;
2123
2303
  const dstBuffer = Buffer.allocUnsafe(dstStride * target.height + 64);
@@ -2153,14 +2333,16 @@ var DecodeWorkerChild = class {
2153
2333
  }
2154
2334
  /**
2155
2335
  * Build-once / reuse the dedicated native-crop software scaler (rgb out).
2156
- * `srcFmt` parameterizes the SOURCE pixel format so the scaler matches the
2157
- * retained frame: `AV_PIX_FMT_YUV420P` (flag OFF / degrade — YUV→RGB convert)
2158
- * or `AV_PIX_FMT_RGB24` (`pinnedRgbCrop`a pure RGB→RGB spatial resize). The
2159
- * cache key folds the format in so an RGB and a YUV scaler of identical
2336
+ * `srcFormat` names the SOURCE layout so the scaler matches the retained
2337
+ * frame: `yuv420p` (flag OFF / degrade — YUV→RGB convert), `nv12` (a
2338
+ * two-plane retained framelibswscale reads it natively, so this is the same
2339
+ * single pass, not a conversion) or `rgb24` (`pinnedRgbCrop` a pure RGB→RGB
2340
+ * spatial resize). The cache key folds the format in so scalers of identical
2160
2341
  * geometry never alias one slot (see {@link nativeCropScalerKey}).
2161
2342
  */
2162
- ensureNativeCropScaler(nav, C, srcW, srcH, dstW, dstH, srcFmt) {
2163
- const key = nativeCropScalerKey(srcW, srcH, dstW, dstH, srcFmt === C.AV_PIX_FMT_RGB24);
2343
+ ensureNativeCropScaler(nav, C, srcW, srcH, dstW, dstH, srcFormat) {
2344
+ const srcFmt = navPixelFormatFor(srcFormat, C);
2345
+ const key = nativeCropScalerKey(srcW, srcH, dstW, dstH, srcFormat);
2164
2346
  const cached = this.nativeCropScaler;
2165
2347
  if (cached && key === this.nativeCropScalerKey) return cached;
2166
2348
  this.nativeCropScaler?.[Symbol.dispose]?.();
@@ -2612,6 +2794,9 @@ var DecodeWorkerChild = class {
2612
2794
  }
2613
2795
  };
2614
2796
  var worker = new DecodeWorkerChild();
2797
+ require_lazy_sharp.setSharpWarnSink((message) => {
2798
+ worker.emitStderr(`decode-worker-child: ${message}\n`, "warn");
2799
+ });
2615
2800
  process.on("message", (message) => {
2616
2801
  worker.handleMessage(message);
2617
2802
  });