@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.
- package/dist/{addon-utils-CQs-AjTJ.js → addon-utils-a_36HH_N.js} +4 -4
- package/dist/audio-analyzer/index.js +5 -6
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/detection-pipeline/index.js +9 -10
- package/dist/detection-pipeline/index.mjs +5 -5
- package/dist/{dist-CgJ8MRAy.js → dist-6ZJCV0jY.js} +1900 -27
- package/dist/{dist-CKY0efl3.mjs → dist-T_8jMw7d.mjs} +1865 -27
- package/dist/{event-loop-stall-monitor-Dx-zXcNg.js → event-loop-stall-monitor-Cjof8vA1.js} +1 -1
- package/dist/{event-loop-stall-monitor-CIapFIez.mjs → event-loop-stall-monitor-XeGmL4xr.mjs} +1 -1
- package/dist/lazy-sharp-6oymT_yf.mjs +341 -0
- package/dist/lazy-sharp-DvwPafeP.js +360 -0
- package/dist/motion-wasm/index.js +3 -3
- package/dist/motion-wasm/index.mjs +3 -3
- package/dist/pipeline-runner/index.js +6 -7
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/recorder/index.js +138 -32
- package/dist/recorder/index.mjs +137 -30
- package/dist/session-decode/decode-worker-child.js +311 -126
- package/dist/session-decode/decode-worker-child.mjs +311 -123
- package/dist/stream-broker/_stub.js +94 -94
- 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
- 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
- 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
- package/dist/stream-broker/{hostInit-5SFdfUyc.mjs → hostInit-4XDN_Nt0.mjs} +2 -2
- package/dist/stream-broker/index.js +12 -13
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-D44DCkcs.js → worker-protocol-B0cbTXyw.js} +1 -1
- package/dist/{worker-protocol-BzAQPUR-.mjs → worker-protocol-SqED7Y9i.mjs} +1 -1
- package/package.json +1 -1
- package/dist/chunk-emK7D4bc.js +0 -35
- package/dist/lazy-sharp-B-mb8uWx.mjs +0 -20
- package/dist/lazy-sharp-CcYuXtsa.js +0 -25
- 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
- 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
- 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
|
|
3
|
-
const
|
|
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;
|
|
938
|
-
*
|
|
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,
|
|
941
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
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
|
-
*
|
|
1221
|
-
*
|
|
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
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
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
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
|
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
|
|
1359
|
+
async function importNodeAv(bareSpecifier) {
|
|
1264
1360
|
try {
|
|
1265
1361
|
return await import(bareSpecifier);
|
|
1266
1362
|
} catch (bareErr) {
|
|
1267
|
-
for (const url of
|
|
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"
|
|
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
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1857
|
-
region,
|
|
1979
|
+
return {
|
|
1980
|
+
region: tile.region,
|
|
1858
1981
|
bytes: new Uint8Array(jpeg)
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
this.
|
|
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
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
const
|
|
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
|
|
2092
|
-
|
|
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,
|
|
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
|
-
* `
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2159
|
-
*
|
|
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 frame — libswscale 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,
|
|
2163
|
-
const
|
|
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
|
});
|