@camstack/addon-pipeline 1.2.42 → 1.2.44
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/audio-analyzer/index.js +4 -4
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +164 -88
- package/dist/detection-pipeline/index.mjs +158 -82
- package/dist/{dist-BdDd3ql_.js → dist-BuFE5rOK.js} +1608 -36
- package/dist/{dist-COK_1Ot8.mjs → dist-DOu93i_g.mjs} +1525 -37
- package/dist/{event-loop-stall-monitor-CWOJtTNc.mjs → event-loop-stall-monitor-CShgQE6l.mjs} +40 -2
- package/dist/{event-loop-stall-monitor-DNFRgNef.js → event-loop-stall-monitor-L08yG6bB.js} +40 -2
- package/dist/{model-download-service-Cp9f4dk6-fsdDExML.js → model-download-service-D8B-4ktF-7-YxE9Wx.js} +2 -2
- package/dist/{model-download-service-Cp9f4dk6-CwUb5v3Y.mjs → model-download-service-D8B-4ktF-BB7oZL3y.mjs} +2 -2
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +496 -67
- package/dist/pipeline-runner/index.mjs +496 -67
- package/dist/recorder/index.js +194 -11
- package/dist/recorder/index.mjs +191 -8
- package/dist/session-decode/decode-worker-child.js +197 -24
- package/dist/session-decode/decode-worker-child.mjs +197 -24
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C8ovZN0g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DiXoZnou.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BRc-m6W-.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-DAjAxXHm.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_hakWAb.mjs} +1 -1
- package/dist/stream-broker/{hostInit-mEtjQWcr.mjs → hostInit-khRXMnjl.mjs} +2 -2
- package/dist/stream-broker/index.js +17331 -14101
- package/dist/stream-broker/index.mjs +17330 -14100
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-BOXlUhWO.mjs +244 -0
- package/dist/worker-protocol-VURr0nUh.js +279 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C0TIfr0G.js → MaskShapeCanvas-DI4BY7W2-DHZuYYjH.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-x0SkfHwv.js → MotionZonesSettings-NcxxQN8r-jFDOzPCD.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BoaAB4Ta.js → PrivacyMaskSettings-APgPLF7p-BPBXgIX8.js} +1 -1
- package/embed-dist/assets/{index-BgRdCCVy.js → index-4CtQAybX.js} +12 -12
- package/embed-dist/assets/index-DC63og2c.css +2 -0
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-SU_6F3Y7.mjs +0 -26
- package/dist/worker-protocol-D7RzZIla.mjs +0 -77
- package/dist/worker-protocol-DextwlTX.js +0 -94
- package/embed-dist/assets/index-CGdwTcwE.css +0 -2
|
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
6
|
+
const require_dist = require("../dist-BuFE5rOK.js");
|
|
7
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-L08yG6bB.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-VURr0nUh.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
10
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
11
11
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
@@ -83,9 +83,28 @@ function wrapChild(child) {
|
|
|
83
83
|
* Fork one decode-worker child. `source` is part of the coordinator's fork
|
|
84
84
|
* signature (it forwards the same source in the subsequent `start` message);
|
|
85
85
|
* the child entry itself is source-independent, so it is not consumed here.
|
|
86
|
+
*
|
|
87
|
+
* `leaseOverride` carries the operator's cluster-wide native-lease settings
|
|
88
|
+
* (`pipeline-orchestrator` globals, read by `native-lease-settings-source.ts`)
|
|
89
|
+
* into the child as environment variables — the child constructs its lease
|
|
90
|
+
* store and demand gate at module load, before the first `start` message, so
|
|
91
|
+
* the values have to be present at spawn. Only the knobs the operator actually
|
|
92
|
+
* SET are emitted, which is what keeps the pre-settings env override
|
|
93
|
+
* (`CAMSTACK_SESSION_NATIVE_LEASE_*`) working for the untouched ones; see
|
|
94
|
+
* `native-lease-config.ts` for the full precedence.
|
|
95
|
+
*
|
|
96
|
+
* The inherited environment is SPREAD, never replaced: this child resolves
|
|
97
|
+
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
98
|
+
* agents, so a replaced env would silently break decoding off the hub.
|
|
86
99
|
*/
|
|
87
|
-
function forkDecodeWorker(_source) {
|
|
88
|
-
return wrapChild((0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
|
|
100
|
+
function forkDecodeWorker(_source, leaseOverride = {}) {
|
|
101
|
+
return wrapChild((0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
|
|
102
|
+
serialization: "advanced",
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
...require_worker_protocol.nativeLeaseSettingEnv(leaseOverride)
|
|
106
|
+
}
|
|
107
|
+
}));
|
|
89
108
|
}
|
|
90
109
|
//#endregion
|
|
91
110
|
//#region src/session-decode/guarded-session-decode.ts
|
|
@@ -305,7 +324,7 @@ var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
|
305
324
|
* missing thumbnails/firstFrames concentrated on exactly those cameras).
|
|
306
325
|
* 16 frames ≈ 0.64 s @25 fps (1.6 s @10 fps). Only ACTIVE cameras hold frames,
|
|
307
326
|
* so the worst-case protected footprint stays well inside the byte budget for
|
|
308
|
-
* realistic concurrency (4 active cams ≈ 44 MB of
|
|
327
|
+
* realistic concurrency (4 active cams ≈ 44 MB of 160); the hard ceiling always
|
|
309
328
|
* overrides the floor regardless. Read ONCE from `CAMSTACK_SESSION_RETAIN_FLOOR`;
|
|
310
329
|
* `0` disables the floor (pure global FIFO — the pre-2026-07-19 behaviour).
|
|
311
330
|
*/
|
|
@@ -657,6 +676,19 @@ var SessionDecodeSession = class {
|
|
|
657
676
|
frameId
|
|
658
677
|
});
|
|
659
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Tell the worker `frameId` reached inference, so its native lease is worth
|
|
681
|
+
* keeping. Fire and forget, no reply. Ordering matters and is free here: the
|
|
682
|
+
* caller runs in the same turn that consumed the frame, and the pull that
|
|
683
|
+
* decides this frame's admission is issued after that turn returns.
|
|
684
|
+
*/
|
|
685
|
+
markInferred(frameId) {
|
|
686
|
+
if (this.exited) return;
|
|
687
|
+
this.trySend({
|
|
688
|
+
kind: "markInferred",
|
|
689
|
+
frameId
|
|
690
|
+
});
|
|
691
|
+
}
|
|
660
692
|
/** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
|
|
661
693
|
teardown() {
|
|
662
694
|
this.stopRequested = true;
|
|
@@ -877,6 +909,7 @@ function buildFrameImage(session, reply) {
|
|
|
877
909
|
},
|
|
878
910
|
nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
|
|
879
911
|
releaseNativeLease: () => session.releaseLease(reply.frameId),
|
|
912
|
+
markInferred: () => session.markInferred(reply.frameId),
|
|
880
913
|
close: () => {
|
|
881
914
|
closed = true;
|
|
882
915
|
}
|
|
@@ -1006,7 +1039,10 @@ function startSessionDecodePump(deps) {
|
|
|
1006
1039
|
const decoded = await toDecodedFrame(frame, format);
|
|
1007
1040
|
const image = frame.image;
|
|
1008
1041
|
const cropFn = image.nativeCrop;
|
|
1009
|
-
const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
|
|
1042
|
+
const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
|
|
1043
|
+
release: () => image.releaseNativeLease?.(),
|
|
1044
|
+
markInferred: () => image.markInferred?.()
|
|
1045
|
+
}) : void 0;
|
|
1010
1046
|
deps.onDecodedFrame(decoded, nativeCrop);
|
|
1011
1047
|
} catch (err) {
|
|
1012
1048
|
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
|
|
@@ -1788,7 +1824,121 @@ var CropConventionSource = class {
|
|
|
1788
1824
|
}
|
|
1789
1825
|
};
|
|
1790
1826
|
//#endregion
|
|
1827
|
+
//#region src/pipeline-runner/native-lease-settings-source.ts
|
|
1828
|
+
/**
|
|
1829
|
+
* Where the runner gets the cluster-wide native-frame LEASE knobs.
|
|
1830
|
+
*
|
|
1831
|
+
* ## Why a settings read and not the attach config
|
|
1832
|
+
*
|
|
1833
|
+
* Identical reasoning to `crop-convention-source.ts`, and worth restating
|
|
1834
|
+
* because the temptation is the same: `RunnerCameraConfig` is the obvious
|
|
1835
|
+
* orchestrator→runner channel and it is the wrong one. Eleven call sites build
|
|
1836
|
+
* one; a cluster-wide constant threaded through eleven builders eventually
|
|
1837
|
+
* reaches ten of them, and the cameras attached by the forgotten builder just
|
|
1838
|
+
* quietly run a different lease window. Instead the runner reads the OWNER
|
|
1839
|
+
* addon's global settings directly, hub-routed by `AddonCallGateway`, so an
|
|
1840
|
+
* agent runner transparently reads the hub-central store with no placement
|
|
1841
|
+
* branching.
|
|
1842
|
+
*
|
|
1843
|
+
* ## Deliberately NOT node-scoped
|
|
1844
|
+
*
|
|
1845
|
+
* The read passes no `nodeId`. With one, `addon-settings` would project that
|
|
1846
|
+
* node's scoped value and two nodes could hold different lease windows for the
|
|
1847
|
+
* same camera — so whether a stored crop is native pixels or the ≤640 fallback
|
|
1848
|
+
* would depend on where the balancer happened to place it, which is invisible
|
|
1849
|
+
* from the stored media.
|
|
1850
|
+
*
|
|
1851
|
+
* ## What it hands out
|
|
1852
|
+
*
|
|
1853
|
+
* A {@link NativeLeaseSettingsOverride} — only the knobs the operator SET. The
|
|
1854
|
+
* decode worker resolves the rest (env var, then the shipped default); see
|
|
1855
|
+
* `session-decode/native-lease-config.ts` for the precedence. Handing out a
|
|
1856
|
+
* fully-resolved triple here would erase the "operator set nothing" state and
|
|
1857
|
+
* with it the env-var escape hatch.
|
|
1858
|
+
*/
|
|
1859
|
+
/**
|
|
1860
|
+
* The addon whose GLOBAL settings own the knobs. `pipeline-orchestrator` is
|
|
1861
|
+
* hub-resident and neutral — the same owner as the detail-crop convention and
|
|
1862
|
+
* the per-node decoder-backend selector.
|
|
1863
|
+
*/
|
|
1864
|
+
var NATIVE_LEASE_OWNER_ADDON_ID = "pipeline-orchestrator";
|
|
1865
|
+
/**
|
|
1866
|
+
* Whether two overrides name the same knobs with the same values.
|
|
1867
|
+
*
|
|
1868
|
+
* **Every knob has to appear here.** This is the equality that decides whether
|
|
1869
|
+
* a re-read is ADOPTED at all: a knob missing from the comparison is read from
|
|
1870
|
+
* the hub, judged identical, and discarded, so the operator's change never
|
|
1871
|
+
* reaches a decode worker and nothing anywhere says so. `admission` shipped
|
|
1872
|
+
* missing from this list and did exactly that — the setting was stored, the
|
|
1873
|
+
* hub served it, the runner fetched it, and every worker kept spawning with the
|
|
1874
|
+
* old value.
|
|
1875
|
+
*/
|
|
1876
|
+
function sameOverride(a, b) {
|
|
1877
|
+
return a.ttlMs === b.ttlMs && a.budgetMb === b.budgetMb && a.activityMs === b.activityMs && a.admission === b.admission;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* TTL-cached read of the cluster native-lease knobs.
|
|
1881
|
+
*
|
|
1882
|
+
* {@link current} is synchronous because the fork that consumes it is;
|
|
1883
|
+
* {@link refresh} is what talks to the hub, and the runner awaits it on the
|
|
1884
|
+
* path that precedes a decode-worker spawn.
|
|
1885
|
+
*/
|
|
1886
|
+
var NativeLeaseSettingsSource = class {
|
|
1887
|
+
override = {};
|
|
1888
|
+
lastReadAtMs = Number.NEGATIVE_INFINITY;
|
|
1889
|
+
inFlight = null;
|
|
1890
|
+
logger;
|
|
1891
|
+
now;
|
|
1892
|
+
ttlMs;
|
|
1893
|
+
constructor(options) {
|
|
1894
|
+
this.logger = options.logger;
|
|
1895
|
+
this.now = options.now ?? (() => Date.now());
|
|
1896
|
+
this.ttlMs = options.ttlMs ?? 3e4;
|
|
1897
|
+
}
|
|
1898
|
+
/** The operator's overrides as last read — `{}` until a read lands. */
|
|
1899
|
+
current() {
|
|
1900
|
+
return this.override;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Re-read if the cached value has expired. Concurrent callers share one
|
|
1904
|
+
* in-flight read; a failure keeps the LAST KNOWN value rather than reverting
|
|
1905
|
+
* to `{}`, because a revert would silently hand the next-spawned workers a
|
|
1906
|
+
* different RAM profile than the ones already running.
|
|
1907
|
+
*/
|
|
1908
|
+
async refresh(api) {
|
|
1909
|
+
if (api === void 0) return this.override;
|
|
1910
|
+
if (this.now() - this.lastReadAtMs < this.ttlMs) return this.override;
|
|
1911
|
+
this.inFlight ??= this.read(api).finally(() => {
|
|
1912
|
+
this.inFlight = null;
|
|
1913
|
+
});
|
|
1914
|
+
await this.inFlight;
|
|
1915
|
+
return this.override;
|
|
1916
|
+
}
|
|
1917
|
+
async read(api) {
|
|
1918
|
+
try {
|
|
1919
|
+
const next = require_dist.pickNativeLeaseOverride(await api.addonSettings.getGlobalSettings.query({ addonId: NATIVE_LEASE_OWNER_ADDON_ID }));
|
|
1920
|
+
this.lastReadAtMs = this.now();
|
|
1921
|
+
if (sameOverride(next, this.override)) return;
|
|
1922
|
+
this.logger.info("native lease settings changed — applies to NEW decode sessions", { meta: {
|
|
1923
|
+
from: { ...this.override },
|
|
1924
|
+
to: { ...next }
|
|
1925
|
+
} });
|
|
1926
|
+
this.override = next;
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
this.logger.warn("native lease settings read failed — keeping the last known value", { meta: {
|
|
1929
|
+
owner: NATIVE_LEASE_OWNER_ADDON_ID,
|
|
1930
|
+
inUse: { ...this.override },
|
|
1931
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1932
|
+
} });
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
//#endregion
|
|
1791
1937
|
//#region src/pipeline-runner/detail-subtree.ts
|
|
1938
|
+
/** Did the native-lease fetch report a miss (rather than return pixels)? */
|
|
1939
|
+
function isNativeCropMiss(fetch) {
|
|
1940
|
+
return "miss" in fetch;
|
|
1941
|
+
}
|
|
1792
1942
|
/** Normalize a frame-space pixel rect to `[0,1]` by the given frame dims. */
|
|
1793
1943
|
function normalizeRect(rect, frameWidth, frameHeight) {
|
|
1794
1944
|
return {
|
|
@@ -1874,6 +2024,15 @@ function encodeEmbeddingBase64(embedding) {
|
|
|
1874
2024
|
function pickLabel(detection) {
|
|
1875
2025
|
return detection.labels.find((l) => !l.label.startsWith("embedding:"))?.label;
|
|
1876
2026
|
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Turn one executor detection into the cap's `DetailResult`, reprojecting its
|
|
2029
|
+
* crop-space bbox back onto the frame the crop came from.
|
|
2030
|
+
*
|
|
2031
|
+
* Exported because the STATELESS step path (`stateless-step.ts`) produces the
|
|
2032
|
+
* same shape from the same executor output and must not grow its own mapping —
|
|
2033
|
+
* a second reprojection is a second set of coordinates, and the two would agree
|
|
2034
|
+
* right up until one of them changed.
|
|
2035
|
+
*/
|
|
1877
2036
|
function toDetailResult(detection, stepId, crop) {
|
|
1878
2037
|
const label = pickLabel(detection);
|
|
1879
2038
|
return {
|
|
@@ -1893,6 +2052,13 @@ function toDetailResult(detection, stepId, crop) {
|
|
|
1893
2052
|
* traffic: before it existed the only way to feed stored pixels in was a
|
|
1894
2053
|
* pre-cut tile, so the caller decided the rectangle and every caller decided
|
|
1895
2054
|
* differently.
|
|
2055
|
+
*
|
|
2056
|
+
* Exported so the STATELESS step path can reach `deriveDetailCropRect` through
|
|
2057
|
+
* this function rather than importing it — `scripts/check-clip-crop-single-owner.ts`
|
|
2058
|
+
* permits exactly one importer, and that is the enforcement of
|
|
2059
|
+
* [D52](../../../../docs/decisions/adr-0052.md). A second caller of the
|
|
2060
|
+
* derivation is a second cutter no matter how similar its arithmetic looks
|
|
2061
|
+
* today.
|
|
1896
2062
|
*/
|
|
1897
2063
|
async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
1898
2064
|
const image = (await require_lazy_sharp.getSharp())(Buffer.from(frameJpegBase64, "base64"));
|
|
@@ -1900,7 +2066,7 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
|
1900
2066
|
const frameWidth = meta.width ?? 0;
|
|
1901
2067
|
const frameHeight = meta.height ?? 0;
|
|
1902
2068
|
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
1903
|
-
const rect = require_dist.deriveDetailCropRect(bbox, frameWidth, frameHeight, convention);
|
|
2069
|
+
const rect = require_dist.deriveDetailCropRect(bbox.kind === "frame-pixels" ? bbox.rect : denormalizeRect(bbox.rect, frameWidth, frameHeight), frameWidth, frameHeight, convention);
|
|
1904
2070
|
const left = Math.max(0, Math.min(Math.round(rect.x), frameWidth - 1));
|
|
1905
2071
|
const top = Math.max(0, Math.min(Math.round(rect.y), frameHeight - 1));
|
|
1906
2072
|
const width = Math.max(1, Math.min(Math.round(rect.w), frameWidth - left));
|
|
@@ -1924,37 +2090,82 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
|
1924
2090
|
};
|
|
1925
2091
|
}
|
|
1926
2092
|
async function resolveCrop(deps, input, convention) {
|
|
2093
|
+
let nativeMiss = null;
|
|
1927
2094
|
if (input.frameHandle) {
|
|
1928
2095
|
const normalized = normalizeRect(require_dist.deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
|
|
1929
2096
|
const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
|
|
1930
|
-
if (native)
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2097
|
+
if (native !== null) {
|
|
2098
|
+
if (!isNativeCropMiss(native)) return {
|
|
2099
|
+
crop: {
|
|
2100
|
+
...native,
|
|
2101
|
+
source: "native"
|
|
2102
|
+
},
|
|
2103
|
+
nativeMiss
|
|
2104
|
+
};
|
|
2105
|
+
nativeMiss = native;
|
|
2106
|
+
}
|
|
1934
2107
|
}
|
|
1935
|
-
if (input.frameJpeg) return
|
|
2108
|
+
if (input.frameJpeg) return {
|
|
2109
|
+
crop: await cutFromFullFrame(input.frameJpeg, {
|
|
2110
|
+
kind: "frame-pixels",
|
|
2111
|
+
rect: input.parent.bbox
|
|
2112
|
+
}, convention),
|
|
2113
|
+
nativeMiss
|
|
2114
|
+
};
|
|
1936
2115
|
if (input.cropJpeg) {
|
|
1937
2116
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
1938
2117
|
const meta = await (await require_lazy_sharp.getSharp())(jpeg).metadata();
|
|
1939
2118
|
const width = meta.width ?? 0;
|
|
1940
2119
|
const height = meta.height ?? 0;
|
|
1941
|
-
if (width <= 0 || height <= 0) return
|
|
2120
|
+
if (width <= 0 || height <= 0) return {
|
|
2121
|
+
crop: null,
|
|
2122
|
+
nativeMiss
|
|
2123
|
+
};
|
|
1942
2124
|
return {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2125
|
+
crop: {
|
|
2126
|
+
jpeg,
|
|
2127
|
+
width,
|
|
2128
|
+
height,
|
|
2129
|
+
frameSpace: input.parent.bbox,
|
|
2130
|
+
source: "fallback"
|
|
2131
|
+
},
|
|
2132
|
+
nativeMiss
|
|
1948
2133
|
};
|
|
1949
2134
|
}
|
|
1950
|
-
return
|
|
2135
|
+
return {
|
|
2136
|
+
crop: null,
|
|
2137
|
+
nativeMiss
|
|
2138
|
+
};
|
|
1951
2139
|
}
|
|
1952
2140
|
/**
|
|
1953
|
-
* Run the DETAIL subtree for a single tracked detection: resolve a crop
|
|
1954
|
-
*
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
2141
|
+
* Run the DETAIL subtree for a single tracked detection: resolve a crop, run
|
|
2142
|
+
* each matched top-level child step's chain against it, and map every result
|
|
2143
|
+
* bbox back to frame space. Returns `null` when the camera isn't attached, no
|
|
2144
|
+
* configured child matches `input.parent.className`, or no crop source
|
|
2145
|
+
* resolves.
|
|
2146
|
+
*
|
|
2147
|
+
* ## The three pixel sources, in the order they are tried
|
|
2148
|
+
*
|
|
2149
|
+
* The distinction between them is not the pixels — it is WHICH SIDE derives the
|
|
2150
|
+
* rectangle, and conflating that is how one vector index came to hold two
|
|
2151
|
+
* feature spaces ([D52](../../../../docs/decisions/adr-0052.md)):
|
|
2152
|
+
*
|
|
2153
|
+
* 1. `frameHandle` — the node-local native lease. Cheapest when it hits (no
|
|
2154
|
+
* payload crosses anything) and the only rung that yields a `nativeCropRef`,
|
|
2155
|
+
* so leaf children re-cut their ROI at native resolution. **The runner cuts.**
|
|
2156
|
+
* 2. `frameJpeg` — a FULL frame the caller still holds. **The runner cuts**, with
|
|
2157
|
+
* the same `deriveDetailCropRect` and the same convention, so the rectangle is
|
|
2158
|
+
* identical to what rung 1 would have asked for. Reached when the lease has
|
|
2159
|
+
* already expired — which, for the live detail plane, is the common case: the
|
|
2160
|
+
* dispatcher's tick plus a 500–900 ms inference routinely outlives a 1.2 s
|
|
2161
|
+
* lease, and before this rung was wired the step was simply abandoned.
|
|
2162
|
+
* 3. `cropJpeg` — an ALREADY-CUT tile, used verbatim, no padding and no squaring.
|
|
2163
|
+
* **The caller decided the rectangle**, so it is outside the single-derivation
|
|
2164
|
+
* guarantee and is only for the lease-miss retry.
|
|
2165
|
+
*
|
|
2166
|
+
* A rung-1 miss is remembered even when a lower rung succeeds, and travels into
|
|
2167
|
+
* the drop line below: "the native lease never hits any more" is precisely the
|
|
2168
|
+
* condition that goes invisible once something else covers for it.
|
|
1958
2169
|
*
|
|
1959
2170
|
* STRICT `steps` semantics: when the caller names an explicit `input.steps`
|
|
1960
2171
|
* set, each matched root's descendant chain is pruned to exactly those ids
|
|
@@ -1990,7 +2201,7 @@ async function runDetailSubtree(deps, input) {
|
|
|
1990
2201
|
}
|
|
1991
2202
|
const requested = input.steps;
|
|
1992
2203
|
const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
|
|
1993
|
-
const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
2204
|
+
const { crop, nativeMiss } = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
1994
2205
|
if (!crop) {
|
|
1995
2206
|
deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
|
|
1996
2207
|
tags: { deviceId: input.deviceId },
|
|
@@ -1998,7 +2209,9 @@ async function runDetailSubtree(deps, input) {
|
|
|
1998
2209
|
parentClass: input.parent.className,
|
|
1999
2210
|
hadFrameHandle: input.frameHandle !== void 0,
|
|
2000
2211
|
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
2001
|
-
hadCropJpeg: input.cropJpeg !== void 0
|
|
2212
|
+
hadCropJpeg: input.cropJpeg !== void 0,
|
|
2213
|
+
nativeMissReason: nativeMiss?.miss ?? null,
|
|
2214
|
+
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
|
|
2002
2215
|
}
|
|
2003
2216
|
});
|
|
2004
2217
|
return null;
|
|
@@ -2953,16 +3166,24 @@ var PipelineRunner = class {
|
|
|
2953
3166
|
handle
|
|
2954
3167
|
});
|
|
2955
3168
|
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Returns whether the frame was ADMITTED to the detection queue — i.e.
|
|
3171
|
+
* whether it will reach inference. The caller uses that to mark the frame's
|
|
3172
|
+
* native lease worth keeping (`NativeCropRequester.markInferred`): a native
|
|
3173
|
+
* crop can only ever name a frameId that rode an inference result, so a
|
|
3174
|
+
* frame rejected here is one nobody can ask for. A `false` here is the whole
|
|
3175
|
+
* saving; do not turn it back into `void` for tidiness.
|
|
3176
|
+
*/
|
|
2956
3177
|
enqueueDetectionFrame(deviceId, frame, handle) {
|
|
2957
3178
|
const state = this.cameras.get(deviceId);
|
|
2958
|
-
if (!state) return;
|
|
3179
|
+
if (!state) return false;
|
|
2959
3180
|
this.arrivalCounts.set(deviceId, (this.arrivalCounts.get(deviceId) ?? 0) + 1);
|
|
2960
|
-
if (state.phase !== "active") return;
|
|
3181
|
+
if (state.phase !== "active") return false;
|
|
2961
3182
|
const interval = this.governor.minIntervalMs(deviceId);
|
|
2962
3183
|
if (interval > 0) {
|
|
2963
3184
|
const last = this.lastAdmittedAt.get(deviceId) ?? 0;
|
|
2964
3185
|
const now = Date.now();
|
|
2965
|
-
if (now - last < interval) return;
|
|
3186
|
+
if (now - last < interval) return false;
|
|
2966
3187
|
this.lastAdmittedAt.set(deviceId, now);
|
|
2967
3188
|
}
|
|
2968
3189
|
frame._enqueuedAt = Date.now();
|
|
@@ -2970,6 +3191,7 @@ var PipelineRunner = class {
|
|
|
2970
3191
|
frame,
|
|
2971
3192
|
handle
|
|
2972
3193
|
});
|
|
3194
|
+
return true;
|
|
2973
3195
|
}
|
|
2974
3196
|
/**
|
|
2975
3197
|
* `handle` is optional (mirrors {@link DetectionQueueEntry.handle}) — the
|
|
@@ -2981,13 +3203,14 @@ var PipelineRunner = class {
|
|
|
2981
3203
|
*/
|
|
2982
3204
|
enqueueOccupancyFrame(deviceId, frame, handle) {
|
|
2983
3205
|
const state = this.cameras.get(deviceId);
|
|
2984
|
-
if (!state) return;
|
|
2985
|
-
if (state.phase !== "watching") return;
|
|
3206
|
+
if (!state) return false;
|
|
3207
|
+
if (state.phase !== "watching") return false;
|
|
2986
3208
|
frame._enqueuedAt = Date.now();
|
|
2987
3209
|
state.detectionQueue.enqueue({
|
|
2988
3210
|
frame,
|
|
2989
3211
|
handle
|
|
2990
3212
|
});
|
|
3213
|
+
return true;
|
|
2991
3214
|
}
|
|
2992
3215
|
/**
|
|
2993
3216
|
* Report a motion event for a camera. Drives the unified phase
|
|
@@ -3397,6 +3620,83 @@ var PipelineRunner = class {
|
|
|
3397
3620
|
}
|
|
3398
3621
|
};
|
|
3399
3622
|
//#endregion
|
|
3623
|
+
//#region src/pipeline-runner/stateless-step.ts
|
|
3624
|
+
function modelServability(step, modelId, format) {
|
|
3625
|
+
const entry = step.models.find((m) => m.id === modelId);
|
|
3626
|
+
if (entry === void 0) return { kind: "custom-model" };
|
|
3627
|
+
if (entry.formats[format] !== void 0) return { kind: "catalog-build" };
|
|
3628
|
+
return {
|
|
3629
|
+
kind: "no-build",
|
|
3630
|
+
availableFormats: Object.keys(entry.formats)
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
/** Build the refusal answer, log it, and return it — never one without the other. */
|
|
3634
|
+
function refuse(deps, input, reason, detail) {
|
|
3635
|
+
deps.logger.warn("runStatelessStep refused", {
|
|
3636
|
+
tags: { deviceId: input.sourceDeviceId },
|
|
3637
|
+
meta: {
|
|
3638
|
+
nodeId: deps.nodeId,
|
|
3639
|
+
stepId: input.stepId,
|
|
3640
|
+
modelId: input.modelId,
|
|
3641
|
+
reason,
|
|
3642
|
+
detail
|
|
3643
|
+
}
|
|
3644
|
+
});
|
|
3645
|
+
return {
|
|
3646
|
+
kind: "refused",
|
|
3647
|
+
nodeId: deps.nodeId,
|
|
3648
|
+
reason,
|
|
3649
|
+
detail
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
function errorMessage(err) {
|
|
3653
|
+
return err instanceof Error ? err.message : String(err);
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Run one step over one subject. See the module header for the two invariants.
|
|
3657
|
+
*
|
|
3658
|
+
* Never throws: every failure mode is a named, logged refusal, because the
|
|
3659
|
+
* caller is a bulk pass whose counters are the only signal an operator gets and
|
|
3660
|
+
* a thrown error there reads as "the whole pass broke".
|
|
3661
|
+
*/
|
|
3662
|
+
async function runStatelessStep(deps, input) {
|
|
3663
|
+
const definition = deps.lookupStep(input.stepId);
|
|
3664
|
+
if (definition === null) return refuse(deps, input, "unknown-step", `this node's step catalog has no "${input.stepId}" — version skew, or a custom step that is not installed here`);
|
|
3665
|
+
if (input.bbox.w <= 0 || input.bbox.h <= 0) return refuse(deps, input, "unreadable-frame", `degenerate normalized bbox ${JSON.stringify(input.bbox)} — nothing to crop`);
|
|
3666
|
+
const format = await deps.executionFormat();
|
|
3667
|
+
if (format === null) return refuse(deps, input, "model-not-servable", "this node could not report the engine format it would dispatch on, so the model pin cannot be verified — running anyway would risk a silent format substitution");
|
|
3668
|
+
const servability = modelServability(definition, input.modelId, format);
|
|
3669
|
+
if (servability.kind === "no-build") return refuse(deps, input, "model-not-servable", `model "${input.modelId}" has no ${format} build (catalog ships: ${servability.availableFormats.join(", ") || "none"}) — this node would have substituted the ${format} default`);
|
|
3670
|
+
let decodeError = null;
|
|
3671
|
+
const crop = await cutFromFullFrame(input.frameJpeg, {
|
|
3672
|
+
kind: "normalized",
|
|
3673
|
+
rect: input.bbox
|
|
3674
|
+
}, deps.resolveCropConvention()).catch((err) => {
|
|
3675
|
+
decodeError = errorMessage(err);
|
|
3676
|
+
return null;
|
|
3677
|
+
});
|
|
3678
|
+
if (crop === null) return refuse(deps, input, "unreadable-frame", decodeError ?? "the supplied frame decoded to no readable dimensions");
|
|
3679
|
+
const steps = [{
|
|
3680
|
+
addonId: input.stepId,
|
|
3681
|
+
modelId: input.modelId,
|
|
3682
|
+
enabled: true
|
|
3683
|
+
}];
|
|
3684
|
+
let result;
|
|
3685
|
+
try {
|
|
3686
|
+
result = await deps.runPipeline(steps, crop.jpeg, input.sourceDeviceId);
|
|
3687
|
+
} catch (err) {
|
|
3688
|
+
return refuse(deps, input, "execution-failed", errorMessage(err));
|
|
3689
|
+
}
|
|
3690
|
+
if (result === null) return refuse(deps, input, "execution-failed", "the executor returned no frame result for the crop");
|
|
3691
|
+
const details = result.detections.map((detection) => toDetailResult(detection, input.stepId, crop));
|
|
3692
|
+
return {
|
|
3693
|
+
kind: "ran",
|
|
3694
|
+
nodeId: deps.nodeId,
|
|
3695
|
+
modelId: input.modelId,
|
|
3696
|
+
details
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
//#endregion
|
|
3400
3700
|
//#region src/pipeline-runner/index.ts
|
|
3401
3701
|
var DEFAULT_CONFIG = {
|
|
3402
3702
|
maxQueueDepth: 30,
|
|
@@ -4673,13 +4973,38 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4673
4973
|
* resolve legitimately want the lower-RES honest frame).
|
|
4674
4974
|
*/
|
|
4675
4975
|
async getNativeCrop(input) {
|
|
4976
|
+
const resolved = await this.resolveNativeCropPixels(input);
|
|
4977
|
+
if (resolved.tier === "native" && resolved.pixels !== null) return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
|
|
4978
|
+
if (resolved.tier !== "ram-fullframe") return null;
|
|
4979
|
+
const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
|
|
4980
|
+
if (!crop) return null;
|
|
4981
|
+
return this.encodeNativeCropResult({
|
|
4982
|
+
bytes: crop.bytes,
|
|
4983
|
+
width: crop.width,
|
|
4984
|
+
height: crop.height
|
|
4985
|
+
}, resolved.tier, input.encodeJpeg);
|
|
4986
|
+
}
|
|
4987
|
+
/**
|
|
4988
|
+
* The native-lease half of {@link getNativeCrop}, extracted so the DETAIL path
|
|
4989
|
+
* can keep the miss REASON the cap's `null` return has no room for. Both
|
|
4990
|
+
* callers share this one decision — a second copy of the tier arithmetic is
|
|
4991
|
+
* how a quality tier starts differing by caller.
|
|
4992
|
+
*
|
|
4993
|
+
* Logging level follows the CONSEQUENCE, not the event: a native miss that the
|
|
4994
|
+
* RAM tier is allowed to serve (a full-frame resolve) cost nobody anything and
|
|
4995
|
+
* stays at debug; a miss the caller is handed as `null` is lost work and is a
|
|
4996
|
+
* warn, carrying the numbers that decide the fix.
|
|
4997
|
+
*/
|
|
4998
|
+
async resolveNativeCropPixels(input) {
|
|
4676
4999
|
const b = input.bbox;
|
|
4677
5000
|
const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
|
|
4678
5001
|
const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
|
|
4679
5002
|
let nativeResult = null;
|
|
4680
5003
|
let missReason = null;
|
|
5004
|
+
let handleAgeMs = null;
|
|
4681
5005
|
if (!isUnboundedFullFrame) {
|
|
4682
5006
|
const found = this.nativeCropRegistry.get(input.handle);
|
|
5007
|
+
handleAgeMs = found?.ageMs ?? null;
|
|
4683
5008
|
if (found) {
|
|
4684
5009
|
const result = await found.request(input.bbox, input.maxWidth);
|
|
4685
5010
|
if (result) nativeResult = {
|
|
@@ -4689,28 +5014,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4689
5014
|
};
|
|
4690
5015
|
else missReason = "worker-lease-gone";
|
|
4691
5016
|
} else missReason = "no-registry-entry";
|
|
4692
|
-
if (missReason !== null)
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
5017
|
+
if (missReason !== null) {
|
|
5018
|
+
const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
|
|
5019
|
+
const fields = {
|
|
5020
|
+
tags: { deviceId: found?.deviceId ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
|
|
5021
|
+
meta: {
|
|
5022
|
+
reason: missReason,
|
|
5023
|
+
servedTier: tierOnMiss,
|
|
5024
|
+
handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
|
|
5025
|
+
handleNodeId: input.handle.nodeId,
|
|
5026
|
+
handleAgeMs,
|
|
5027
|
+
registrySize: this.nativeCropRegistry.size,
|
|
5028
|
+
registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
|
|
5029
|
+
}
|
|
5030
|
+
};
|
|
5031
|
+
if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
|
|
5032
|
+
else this.ctx.logger.debug("native crop miss", fields);
|
|
5033
|
+
}
|
|
4703
5034
|
}
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
bytes: crop.bytes,
|
|
4711
|
-
width: crop.width,
|
|
4712
|
-
height: crop.height
|
|
4713
|
-
}, tier, input.encodeJpeg);
|
|
5035
|
+
return {
|
|
5036
|
+
pixels: nativeResult,
|
|
5037
|
+
tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
|
|
5038
|
+
missReason,
|
|
5039
|
+
handleAgeMs
|
|
5040
|
+
};
|
|
4714
5041
|
}
|
|
4715
5042
|
/**
|
|
4716
5043
|
* Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
|
|
@@ -4765,6 +5092,66 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4765
5092
|
}, input);
|
|
4766
5093
|
}
|
|
4767
5094
|
/**
|
|
5095
|
+
* Stateless single-step execution — no attach, no handle, no device
|
|
5096
|
+
* affinity. Delegates the decision table (catalog lookup, MODEL PIN
|
|
5097
|
+
* servability, crop, dispatch) to the deps-injected pure function in
|
|
5098
|
+
* {@link runStatelessStepImpl}; this method only wires the real deps.
|
|
5099
|
+
*
|
|
5100
|
+
* Note what is NOT wired, deliberately: no `resolveChildSteps` (the step
|
|
5101
|
+
* comes from the catalog, not from an attachment) and no `deviceKey` /
|
|
5102
|
+
* device jump (the jump roster is part of the attach config). The step runs
|
|
5103
|
+
* on the node's default engine, which is exactly the engine
|
|
5104
|
+
* `executionFormat` reports — so the servability check and the dispatch
|
|
5105
|
+
* cannot disagree about which format was verified.
|
|
5106
|
+
*/
|
|
5107
|
+
async runStatelessStep(input) {
|
|
5108
|
+
const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
|
|
5109
|
+
return runStatelessStep({
|
|
5110
|
+
nodeId: this.nodeId,
|
|
5111
|
+
lookupStep: (stepId) => tryGetStepDefinition(stepId),
|
|
5112
|
+
executionFormat: () => this.resolveNodeExecutionFormat(),
|
|
5113
|
+
resolveCropConvention: () => convention,
|
|
5114
|
+
runPipeline: (steps, imageJpeg, sourceDeviceId) => this.runStatelessPipeline(steps, imageJpeg, sourceDeviceId),
|
|
5115
|
+
logger: this.detailSubtreeLogger()
|
|
5116
|
+
}, input);
|
|
5117
|
+
}
|
|
5118
|
+
/**
|
|
5119
|
+
* The model format this node dispatches on when no `deviceKey` is given —
|
|
5120
|
+
* its selected engine's format. `null` on any failure, which the caller
|
|
5121
|
+
* turns into a refusal rather than a guess: a model pin that cannot be
|
|
5122
|
+
* checked is a substitution waiting to happen.
|
|
5123
|
+
*/
|
|
5124
|
+
async resolveNodeExecutionFormat() {
|
|
5125
|
+
const api = this.ctxIfReady?.api;
|
|
5126
|
+
if (!api) return null;
|
|
5127
|
+
try {
|
|
5128
|
+
return (await api.pipelineExecutor.getSelectedEngine.query()).format;
|
|
5129
|
+
} catch (err) {
|
|
5130
|
+
this.ctxIfReady?.logger.warn("runStatelessStep: could not read this node selected engine — refusing rather than guessing a format", { meta: { error: require_dist.errMsg(err) } });
|
|
5131
|
+
return null;
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
/**
|
|
5135
|
+
* Real `runPipeline` dep for the stateless path: node-local
|
|
5136
|
+
* `pipelineExecutor.runPipeline` on the `'full'` plane with NO `deviceKey`.
|
|
5137
|
+
*
|
|
5138
|
+
* `deviceId` is passed so the executor's own gate/substitution lines are
|
|
5139
|
+
* tagged with the source camera. It is a routing hint on other transports,
|
|
5140
|
+
* but not here: the executor is co-located in the `detection` group, so
|
|
5141
|
+
* `localProviderLink` answers in-process before any router sees it — the
|
|
5142
|
+
* call cannot leave the node this one was pinned to.
|
|
5143
|
+
*/
|
|
5144
|
+
async runStatelessPipeline(steps, imageJpeg, sourceDeviceId) {
|
|
5145
|
+
const api = this.ctxIfReady?.api;
|
|
5146
|
+
if (!api) return null;
|
|
5147
|
+
return api.pipelineExecutor.runPipeline.mutate({
|
|
5148
|
+
steps: [...steps],
|
|
5149
|
+
image: new Uint8Array(imageJpeg),
|
|
5150
|
+
deviceId: sourceDeviceId,
|
|
5151
|
+
plane: "full"
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
/**
|
|
4768
5155
|
* The cluster-wide detail-crop convention — read from the orchestrator's
|
|
4769
5156
|
* global settings, NOT from the attach config. See `crop-convention-source.ts`
|
|
4770
5157
|
* for why eleven attach builders make the attach config the wrong carrier.
|
|
@@ -4773,6 +5160,34 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4773
5160
|
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
4774
5161
|
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
4775
5162
|
} });
|
|
5163
|
+
/**
|
|
5164
|
+
* The cluster-wide native-frame LEASE knobs (TTL / RAM ceiling / demand
|
|
5165
|
+
* window) — read from the orchestrator's global settings for the same reason
|
|
5166
|
+
* as the crop convention above, and injected into each decode worker at fork
|
|
5167
|
+
* time. See `native-lease-settings-source.ts`.
|
|
5168
|
+
*/
|
|
5169
|
+
nativeLease = new NativeLeaseSettingsSource({ logger: {
|
|
5170
|
+
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
5171
|
+
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
5172
|
+
} });
|
|
5173
|
+
/**
|
|
5174
|
+
* Fork a decode worker carrying the operator's lease knobs. Used in place of
|
|
5175
|
+
* a bare `forkDecodeWorker` reference at every session-decode spawn site: the
|
|
5176
|
+
* knobs are read at spawn, so a changed setting reaches NEW sessions and
|
|
5177
|
+
* leaves running ones alone (a live session would have to be re-dialled).
|
|
5178
|
+
*/
|
|
5179
|
+
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current());
|
|
5180
|
+
/**
|
|
5181
|
+
* Refresh the lease knobs before a decode worker is spawned. Awaited on the
|
|
5182
|
+
* `acquire` leg of `startGuardedSessionDecode` (which always completes before
|
|
5183
|
+
* `startPump` forks), so the spawn sees a value at most one TTL stale rather
|
|
5184
|
+
* than whatever a background refresh happened to have landed.
|
|
5185
|
+
* `NativeLeaseSettingsSource.refresh` swallows its own failures, so this can
|
|
5186
|
+
* never break a dial.
|
|
5187
|
+
*/
|
|
5188
|
+
async refreshNativeLeaseSettings() {
|
|
5189
|
+
await this.nativeLease.refresh(this.ctxIfReady?.api);
|
|
5190
|
+
}
|
|
4776
5191
|
/** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
|
|
4777
5192
|
detailSubtreeLogger() {
|
|
4778
5193
|
return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
|
|
@@ -4855,18 +5270,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4855
5270
|
* lease (same path the `getNativeCrop` cap method above uses), encodes the
|
|
4856
5271
|
* raw RGB crop to JPEG, and denormalizes `paddedBbox` back to frame-space
|
|
4857
5272
|
* pixels for the `frameSpace` the caller maps result bboxes against.
|
|
5273
|
+
*
|
|
5274
|
+
* Returns the MISS REASON rather than a bare `null`. This is the only caller
|
|
5275
|
+
* that can carry it anywhere useful — the cap's return shape has no room for
|
|
5276
|
+
* it, and the detail plane's drop line is where an operator reads why an
|
|
5277
|
+
* enrichment dispatch produced nothing.
|
|
4858
5278
|
*/
|
|
4859
5279
|
async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
|
|
4860
|
-
const
|
|
5280
|
+
const resolved = await this.resolveNativeCropPixels({
|
|
4861
5281
|
handle,
|
|
4862
|
-
bbox: paddedBbox
|
|
4863
|
-
encodeJpeg: true
|
|
5282
|
+
bbox: paddedBbox
|
|
4864
5283
|
});
|
|
4865
|
-
if (
|
|
5284
|
+
if (resolved.pixels === null || resolved.tier !== "native") return {
|
|
5285
|
+
miss: resolved.missReason ?? "worker-lease-gone",
|
|
5286
|
+
handleAgeMs: resolved.handleAgeMs
|
|
5287
|
+
};
|
|
5288
|
+
const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
|
|
5289
|
+
if (!encoded?.jpeg) return {
|
|
5290
|
+
miss: "jpeg-encode-failed",
|
|
5291
|
+
handleAgeMs: resolved.handleAgeMs
|
|
5292
|
+
};
|
|
4866
5293
|
return {
|
|
4867
|
-
jpeg: Buffer.from(
|
|
4868
|
-
width:
|
|
4869
|
-
height:
|
|
5294
|
+
jpeg: Buffer.from(encoded.jpeg, "base64"),
|
|
5295
|
+
width: encoded.width,
|
|
5296
|
+
height: encoded.height,
|
|
4870
5297
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
4871
5298
|
};
|
|
4872
5299
|
}
|
|
@@ -5091,6 +5518,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5091
5518
|
* Kept as a method so the two call sites need no change.
|
|
5092
5519
|
*/
|
|
5093
5520
|
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
5521
|
+
await this.refreshNativeLeaseSettings();
|
|
5094
5522
|
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
5095
5523
|
deviceId,
|
|
5096
5524
|
streamId,
|
|
@@ -5132,8 +5560,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5132
5560
|
*/
|
|
5133
5561
|
enqueueSharedDetectionFrame(deviceId, frame, handle) {
|
|
5134
5562
|
const runner = this.runner;
|
|
5135
|
-
if (!runner) return;
|
|
5136
|
-
runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5563
|
+
if (!runner) return false;
|
|
5564
|
+
const admitted = runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5137
5565
|
const sink = this.sharedMotionSinks.get(deviceId);
|
|
5138
5566
|
if (sink) {
|
|
5139
5567
|
const now = this.sharedSinkNow();
|
|
@@ -5142,6 +5570,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5142
5570
|
runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
|
|
5143
5571
|
}
|
|
5144
5572
|
}
|
|
5573
|
+
return admitted;
|
|
5145
5574
|
}
|
|
5146
5575
|
/**
|
|
5147
5576
|
* Epic C P1 detection path for a flagged camera: decode `rgb` in a
|
|
@@ -5197,7 +5626,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5197
5626
|
codec: source.codec
|
|
5198
5627
|
} });
|
|
5199
5628
|
return startSessionDecodePump({
|
|
5200
|
-
fork:
|
|
5629
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5201
5630
|
logger: log,
|
|
5202
5631
|
source,
|
|
5203
5632
|
opts: {
|
|
@@ -5209,7 +5638,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5209
5638
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
5210
5639
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5211
5640
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5212
|
-
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
5641
|
+
if (this.enqueueSharedDetectionFrame(config.deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5213
5642
|
}
|
|
5214
5643
|
});
|
|
5215
5644
|
}
|
|
@@ -5242,7 +5671,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5242
5671
|
codec: source.codec
|
|
5243
5672
|
} });
|
|
5244
5673
|
return startSessionDecodePump({
|
|
5245
|
-
fork:
|
|
5674
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5246
5675
|
logger: log,
|
|
5247
5676
|
source,
|
|
5248
5677
|
opts: {
|
|
@@ -5714,7 +6143,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5714
6143
|
logger: log,
|
|
5715
6144
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
5716
6145
|
startPump: (source) => startSessionDecodePump({
|
|
5717
|
-
fork:
|
|
6146
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5718
6147
|
logger: log,
|
|
5719
6148
|
source,
|
|
5720
6149
|
opts: {
|
|
@@ -5727,7 +6156,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5727
6156
|
if (settled) return;
|
|
5728
6157
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5729
6158
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5730
|
-
runner.enqueueOccupancyFrame(deviceId, frame, handle);
|
|
6159
|
+
if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5731
6160
|
count++;
|
|
5732
6161
|
if (count >= frames) {
|
|
5733
6162
|
clearTimeout(safetyTimer);
|