@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-
|
|
3
|
-
import { t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { Ct as literal, Dt as string, G as pickDetailCropConvention, J as pipelineRunnerCapability, K as pickNativeLeaseOverride, M as deriveDetailCropRect, St as lazy, Tt as object, bt as boolean, c as DEFAULT_DETAIL_CROP_CONVENTION, ct as createEvent, it as BaseAddon, j as defineCustomActions, k as customAction, kt as EventCategory, pt as nodePin, rt as errMsg, u as DEVICE_BACKEND_TO_FORMAT, vt as _enum, wt as number, yt as array } from "../dist-DOu93i_g.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-CShgQE6l.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-BOXlUhWO.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
6
6
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
@@ -76,9 +76,28 @@ function wrapChild(child) {
|
|
|
76
76
|
* Fork one decode-worker child. `source` is part of the coordinator's fork
|
|
77
77
|
* signature (it forwards the same source in the subsequent `start` message);
|
|
78
78
|
* the child entry itself is source-independent, so it is not consumed here.
|
|
79
|
+
*
|
|
80
|
+
* `leaseOverride` carries the operator's cluster-wide native-lease settings
|
|
81
|
+
* (`pipeline-orchestrator` globals, read by `native-lease-settings-source.ts`)
|
|
82
|
+
* into the child as environment variables — the child constructs its lease
|
|
83
|
+
* store and demand gate at module load, before the first `start` message, so
|
|
84
|
+
* the values have to be present at spawn. Only the knobs the operator actually
|
|
85
|
+
* SET are emitted, which is what keeps the pre-settings env override
|
|
86
|
+
* (`CAMSTACK_SESSION_NATIVE_LEASE_*`) working for the untouched ones; see
|
|
87
|
+
* `native-lease-config.ts` for the full precedence.
|
|
88
|
+
*
|
|
89
|
+
* The inherited environment is SPREAD, never replaced: this child resolves
|
|
90
|
+
* node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
|
|
91
|
+
* agents, so a replaced env would silently break decoding off the hub.
|
|
79
92
|
*/
|
|
80
|
-
function forkDecodeWorker(_source) {
|
|
81
|
-
return wrapChild(fork(CHILD_ENTRY_PATH, [], {
|
|
93
|
+
function forkDecodeWorker(_source, leaseOverride = {}) {
|
|
94
|
+
return wrapChild(fork(CHILD_ENTRY_PATH, [], {
|
|
95
|
+
serialization: "advanced",
|
|
96
|
+
env: {
|
|
97
|
+
...process.env,
|
|
98
|
+
...nativeLeaseSettingEnv(leaseOverride)
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
82
101
|
}
|
|
83
102
|
//#endregion
|
|
84
103
|
//#region src/session-decode/guarded-session-decode.ts
|
|
@@ -298,7 +317,7 @@ var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
|
298
317
|
* missing thumbnails/firstFrames concentrated on exactly those cameras).
|
|
299
318
|
* 16 frames ≈ 0.64 s @25 fps (1.6 s @10 fps). Only ACTIVE cameras hold frames,
|
|
300
319
|
* so the worst-case protected footprint stays well inside the byte budget for
|
|
301
|
-
* realistic concurrency (4 active cams ≈ 44 MB of
|
|
320
|
+
* realistic concurrency (4 active cams ≈ 44 MB of 160); the hard ceiling always
|
|
302
321
|
* overrides the floor regardless. Read ONCE from `CAMSTACK_SESSION_RETAIN_FLOOR`;
|
|
303
322
|
* `0` disables the floor (pure global FIFO — the pre-2026-07-19 behaviour).
|
|
304
323
|
*/
|
|
@@ -650,6 +669,19 @@ var SessionDecodeSession = class {
|
|
|
650
669
|
frameId
|
|
651
670
|
});
|
|
652
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Tell the worker `frameId` reached inference, so its native lease is worth
|
|
674
|
+
* keeping. Fire and forget, no reply. Ordering matters and is free here: the
|
|
675
|
+
* caller runs in the same turn that consumed the frame, and the pull that
|
|
676
|
+
* decides this frame's admission is issued after that turn returns.
|
|
677
|
+
*/
|
|
678
|
+
markInferred(frameId) {
|
|
679
|
+
if (this.exited) return;
|
|
680
|
+
this.trySend({
|
|
681
|
+
kind: "markInferred",
|
|
682
|
+
frameId
|
|
683
|
+
});
|
|
684
|
+
}
|
|
653
685
|
/** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
|
|
654
686
|
teardown() {
|
|
655
687
|
this.stopRequested = true;
|
|
@@ -870,6 +902,7 @@ function buildFrameImage(session, reply) {
|
|
|
870
902
|
},
|
|
871
903
|
nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
|
|
872
904
|
releaseNativeLease: () => session.releaseLease(reply.frameId),
|
|
905
|
+
markInferred: () => session.markInferred(reply.frameId),
|
|
873
906
|
close: () => {
|
|
874
907
|
closed = true;
|
|
875
908
|
}
|
|
@@ -999,7 +1032,10 @@ function startSessionDecodePump(deps) {
|
|
|
999
1032
|
const decoded = await toDecodedFrame(frame, format);
|
|
1000
1033
|
const image = frame.image;
|
|
1001
1034
|
const cropFn = image.nativeCrop;
|
|
1002
|
-
const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
|
|
1035
|
+
const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
|
|
1036
|
+
release: () => image.releaseNativeLease?.(),
|
|
1037
|
+
markInferred: () => image.markInferred?.()
|
|
1038
|
+
}) : void 0;
|
|
1003
1039
|
deps.onDecodedFrame(decoded, nativeCrop);
|
|
1004
1040
|
} catch (err) {
|
|
1005
1041
|
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
|
|
@@ -1780,7 +1816,121 @@ var CropConventionSource = class {
|
|
|
1780
1816
|
}
|
|
1781
1817
|
};
|
|
1782
1818
|
//#endregion
|
|
1819
|
+
//#region src/pipeline-runner/native-lease-settings-source.ts
|
|
1820
|
+
/**
|
|
1821
|
+
* Where the runner gets the cluster-wide native-frame LEASE knobs.
|
|
1822
|
+
*
|
|
1823
|
+
* ## Why a settings read and not the attach config
|
|
1824
|
+
*
|
|
1825
|
+
* Identical reasoning to `crop-convention-source.ts`, and worth restating
|
|
1826
|
+
* because the temptation is the same: `RunnerCameraConfig` is the obvious
|
|
1827
|
+
* orchestrator→runner channel and it is the wrong one. Eleven call sites build
|
|
1828
|
+
* one; a cluster-wide constant threaded through eleven builders eventually
|
|
1829
|
+
* reaches ten of them, and the cameras attached by the forgotten builder just
|
|
1830
|
+
* quietly run a different lease window. Instead the runner reads the OWNER
|
|
1831
|
+
* addon's global settings directly, hub-routed by `AddonCallGateway`, so an
|
|
1832
|
+
* agent runner transparently reads the hub-central store with no placement
|
|
1833
|
+
* branching.
|
|
1834
|
+
*
|
|
1835
|
+
* ## Deliberately NOT node-scoped
|
|
1836
|
+
*
|
|
1837
|
+
* The read passes no `nodeId`. With one, `addon-settings` would project that
|
|
1838
|
+
* node's scoped value and two nodes could hold different lease windows for the
|
|
1839
|
+
* same camera — so whether a stored crop is native pixels or the ≤640 fallback
|
|
1840
|
+
* would depend on where the balancer happened to place it, which is invisible
|
|
1841
|
+
* from the stored media.
|
|
1842
|
+
*
|
|
1843
|
+
* ## What it hands out
|
|
1844
|
+
*
|
|
1845
|
+
* A {@link NativeLeaseSettingsOverride} — only the knobs the operator SET. The
|
|
1846
|
+
* decode worker resolves the rest (env var, then the shipped default); see
|
|
1847
|
+
* `session-decode/native-lease-config.ts` for the precedence. Handing out a
|
|
1848
|
+
* fully-resolved triple here would erase the "operator set nothing" state and
|
|
1849
|
+
* with it the env-var escape hatch.
|
|
1850
|
+
*/
|
|
1851
|
+
/**
|
|
1852
|
+
* The addon whose GLOBAL settings own the knobs. `pipeline-orchestrator` is
|
|
1853
|
+
* hub-resident and neutral — the same owner as the detail-crop convention and
|
|
1854
|
+
* the per-node decoder-backend selector.
|
|
1855
|
+
*/
|
|
1856
|
+
var NATIVE_LEASE_OWNER_ADDON_ID = "pipeline-orchestrator";
|
|
1857
|
+
/**
|
|
1858
|
+
* Whether two overrides name the same knobs with the same values.
|
|
1859
|
+
*
|
|
1860
|
+
* **Every knob has to appear here.** This is the equality that decides whether
|
|
1861
|
+
* a re-read is ADOPTED at all: a knob missing from the comparison is read from
|
|
1862
|
+
* the hub, judged identical, and discarded, so the operator's change never
|
|
1863
|
+
* reaches a decode worker and nothing anywhere says so. `admission` shipped
|
|
1864
|
+
* missing from this list and did exactly that — the setting was stored, the
|
|
1865
|
+
* hub served it, the runner fetched it, and every worker kept spawning with the
|
|
1866
|
+
* old value.
|
|
1867
|
+
*/
|
|
1868
|
+
function sameOverride(a, b) {
|
|
1869
|
+
return a.ttlMs === b.ttlMs && a.budgetMb === b.budgetMb && a.activityMs === b.activityMs && a.admission === b.admission;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* TTL-cached read of the cluster native-lease knobs.
|
|
1873
|
+
*
|
|
1874
|
+
* {@link current} is synchronous because the fork that consumes it is;
|
|
1875
|
+
* {@link refresh} is what talks to the hub, and the runner awaits it on the
|
|
1876
|
+
* path that precedes a decode-worker spawn.
|
|
1877
|
+
*/
|
|
1878
|
+
var NativeLeaseSettingsSource = class {
|
|
1879
|
+
override = {};
|
|
1880
|
+
lastReadAtMs = Number.NEGATIVE_INFINITY;
|
|
1881
|
+
inFlight = null;
|
|
1882
|
+
logger;
|
|
1883
|
+
now;
|
|
1884
|
+
ttlMs;
|
|
1885
|
+
constructor(options) {
|
|
1886
|
+
this.logger = options.logger;
|
|
1887
|
+
this.now = options.now ?? (() => Date.now());
|
|
1888
|
+
this.ttlMs = options.ttlMs ?? 3e4;
|
|
1889
|
+
}
|
|
1890
|
+
/** The operator's overrides as last read — `{}` until a read lands. */
|
|
1891
|
+
current() {
|
|
1892
|
+
return this.override;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Re-read if the cached value has expired. Concurrent callers share one
|
|
1896
|
+
* in-flight read; a failure keeps the LAST KNOWN value rather than reverting
|
|
1897
|
+
* to `{}`, because a revert would silently hand the next-spawned workers a
|
|
1898
|
+
* different RAM profile than the ones already running.
|
|
1899
|
+
*/
|
|
1900
|
+
async refresh(api) {
|
|
1901
|
+
if (api === void 0) return this.override;
|
|
1902
|
+
if (this.now() - this.lastReadAtMs < this.ttlMs) return this.override;
|
|
1903
|
+
this.inFlight ??= this.read(api).finally(() => {
|
|
1904
|
+
this.inFlight = null;
|
|
1905
|
+
});
|
|
1906
|
+
await this.inFlight;
|
|
1907
|
+
return this.override;
|
|
1908
|
+
}
|
|
1909
|
+
async read(api) {
|
|
1910
|
+
try {
|
|
1911
|
+
const next = pickNativeLeaseOverride(await api.addonSettings.getGlobalSettings.query({ addonId: NATIVE_LEASE_OWNER_ADDON_ID }));
|
|
1912
|
+
this.lastReadAtMs = this.now();
|
|
1913
|
+
if (sameOverride(next, this.override)) return;
|
|
1914
|
+
this.logger.info("native lease settings changed — applies to NEW decode sessions", { meta: {
|
|
1915
|
+
from: { ...this.override },
|
|
1916
|
+
to: { ...next }
|
|
1917
|
+
} });
|
|
1918
|
+
this.override = next;
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
this.logger.warn("native lease settings read failed — keeping the last known value", { meta: {
|
|
1921
|
+
owner: NATIVE_LEASE_OWNER_ADDON_ID,
|
|
1922
|
+
inUse: { ...this.override },
|
|
1923
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1924
|
+
} });
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
//#endregion
|
|
1783
1929
|
//#region src/pipeline-runner/detail-subtree.ts
|
|
1930
|
+
/** Did the native-lease fetch report a miss (rather than return pixels)? */
|
|
1931
|
+
function isNativeCropMiss(fetch) {
|
|
1932
|
+
return "miss" in fetch;
|
|
1933
|
+
}
|
|
1784
1934
|
/** Normalize a frame-space pixel rect to `[0,1]` by the given frame dims. */
|
|
1785
1935
|
function normalizeRect(rect, frameWidth, frameHeight) {
|
|
1786
1936
|
return {
|
|
@@ -1866,6 +2016,15 @@ function encodeEmbeddingBase64(embedding) {
|
|
|
1866
2016
|
function pickLabel(detection) {
|
|
1867
2017
|
return detection.labels.find((l) => !l.label.startsWith("embedding:"))?.label;
|
|
1868
2018
|
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Turn one executor detection into the cap's `DetailResult`, reprojecting its
|
|
2021
|
+
* crop-space bbox back onto the frame the crop came from.
|
|
2022
|
+
*
|
|
2023
|
+
* Exported because the STATELESS step path (`stateless-step.ts`) produces the
|
|
2024
|
+
* same shape from the same executor output and must not grow its own mapping —
|
|
2025
|
+
* a second reprojection is a second set of coordinates, and the two would agree
|
|
2026
|
+
* right up until one of them changed.
|
|
2027
|
+
*/
|
|
1869
2028
|
function toDetailResult(detection, stepId, crop) {
|
|
1870
2029
|
const label = pickLabel(detection);
|
|
1871
2030
|
return {
|
|
@@ -1885,6 +2044,13 @@ function toDetailResult(detection, stepId, crop) {
|
|
|
1885
2044
|
* traffic: before it existed the only way to feed stored pixels in was a
|
|
1886
2045
|
* pre-cut tile, so the caller decided the rectangle and every caller decided
|
|
1887
2046
|
* differently.
|
|
2047
|
+
*
|
|
2048
|
+
* Exported so the STATELESS step path can reach `deriveDetailCropRect` through
|
|
2049
|
+
* this function rather than importing it — `scripts/check-clip-crop-single-owner.ts`
|
|
2050
|
+
* permits exactly one importer, and that is the enforcement of
|
|
2051
|
+
* [D52](../../../../docs/decisions/adr-0052.md). A second caller of the
|
|
2052
|
+
* derivation is a second cutter no matter how similar its arithmetic looks
|
|
2053
|
+
* today.
|
|
1888
2054
|
*/
|
|
1889
2055
|
async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
1890
2056
|
const image = (await getSharp())(Buffer.from(frameJpegBase64, "base64"));
|
|
@@ -1892,7 +2058,7 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
|
1892
2058
|
const frameWidth = meta.width ?? 0;
|
|
1893
2059
|
const frameHeight = meta.height ?? 0;
|
|
1894
2060
|
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
1895
|
-
const rect = deriveDetailCropRect(bbox, frameWidth, frameHeight, convention);
|
|
2061
|
+
const rect = deriveDetailCropRect(bbox.kind === "frame-pixels" ? bbox.rect : denormalizeRect(bbox.rect, frameWidth, frameHeight), frameWidth, frameHeight, convention);
|
|
1896
2062
|
const left = Math.max(0, Math.min(Math.round(rect.x), frameWidth - 1));
|
|
1897
2063
|
const top = Math.max(0, Math.min(Math.round(rect.y), frameHeight - 1));
|
|
1898
2064
|
const width = Math.max(1, Math.min(Math.round(rect.w), frameWidth - left));
|
|
@@ -1916,37 +2082,82 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
|
1916
2082
|
};
|
|
1917
2083
|
}
|
|
1918
2084
|
async function resolveCrop(deps, input, convention) {
|
|
2085
|
+
let nativeMiss = null;
|
|
1919
2086
|
if (input.frameHandle) {
|
|
1920
2087
|
const normalized = normalizeRect(deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
|
|
1921
2088
|
const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
|
|
1922
|
-
if (native)
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2089
|
+
if (native !== null) {
|
|
2090
|
+
if (!isNativeCropMiss(native)) return {
|
|
2091
|
+
crop: {
|
|
2092
|
+
...native,
|
|
2093
|
+
source: "native"
|
|
2094
|
+
},
|
|
2095
|
+
nativeMiss
|
|
2096
|
+
};
|
|
2097
|
+
nativeMiss = native;
|
|
2098
|
+
}
|
|
1926
2099
|
}
|
|
1927
|
-
if (input.frameJpeg) return
|
|
2100
|
+
if (input.frameJpeg) return {
|
|
2101
|
+
crop: await cutFromFullFrame(input.frameJpeg, {
|
|
2102
|
+
kind: "frame-pixels",
|
|
2103
|
+
rect: input.parent.bbox
|
|
2104
|
+
}, convention),
|
|
2105
|
+
nativeMiss
|
|
2106
|
+
};
|
|
1928
2107
|
if (input.cropJpeg) {
|
|
1929
2108
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
1930
2109
|
const meta = await (await getSharp())(jpeg).metadata();
|
|
1931
2110
|
const width = meta.width ?? 0;
|
|
1932
2111
|
const height = meta.height ?? 0;
|
|
1933
|
-
if (width <= 0 || height <= 0) return
|
|
2112
|
+
if (width <= 0 || height <= 0) return {
|
|
2113
|
+
crop: null,
|
|
2114
|
+
nativeMiss
|
|
2115
|
+
};
|
|
1934
2116
|
return {
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2117
|
+
crop: {
|
|
2118
|
+
jpeg,
|
|
2119
|
+
width,
|
|
2120
|
+
height,
|
|
2121
|
+
frameSpace: input.parent.bbox,
|
|
2122
|
+
source: "fallback"
|
|
2123
|
+
},
|
|
2124
|
+
nativeMiss
|
|
1940
2125
|
};
|
|
1941
2126
|
}
|
|
1942
|
-
return
|
|
2127
|
+
return {
|
|
2128
|
+
crop: null,
|
|
2129
|
+
nativeMiss
|
|
2130
|
+
};
|
|
1943
2131
|
}
|
|
1944
2132
|
/**
|
|
1945
|
-
* Run the DETAIL subtree for a single tracked detection: resolve a crop
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1949
|
-
*
|
|
2133
|
+
* Run the DETAIL subtree for a single tracked detection: resolve a crop, run
|
|
2134
|
+
* each matched top-level child step's chain against it, and map every result
|
|
2135
|
+
* bbox back to frame space. Returns `null` when the camera isn't attached, no
|
|
2136
|
+
* configured child matches `input.parent.className`, or no crop source
|
|
2137
|
+
* resolves.
|
|
2138
|
+
*
|
|
2139
|
+
* ## The three pixel sources, in the order they are tried
|
|
2140
|
+
*
|
|
2141
|
+
* The distinction between them is not the pixels — it is WHICH SIDE derives the
|
|
2142
|
+
* rectangle, and conflating that is how one vector index came to hold two
|
|
2143
|
+
* feature spaces ([D52](../../../../docs/decisions/adr-0052.md)):
|
|
2144
|
+
*
|
|
2145
|
+
* 1. `frameHandle` — the node-local native lease. Cheapest when it hits (no
|
|
2146
|
+
* payload crosses anything) and the only rung that yields a `nativeCropRef`,
|
|
2147
|
+
* so leaf children re-cut their ROI at native resolution. **The runner cuts.**
|
|
2148
|
+
* 2. `frameJpeg` — a FULL frame the caller still holds. **The runner cuts**, with
|
|
2149
|
+
* the same `deriveDetailCropRect` and the same convention, so the rectangle is
|
|
2150
|
+
* identical to what rung 1 would have asked for. Reached when the lease has
|
|
2151
|
+
* already expired — which, for the live detail plane, is the common case: the
|
|
2152
|
+
* dispatcher's tick plus a 500–900 ms inference routinely outlives a 1.2 s
|
|
2153
|
+
* lease, and before this rung was wired the step was simply abandoned.
|
|
2154
|
+
* 3. `cropJpeg` — an ALREADY-CUT tile, used verbatim, no padding and no squaring.
|
|
2155
|
+
* **The caller decided the rectangle**, so it is outside the single-derivation
|
|
2156
|
+
* guarantee and is only for the lease-miss retry.
|
|
2157
|
+
*
|
|
2158
|
+
* A rung-1 miss is remembered even when a lower rung succeeds, and travels into
|
|
2159
|
+
* the drop line below: "the native lease never hits any more" is precisely the
|
|
2160
|
+
* condition that goes invisible once something else covers for it.
|
|
1950
2161
|
*
|
|
1951
2162
|
* STRICT `steps` semantics: when the caller names an explicit `input.steps`
|
|
1952
2163
|
* set, each matched root's descendant chain is pruned to exactly those ids
|
|
@@ -1982,7 +2193,7 @@ async function runDetailSubtree(deps, input) {
|
|
|
1982
2193
|
}
|
|
1983
2194
|
const requested = input.steps;
|
|
1984
2195
|
const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
|
|
1985
|
-
const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
2196
|
+
const { crop, nativeMiss } = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
1986
2197
|
if (!crop) {
|
|
1987
2198
|
deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
|
|
1988
2199
|
tags: { deviceId: input.deviceId },
|
|
@@ -1990,7 +2201,9 @@ async function runDetailSubtree(deps, input) {
|
|
|
1990
2201
|
parentClass: input.parent.className,
|
|
1991
2202
|
hadFrameHandle: input.frameHandle !== void 0,
|
|
1992
2203
|
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
1993
|
-
hadCropJpeg: input.cropJpeg !== void 0
|
|
2204
|
+
hadCropJpeg: input.cropJpeg !== void 0,
|
|
2205
|
+
nativeMissReason: nativeMiss?.miss ?? null,
|
|
2206
|
+
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
|
|
1994
2207
|
}
|
|
1995
2208
|
});
|
|
1996
2209
|
return null;
|
|
@@ -2945,16 +3158,24 @@ var PipelineRunner = class {
|
|
|
2945
3158
|
handle
|
|
2946
3159
|
});
|
|
2947
3160
|
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Returns whether the frame was ADMITTED to the detection queue — i.e.
|
|
3163
|
+
* whether it will reach inference. The caller uses that to mark the frame's
|
|
3164
|
+
* native lease worth keeping (`NativeCropRequester.markInferred`): a native
|
|
3165
|
+
* crop can only ever name a frameId that rode an inference result, so a
|
|
3166
|
+
* frame rejected here is one nobody can ask for. A `false` here is the whole
|
|
3167
|
+
* saving; do not turn it back into `void` for tidiness.
|
|
3168
|
+
*/
|
|
2948
3169
|
enqueueDetectionFrame(deviceId, frame, handle) {
|
|
2949
3170
|
const state = this.cameras.get(deviceId);
|
|
2950
|
-
if (!state) return;
|
|
3171
|
+
if (!state) return false;
|
|
2951
3172
|
this.arrivalCounts.set(deviceId, (this.arrivalCounts.get(deviceId) ?? 0) + 1);
|
|
2952
|
-
if (state.phase !== "active") return;
|
|
3173
|
+
if (state.phase !== "active") return false;
|
|
2953
3174
|
const interval = this.governor.minIntervalMs(deviceId);
|
|
2954
3175
|
if (interval > 0) {
|
|
2955
3176
|
const last = this.lastAdmittedAt.get(deviceId) ?? 0;
|
|
2956
3177
|
const now = Date.now();
|
|
2957
|
-
if (now - last < interval) return;
|
|
3178
|
+
if (now - last < interval) return false;
|
|
2958
3179
|
this.lastAdmittedAt.set(deviceId, now);
|
|
2959
3180
|
}
|
|
2960
3181
|
frame._enqueuedAt = Date.now();
|
|
@@ -2962,6 +3183,7 @@ var PipelineRunner = class {
|
|
|
2962
3183
|
frame,
|
|
2963
3184
|
handle
|
|
2964
3185
|
});
|
|
3186
|
+
return true;
|
|
2965
3187
|
}
|
|
2966
3188
|
/**
|
|
2967
3189
|
* `handle` is optional (mirrors {@link DetectionQueueEntry.handle}) — the
|
|
@@ -2973,13 +3195,14 @@ var PipelineRunner = class {
|
|
|
2973
3195
|
*/
|
|
2974
3196
|
enqueueOccupancyFrame(deviceId, frame, handle) {
|
|
2975
3197
|
const state = this.cameras.get(deviceId);
|
|
2976
|
-
if (!state) return;
|
|
2977
|
-
if (state.phase !== "watching") return;
|
|
3198
|
+
if (!state) return false;
|
|
3199
|
+
if (state.phase !== "watching") return false;
|
|
2978
3200
|
frame._enqueuedAt = Date.now();
|
|
2979
3201
|
state.detectionQueue.enqueue({
|
|
2980
3202
|
frame,
|
|
2981
3203
|
handle
|
|
2982
3204
|
});
|
|
3205
|
+
return true;
|
|
2983
3206
|
}
|
|
2984
3207
|
/**
|
|
2985
3208
|
* Report a motion event for a camera. Drives the unified phase
|
|
@@ -3389,6 +3612,83 @@ var PipelineRunner = class {
|
|
|
3389
3612
|
}
|
|
3390
3613
|
};
|
|
3391
3614
|
//#endregion
|
|
3615
|
+
//#region src/pipeline-runner/stateless-step.ts
|
|
3616
|
+
function modelServability(step, modelId, format) {
|
|
3617
|
+
const entry = step.models.find((m) => m.id === modelId);
|
|
3618
|
+
if (entry === void 0) return { kind: "custom-model" };
|
|
3619
|
+
if (entry.formats[format] !== void 0) return { kind: "catalog-build" };
|
|
3620
|
+
return {
|
|
3621
|
+
kind: "no-build",
|
|
3622
|
+
availableFormats: Object.keys(entry.formats)
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
/** Build the refusal answer, log it, and return it — never one without the other. */
|
|
3626
|
+
function refuse(deps, input, reason, detail) {
|
|
3627
|
+
deps.logger.warn("runStatelessStep refused", {
|
|
3628
|
+
tags: { deviceId: input.sourceDeviceId },
|
|
3629
|
+
meta: {
|
|
3630
|
+
nodeId: deps.nodeId,
|
|
3631
|
+
stepId: input.stepId,
|
|
3632
|
+
modelId: input.modelId,
|
|
3633
|
+
reason,
|
|
3634
|
+
detail
|
|
3635
|
+
}
|
|
3636
|
+
});
|
|
3637
|
+
return {
|
|
3638
|
+
kind: "refused",
|
|
3639
|
+
nodeId: deps.nodeId,
|
|
3640
|
+
reason,
|
|
3641
|
+
detail
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
function errorMessage(err) {
|
|
3645
|
+
return err instanceof Error ? err.message : String(err);
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Run one step over one subject. See the module header for the two invariants.
|
|
3649
|
+
*
|
|
3650
|
+
* Never throws: every failure mode is a named, logged refusal, because the
|
|
3651
|
+
* caller is a bulk pass whose counters are the only signal an operator gets and
|
|
3652
|
+
* a thrown error there reads as "the whole pass broke".
|
|
3653
|
+
*/
|
|
3654
|
+
async function runStatelessStep(deps, input) {
|
|
3655
|
+
const definition = deps.lookupStep(input.stepId);
|
|
3656
|
+
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`);
|
|
3657
|
+
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`);
|
|
3658
|
+
const format = await deps.executionFormat();
|
|
3659
|
+
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");
|
|
3660
|
+
const servability = modelServability(definition, input.modelId, format);
|
|
3661
|
+
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`);
|
|
3662
|
+
let decodeError = null;
|
|
3663
|
+
const crop = await cutFromFullFrame(input.frameJpeg, {
|
|
3664
|
+
kind: "normalized",
|
|
3665
|
+
rect: input.bbox
|
|
3666
|
+
}, deps.resolveCropConvention()).catch((err) => {
|
|
3667
|
+
decodeError = errorMessage(err);
|
|
3668
|
+
return null;
|
|
3669
|
+
});
|
|
3670
|
+
if (crop === null) return refuse(deps, input, "unreadable-frame", decodeError ?? "the supplied frame decoded to no readable dimensions");
|
|
3671
|
+
const steps = [{
|
|
3672
|
+
addonId: input.stepId,
|
|
3673
|
+
modelId: input.modelId,
|
|
3674
|
+
enabled: true
|
|
3675
|
+
}];
|
|
3676
|
+
let result;
|
|
3677
|
+
try {
|
|
3678
|
+
result = await deps.runPipeline(steps, crop.jpeg, input.sourceDeviceId);
|
|
3679
|
+
} catch (err) {
|
|
3680
|
+
return refuse(deps, input, "execution-failed", errorMessage(err));
|
|
3681
|
+
}
|
|
3682
|
+
if (result === null) return refuse(deps, input, "execution-failed", "the executor returned no frame result for the crop");
|
|
3683
|
+
const details = result.detections.map((detection) => toDetailResult(detection, input.stepId, crop));
|
|
3684
|
+
return {
|
|
3685
|
+
kind: "ran",
|
|
3686
|
+
nodeId: deps.nodeId,
|
|
3687
|
+
modelId: input.modelId,
|
|
3688
|
+
details
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
//#endregion
|
|
3392
3692
|
//#region src/pipeline-runner/index.ts
|
|
3393
3693
|
var DEFAULT_CONFIG = {
|
|
3394
3694
|
maxQueueDepth: 30,
|
|
@@ -4665,13 +4965,38 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4665
4965
|
* resolve legitimately want the lower-RES honest frame).
|
|
4666
4966
|
*/
|
|
4667
4967
|
async getNativeCrop(input) {
|
|
4968
|
+
const resolved = await this.resolveNativeCropPixels(input);
|
|
4969
|
+
if (resolved.tier === "native" && resolved.pixels !== null) return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
|
|
4970
|
+
if (resolved.tier !== "ram-fullframe") return null;
|
|
4971
|
+
const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
|
|
4972
|
+
if (!crop) return null;
|
|
4973
|
+
return this.encodeNativeCropResult({
|
|
4974
|
+
bytes: crop.bytes,
|
|
4975
|
+
width: crop.width,
|
|
4976
|
+
height: crop.height
|
|
4977
|
+
}, resolved.tier, input.encodeJpeg);
|
|
4978
|
+
}
|
|
4979
|
+
/**
|
|
4980
|
+
* The native-lease half of {@link getNativeCrop}, extracted so the DETAIL path
|
|
4981
|
+
* can keep the miss REASON the cap's `null` return has no room for. Both
|
|
4982
|
+
* callers share this one decision — a second copy of the tier arithmetic is
|
|
4983
|
+
* how a quality tier starts differing by caller.
|
|
4984
|
+
*
|
|
4985
|
+
* Logging level follows the CONSEQUENCE, not the event: a native miss that the
|
|
4986
|
+
* RAM tier is allowed to serve (a full-frame resolve) cost nobody anything and
|
|
4987
|
+
* stays at debug; a miss the caller is handed as `null` is lost work and is a
|
|
4988
|
+
* warn, carrying the numbers that decide the fix.
|
|
4989
|
+
*/
|
|
4990
|
+
async resolveNativeCropPixels(input) {
|
|
4668
4991
|
const b = input.bbox;
|
|
4669
4992
|
const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
|
|
4670
4993
|
const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
|
|
4671
4994
|
let nativeResult = null;
|
|
4672
4995
|
let missReason = null;
|
|
4996
|
+
let handleAgeMs = null;
|
|
4673
4997
|
if (!isUnboundedFullFrame) {
|
|
4674
4998
|
const found = this.nativeCropRegistry.get(input.handle);
|
|
4999
|
+
handleAgeMs = found?.ageMs ?? null;
|
|
4675
5000
|
if (found) {
|
|
4676
5001
|
const result = await found.request(input.bbox, input.maxWidth);
|
|
4677
5002
|
if (result) nativeResult = {
|
|
@@ -4681,28 +5006,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4681
5006
|
};
|
|
4682
5007
|
else missReason = "worker-lease-gone";
|
|
4683
5008
|
} else missReason = "no-registry-entry";
|
|
4684
|
-
if (missReason !== null)
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
5009
|
+
if (missReason !== null) {
|
|
5010
|
+
const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
|
|
5011
|
+
const fields = {
|
|
5012
|
+
tags: { deviceId: found?.deviceId ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
|
|
5013
|
+
meta: {
|
|
5014
|
+
reason: missReason,
|
|
5015
|
+
servedTier: tierOnMiss,
|
|
5016
|
+
handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
|
|
5017
|
+
handleNodeId: input.handle.nodeId,
|
|
5018
|
+
handleAgeMs,
|
|
5019
|
+
registrySize: this.nativeCropRegistry.size,
|
|
5020
|
+
registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
|
|
5021
|
+
}
|
|
5022
|
+
};
|
|
5023
|
+
if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
|
|
5024
|
+
else this.ctx.logger.debug("native crop miss", fields);
|
|
5025
|
+
}
|
|
4695
5026
|
}
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
bytes: crop.bytes,
|
|
4703
|
-
width: crop.width,
|
|
4704
|
-
height: crop.height
|
|
4705
|
-
}, tier, input.encodeJpeg);
|
|
5027
|
+
return {
|
|
5028
|
+
pixels: nativeResult,
|
|
5029
|
+
tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
|
|
5030
|
+
missReason,
|
|
5031
|
+
handleAgeMs
|
|
5032
|
+
};
|
|
4706
5033
|
}
|
|
4707
5034
|
/**
|
|
4708
5035
|
* Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
|
|
@@ -4757,6 +5084,66 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4757
5084
|
}, input);
|
|
4758
5085
|
}
|
|
4759
5086
|
/**
|
|
5087
|
+
* Stateless single-step execution — no attach, no handle, no device
|
|
5088
|
+
* affinity. Delegates the decision table (catalog lookup, MODEL PIN
|
|
5089
|
+
* servability, crop, dispatch) to the deps-injected pure function in
|
|
5090
|
+
* {@link runStatelessStepImpl}; this method only wires the real deps.
|
|
5091
|
+
*
|
|
5092
|
+
* Note what is NOT wired, deliberately: no `resolveChildSteps` (the step
|
|
5093
|
+
* comes from the catalog, not from an attachment) and no `deviceKey` /
|
|
5094
|
+
* device jump (the jump roster is part of the attach config). The step runs
|
|
5095
|
+
* on the node's default engine, which is exactly the engine
|
|
5096
|
+
* `executionFormat` reports — so the servability check and the dispatch
|
|
5097
|
+
* cannot disagree about which format was verified.
|
|
5098
|
+
*/
|
|
5099
|
+
async runStatelessStep(input) {
|
|
5100
|
+
const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
|
|
5101
|
+
return runStatelessStep({
|
|
5102
|
+
nodeId: this.nodeId,
|
|
5103
|
+
lookupStep: (stepId) => tryGetStepDefinition(stepId),
|
|
5104
|
+
executionFormat: () => this.resolveNodeExecutionFormat(),
|
|
5105
|
+
resolveCropConvention: () => convention,
|
|
5106
|
+
runPipeline: (steps, imageJpeg, sourceDeviceId) => this.runStatelessPipeline(steps, imageJpeg, sourceDeviceId),
|
|
5107
|
+
logger: this.detailSubtreeLogger()
|
|
5108
|
+
}, input);
|
|
5109
|
+
}
|
|
5110
|
+
/**
|
|
5111
|
+
* The model format this node dispatches on when no `deviceKey` is given —
|
|
5112
|
+
* its selected engine's format. `null` on any failure, which the caller
|
|
5113
|
+
* turns into a refusal rather than a guess: a model pin that cannot be
|
|
5114
|
+
* checked is a substitution waiting to happen.
|
|
5115
|
+
*/
|
|
5116
|
+
async resolveNodeExecutionFormat() {
|
|
5117
|
+
const api = this.ctxIfReady?.api;
|
|
5118
|
+
if (!api) return null;
|
|
5119
|
+
try {
|
|
5120
|
+
return (await api.pipelineExecutor.getSelectedEngine.query()).format;
|
|
5121
|
+
} catch (err) {
|
|
5122
|
+
this.ctxIfReady?.logger.warn("runStatelessStep: could not read this node selected engine — refusing rather than guessing a format", { meta: { error: errMsg(err) } });
|
|
5123
|
+
return null;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
/**
|
|
5127
|
+
* Real `runPipeline` dep for the stateless path: node-local
|
|
5128
|
+
* `pipelineExecutor.runPipeline` on the `'full'` plane with NO `deviceKey`.
|
|
5129
|
+
*
|
|
5130
|
+
* `deviceId` is passed so the executor's own gate/substitution lines are
|
|
5131
|
+
* tagged with the source camera. It is a routing hint on other transports,
|
|
5132
|
+
* but not here: the executor is co-located in the `detection` group, so
|
|
5133
|
+
* `localProviderLink` answers in-process before any router sees it — the
|
|
5134
|
+
* call cannot leave the node this one was pinned to.
|
|
5135
|
+
*/
|
|
5136
|
+
async runStatelessPipeline(steps, imageJpeg, sourceDeviceId) {
|
|
5137
|
+
const api = this.ctxIfReady?.api;
|
|
5138
|
+
if (!api) return null;
|
|
5139
|
+
return api.pipelineExecutor.runPipeline.mutate({
|
|
5140
|
+
steps: [...steps],
|
|
5141
|
+
image: new Uint8Array(imageJpeg),
|
|
5142
|
+
deviceId: sourceDeviceId,
|
|
5143
|
+
plane: "full"
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
5146
|
+
/**
|
|
4760
5147
|
* The cluster-wide detail-crop convention — read from the orchestrator's
|
|
4761
5148
|
* global settings, NOT from the attach config. See `crop-convention-source.ts`
|
|
4762
5149
|
* for why eleven attach builders make the attach config the wrong carrier.
|
|
@@ -4765,6 +5152,34 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4765
5152
|
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
4766
5153
|
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
4767
5154
|
} });
|
|
5155
|
+
/**
|
|
5156
|
+
* The cluster-wide native-frame LEASE knobs (TTL / RAM ceiling / demand
|
|
5157
|
+
* window) — read from the orchestrator's global settings for the same reason
|
|
5158
|
+
* as the crop convention above, and injected into each decode worker at fork
|
|
5159
|
+
* time. See `native-lease-settings-source.ts`.
|
|
5160
|
+
*/
|
|
5161
|
+
nativeLease = new NativeLeaseSettingsSource({ logger: {
|
|
5162
|
+
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
5163
|
+
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
5164
|
+
} });
|
|
5165
|
+
/**
|
|
5166
|
+
* Fork a decode worker carrying the operator's lease knobs. Used in place of
|
|
5167
|
+
* a bare `forkDecodeWorker` reference at every session-decode spawn site: the
|
|
5168
|
+
* knobs are read at spawn, so a changed setting reaches NEW sessions and
|
|
5169
|
+
* leaves running ones alone (a live session would have to be re-dialled).
|
|
5170
|
+
*/
|
|
5171
|
+
forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current());
|
|
5172
|
+
/**
|
|
5173
|
+
* Refresh the lease knobs before a decode worker is spawned. Awaited on the
|
|
5174
|
+
* `acquire` leg of `startGuardedSessionDecode` (which always completes before
|
|
5175
|
+
* `startPump` forks), so the spawn sees a value at most one TTL stale rather
|
|
5176
|
+
* than whatever a background refresh happened to have landed.
|
|
5177
|
+
* `NativeLeaseSettingsSource.refresh` swallows its own failures, so this can
|
|
5178
|
+
* never break a dial.
|
|
5179
|
+
*/
|
|
5180
|
+
async refreshNativeLeaseSettings() {
|
|
5181
|
+
await this.nativeLease.refresh(this.ctxIfReady?.api);
|
|
5182
|
+
}
|
|
4768
5183
|
/** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
|
|
4769
5184
|
detailSubtreeLogger() {
|
|
4770
5185
|
return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
|
|
@@ -4847,18 +5262,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4847
5262
|
* lease (same path the `getNativeCrop` cap method above uses), encodes the
|
|
4848
5263
|
* raw RGB crop to JPEG, and denormalizes `paddedBbox` back to frame-space
|
|
4849
5264
|
* pixels for the `frameSpace` the caller maps result bboxes against.
|
|
5265
|
+
*
|
|
5266
|
+
* Returns the MISS REASON rather than a bare `null`. This is the only caller
|
|
5267
|
+
* that can carry it anywhere useful — the cap's return shape has no room for
|
|
5268
|
+
* it, and the detail plane's drop line is where an operator reads why an
|
|
5269
|
+
* enrichment dispatch produced nothing.
|
|
4850
5270
|
*/
|
|
4851
5271
|
async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
|
|
4852
|
-
const
|
|
5272
|
+
const resolved = await this.resolveNativeCropPixels({
|
|
4853
5273
|
handle,
|
|
4854
|
-
bbox: paddedBbox
|
|
4855
|
-
encodeJpeg: true
|
|
5274
|
+
bbox: paddedBbox
|
|
4856
5275
|
});
|
|
4857
|
-
if (
|
|
5276
|
+
if (resolved.pixels === null || resolved.tier !== "native") return {
|
|
5277
|
+
miss: resolved.missReason ?? "worker-lease-gone",
|
|
5278
|
+
handleAgeMs: resolved.handleAgeMs
|
|
5279
|
+
};
|
|
5280
|
+
const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
|
|
5281
|
+
if (!encoded?.jpeg) return {
|
|
5282
|
+
miss: "jpeg-encode-failed",
|
|
5283
|
+
handleAgeMs: resolved.handleAgeMs
|
|
5284
|
+
};
|
|
4858
5285
|
return {
|
|
4859
|
-
jpeg: Buffer.from(
|
|
4860
|
-
width:
|
|
4861
|
-
height:
|
|
5286
|
+
jpeg: Buffer.from(encoded.jpeg, "base64"),
|
|
5287
|
+
width: encoded.width,
|
|
5288
|
+
height: encoded.height,
|
|
4862
5289
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
4863
5290
|
};
|
|
4864
5291
|
}
|
|
@@ -5083,6 +5510,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5083
5510
|
* Kept as a method so the two call sites need no change.
|
|
5084
5511
|
*/
|
|
5085
5512
|
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
5513
|
+
await this.refreshNativeLeaseSettings();
|
|
5086
5514
|
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
5087
5515
|
deviceId,
|
|
5088
5516
|
streamId,
|
|
@@ -5124,8 +5552,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5124
5552
|
*/
|
|
5125
5553
|
enqueueSharedDetectionFrame(deviceId, frame, handle) {
|
|
5126
5554
|
const runner = this.runner;
|
|
5127
|
-
if (!runner) return;
|
|
5128
|
-
runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5555
|
+
if (!runner) return false;
|
|
5556
|
+
const admitted = runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5129
5557
|
const sink = this.sharedMotionSinks.get(deviceId);
|
|
5130
5558
|
if (sink) {
|
|
5131
5559
|
const now = this.sharedSinkNow();
|
|
@@ -5134,6 +5562,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5134
5562
|
runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
|
|
5135
5563
|
}
|
|
5136
5564
|
}
|
|
5565
|
+
return admitted;
|
|
5137
5566
|
}
|
|
5138
5567
|
/**
|
|
5139
5568
|
* Epic C P1 detection path for a flagged camera: decode `rgb` in a
|
|
@@ -5189,7 +5618,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5189
5618
|
codec: source.codec
|
|
5190
5619
|
} });
|
|
5191
5620
|
return startSessionDecodePump({
|
|
5192
|
-
fork:
|
|
5621
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5193
5622
|
logger: log,
|
|
5194
5623
|
source,
|
|
5195
5624
|
opts: {
|
|
@@ -5201,7 +5630,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5201
5630
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
5202
5631
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5203
5632
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5204
|
-
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
5633
|
+
if (this.enqueueSharedDetectionFrame(config.deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5205
5634
|
}
|
|
5206
5635
|
});
|
|
5207
5636
|
}
|
|
@@ -5234,7 +5663,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5234
5663
|
codec: source.codec
|
|
5235
5664
|
} });
|
|
5236
5665
|
return startSessionDecodePump({
|
|
5237
|
-
fork:
|
|
5666
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5238
5667
|
logger: log,
|
|
5239
5668
|
source,
|
|
5240
5669
|
opts: {
|
|
@@ -5706,7 +6135,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5706
6135
|
logger: log,
|
|
5707
6136
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
5708
6137
|
startPump: (source) => startSessionDecodePump({
|
|
5709
|
-
fork:
|
|
6138
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5710
6139
|
logger: log,
|
|
5711
6140
|
source,
|
|
5712
6141
|
opts: {
|
|
@@ -5719,7 +6148,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5719
6148
|
if (settled) return;
|
|
5720
6149
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5721
6150
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5722
|
-
runner.enqueueOccupancyFrame(deviceId, frame, handle);
|
|
6151
|
+
if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5723
6152
|
count++;
|
|
5724
6153
|
if (count >= frames) {
|
|
5725
6154
|
clearTimeout(safetyTimer);
|