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