@camstack/addon-pipeline 1.2.43 → 1.2.45
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 +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +158 -81
- package/dist/detection-pipeline/index.mjs +158 -81
- package/dist/{dist-B-VVBzrL.mjs → dist-BDkEhx4Z.mjs} +1315 -127
- package/dist/{dist-UHbKZiSa.js → dist-QyZ7QSRP.js} +1386 -126
- package/dist/{event-loop-stall-monitor-C7_L6vHO.js → event-loop-stall-monitor-COEKVBQK.js} +53 -8
- package/dist/{event-loop-stall-monitor-DfKdC7G4.mjs → event-loop-stall-monitor-IAuNEdmG.mjs} +53 -8
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +342 -69
- package/dist/pipeline-runner/index.mjs +342 -69
- package/dist/recorder/index.js +189 -6
- package/dist/recorder/index.mjs +189 -6
- 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-D7iQbuKY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C68YfSUH.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CT2bGWqM.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-54IklEep.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B3GxBnXM.mjs} +1 -1
- package/dist/stream-broker/{hostInit-CKYS9yZO.mjs → hostInit-BMCrULpc.mjs} +2 -2
- package/dist/stream-broker/index.js +4602 -1442
- package/dist/stream-broker/index.mjs +4601 -1442
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-DFH_z2-q.mjs +244 -0
- package/dist/worker-protocol-le27Mw_8.js +279 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DxZ75vB8.js → MaskShapeCanvas-DI4BY7W2-C7bsz4cP.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BBY2Ztz5.js → MotionZonesSettings-NcxxQN8r-BmhLinUd.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BC-3QmHa.js → PrivacyMaskSettings-APgPLF7p-BJjyGWFy.js} +1 -1
- package/embed-dist/assets/index-DC63og2c.css +2 -0
- package/embed-dist/assets/{index-on1wyOnd.js → index-a4CMt-Uc.js} +12 -12
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/python/postprocessors/__init__.py +2 -0
- package/python/postprocessors/rfdetr.py +161 -0
- package/python/postprocessors/test_rfdetr.py +193 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCC9fqHH.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 { $ as errMsg, A as deriveDetailCropRect, At as EventCategory, Ct as lazy, D as customAction, Et as object, H as pickNativeLeaseOverride, Ot as string, Tt as number, V as pickDetailCropConvention, W as pipelineRunnerCapability, at as BaseAddon, bt as array, k as defineCustomActions, l as DEVICE_BACKEND_TO_FORMAT, lt as createEvent, mt as nodePin, s as DEFAULT_DETAIL_CROP_CONVENTION, wt as literal, xt as boolean, yt as _enum } from "../dist-BDkEhx4Z.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-IAuNEdmG.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-DFH_z2-q.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 {
|
|
@@ -1932,40 +2082,82 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
|
1932
2082
|
};
|
|
1933
2083
|
}
|
|
1934
2084
|
async function resolveCrop(deps, input, convention) {
|
|
2085
|
+
let nativeMiss = null;
|
|
1935
2086
|
if (input.frameHandle) {
|
|
1936
2087
|
const normalized = normalizeRect(deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
|
|
1937
2088
|
const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
|
|
1938
|
-
if (native)
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2089
|
+
if (native !== null) {
|
|
2090
|
+
if (!isNativeCropMiss(native)) return {
|
|
2091
|
+
crop: {
|
|
2092
|
+
...native,
|
|
2093
|
+
source: "native"
|
|
2094
|
+
},
|
|
2095
|
+
nativeMiss
|
|
2096
|
+
};
|
|
2097
|
+
nativeMiss = native;
|
|
2098
|
+
}
|
|
1942
2099
|
}
|
|
1943
|
-
if (input.frameJpeg) return
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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
|
+
};
|
|
1947
2107
|
if (input.cropJpeg) {
|
|
1948
2108
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
1949
2109
|
const meta = await (await getSharp())(jpeg).metadata();
|
|
1950
2110
|
const width = meta.width ?? 0;
|
|
1951
2111
|
const height = meta.height ?? 0;
|
|
1952
|
-
if (width <= 0 || height <= 0) return
|
|
2112
|
+
if (width <= 0 || height <= 0) return {
|
|
2113
|
+
crop: null,
|
|
2114
|
+
nativeMiss
|
|
2115
|
+
};
|
|
1953
2116
|
return {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2117
|
+
crop: {
|
|
2118
|
+
jpeg,
|
|
2119
|
+
width,
|
|
2120
|
+
height,
|
|
2121
|
+
frameSpace: input.parent.bbox,
|
|
2122
|
+
source: "fallback"
|
|
2123
|
+
},
|
|
2124
|
+
nativeMiss
|
|
1959
2125
|
};
|
|
1960
2126
|
}
|
|
1961
|
-
return
|
|
2127
|
+
return {
|
|
2128
|
+
crop: null,
|
|
2129
|
+
nativeMiss
|
|
2130
|
+
};
|
|
1962
2131
|
}
|
|
1963
2132
|
/**
|
|
1964
|
-
* Run the DETAIL subtree for a single tracked detection: resolve a crop
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
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.
|
|
1969
2161
|
*
|
|
1970
2162
|
* STRICT `steps` semantics: when the caller names an explicit `input.steps`
|
|
1971
2163
|
* set, each matched root's descendant chain is pruned to exactly those ids
|
|
@@ -2001,7 +2193,7 @@ async function runDetailSubtree(deps, input) {
|
|
|
2001
2193
|
}
|
|
2002
2194
|
const requested = input.steps;
|
|
2003
2195
|
const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
|
|
2004
|
-
const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
2196
|
+
const { crop, nativeMiss } = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
2005
2197
|
if (!crop) {
|
|
2006
2198
|
deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
|
|
2007
2199
|
tags: { deviceId: input.deviceId },
|
|
@@ -2009,7 +2201,9 @@ async function runDetailSubtree(deps, input) {
|
|
|
2009
2201
|
parentClass: input.parent.className,
|
|
2010
2202
|
hadFrameHandle: input.frameHandle !== void 0,
|
|
2011
2203
|
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
2012
|
-
hadCropJpeg: input.cropJpeg !== void 0
|
|
2204
|
+
hadCropJpeg: input.cropJpeg !== void 0,
|
|
2205
|
+
nativeMissReason: nativeMiss?.miss ?? null,
|
|
2206
|
+
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
|
|
2013
2207
|
}
|
|
2014
2208
|
});
|
|
2015
2209
|
return null;
|
|
@@ -2964,16 +3158,24 @@ var PipelineRunner = class {
|
|
|
2964
3158
|
handle
|
|
2965
3159
|
});
|
|
2966
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
|
+
*/
|
|
2967
3169
|
enqueueDetectionFrame(deviceId, frame, handle) {
|
|
2968
3170
|
const state = this.cameras.get(deviceId);
|
|
2969
|
-
if (!state) return;
|
|
3171
|
+
if (!state) return false;
|
|
2970
3172
|
this.arrivalCounts.set(deviceId, (this.arrivalCounts.get(deviceId) ?? 0) + 1);
|
|
2971
|
-
if (state.phase !== "active") return;
|
|
3173
|
+
if (state.phase !== "active") return false;
|
|
2972
3174
|
const interval = this.governor.minIntervalMs(deviceId);
|
|
2973
3175
|
if (interval > 0) {
|
|
2974
3176
|
const last = this.lastAdmittedAt.get(deviceId) ?? 0;
|
|
2975
3177
|
const now = Date.now();
|
|
2976
|
-
if (now - last < interval) return;
|
|
3178
|
+
if (now - last < interval) return false;
|
|
2977
3179
|
this.lastAdmittedAt.set(deviceId, now);
|
|
2978
3180
|
}
|
|
2979
3181
|
frame._enqueuedAt = Date.now();
|
|
@@ -2981,6 +3183,7 @@ var PipelineRunner = class {
|
|
|
2981
3183
|
frame,
|
|
2982
3184
|
handle
|
|
2983
3185
|
});
|
|
3186
|
+
return true;
|
|
2984
3187
|
}
|
|
2985
3188
|
/**
|
|
2986
3189
|
* `handle` is optional (mirrors {@link DetectionQueueEntry.handle}) — the
|
|
@@ -2992,13 +3195,14 @@ var PipelineRunner = class {
|
|
|
2992
3195
|
*/
|
|
2993
3196
|
enqueueOccupancyFrame(deviceId, frame, handle) {
|
|
2994
3197
|
const state = this.cameras.get(deviceId);
|
|
2995
|
-
if (!state) return;
|
|
2996
|
-
if (state.phase !== "watching") return;
|
|
3198
|
+
if (!state) return false;
|
|
3199
|
+
if (state.phase !== "watching") return false;
|
|
2997
3200
|
frame._enqueuedAt = Date.now();
|
|
2998
3201
|
state.detectionQueue.enqueue({
|
|
2999
3202
|
frame,
|
|
3000
3203
|
handle
|
|
3001
3204
|
});
|
|
3205
|
+
return true;
|
|
3002
3206
|
}
|
|
3003
3207
|
/**
|
|
3004
3208
|
* Report a motion event for a camera. Drives the unified phase
|
|
@@ -4761,13 +4965,38 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4761
4965
|
* resolve legitimately want the lower-RES honest frame).
|
|
4762
4966
|
*/
|
|
4763
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) {
|
|
4764
4991
|
const b = input.bbox;
|
|
4765
4992
|
const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
|
|
4766
4993
|
const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
|
|
4767
4994
|
let nativeResult = null;
|
|
4768
4995
|
let missReason = null;
|
|
4996
|
+
let handleAgeMs = null;
|
|
4769
4997
|
if (!isUnboundedFullFrame) {
|
|
4770
4998
|
const found = this.nativeCropRegistry.get(input.handle);
|
|
4999
|
+
handleAgeMs = found?.ageMs ?? null;
|
|
4771
5000
|
if (found) {
|
|
4772
5001
|
const result = await found.request(input.bbox, input.maxWidth);
|
|
4773
5002
|
if (result) nativeResult = {
|
|
@@ -4777,28 +5006,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4777
5006
|
};
|
|
4778
5007
|
else missReason = "worker-lease-gone";
|
|
4779
5008
|
} else missReason = "no-registry-entry";
|
|
4780
|
-
if (missReason !== null)
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
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
|
+
}
|
|
4791
5026
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
bytes: crop.bytes,
|
|
4799
|
-
width: crop.width,
|
|
4800
|
-
height: crop.height
|
|
4801
|
-
}, tier, input.encodeJpeg);
|
|
5027
|
+
return {
|
|
5028
|
+
pixels: nativeResult,
|
|
5029
|
+
tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
|
|
5030
|
+
missReason,
|
|
5031
|
+
handleAgeMs
|
|
5032
|
+
};
|
|
4802
5033
|
}
|
|
4803
5034
|
/**
|
|
4804
5035
|
* Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
|
|
@@ -4921,6 +5152,34 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4921
5152
|
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
4922
5153
|
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
4923
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
|
+
}
|
|
4924
5183
|
/** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
|
|
4925
5184
|
detailSubtreeLogger() {
|
|
4926
5185
|
return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
|
|
@@ -5003,18 +5262,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5003
5262
|
* lease (same path the `getNativeCrop` cap method above uses), encodes the
|
|
5004
5263
|
* raw RGB crop to JPEG, and denormalizes `paddedBbox` back to frame-space
|
|
5005
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.
|
|
5006
5270
|
*/
|
|
5007
5271
|
async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
|
|
5008
|
-
const
|
|
5272
|
+
const resolved = await this.resolveNativeCropPixels({
|
|
5009
5273
|
handle,
|
|
5010
|
-
bbox: paddedBbox
|
|
5011
|
-
encodeJpeg: true
|
|
5274
|
+
bbox: paddedBbox
|
|
5012
5275
|
});
|
|
5013
|
-
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
|
+
};
|
|
5014
5285
|
return {
|
|
5015
|
-
jpeg: Buffer.from(
|
|
5016
|
-
width:
|
|
5017
|
-
height:
|
|
5286
|
+
jpeg: Buffer.from(encoded.jpeg, "base64"),
|
|
5287
|
+
width: encoded.width,
|
|
5288
|
+
height: encoded.height,
|
|
5018
5289
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
5019
5290
|
};
|
|
5020
5291
|
}
|
|
@@ -5239,6 +5510,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5239
5510
|
* Kept as a method so the two call sites need no change.
|
|
5240
5511
|
*/
|
|
5241
5512
|
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
5513
|
+
await this.refreshNativeLeaseSettings();
|
|
5242
5514
|
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
5243
5515
|
deviceId,
|
|
5244
5516
|
streamId,
|
|
@@ -5280,8 +5552,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5280
5552
|
*/
|
|
5281
5553
|
enqueueSharedDetectionFrame(deviceId, frame, handle) {
|
|
5282
5554
|
const runner = this.runner;
|
|
5283
|
-
if (!runner) return;
|
|
5284
|
-
runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5555
|
+
if (!runner) return false;
|
|
5556
|
+
const admitted = runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5285
5557
|
const sink = this.sharedMotionSinks.get(deviceId);
|
|
5286
5558
|
if (sink) {
|
|
5287
5559
|
const now = this.sharedSinkNow();
|
|
@@ -5290,6 +5562,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5290
5562
|
runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
|
|
5291
5563
|
}
|
|
5292
5564
|
}
|
|
5565
|
+
return admitted;
|
|
5293
5566
|
}
|
|
5294
5567
|
/**
|
|
5295
5568
|
* Epic C P1 detection path for a flagged camera: decode `rgb` in a
|
|
@@ -5345,7 +5618,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5345
5618
|
codec: source.codec
|
|
5346
5619
|
} });
|
|
5347
5620
|
return startSessionDecodePump({
|
|
5348
|
-
fork:
|
|
5621
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5349
5622
|
logger: log,
|
|
5350
5623
|
source,
|
|
5351
5624
|
opts: {
|
|
@@ -5357,7 +5630,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5357
5630
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
5358
5631
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5359
5632
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5360
|
-
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
5633
|
+
if (this.enqueueSharedDetectionFrame(config.deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5361
5634
|
}
|
|
5362
5635
|
});
|
|
5363
5636
|
}
|
|
@@ -5390,7 +5663,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5390
5663
|
codec: source.codec
|
|
5391
5664
|
} });
|
|
5392
5665
|
return startSessionDecodePump({
|
|
5393
|
-
fork:
|
|
5666
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5394
5667
|
logger: log,
|
|
5395
5668
|
source,
|
|
5396
5669
|
opts: {
|
|
@@ -5862,7 +6135,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5862
6135
|
logger: log,
|
|
5863
6136
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
5864
6137
|
startPump: (source) => startSessionDecodePump({
|
|
5865
|
-
fork:
|
|
6138
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5866
6139
|
logger: log,
|
|
5867
6140
|
source,
|
|
5868
6141
|
opts: {
|
|
@@ -5875,7 +6148,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5875
6148
|
if (settled) return;
|
|
5876
6149
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5877
6150
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5878
|
-
runner.enqueueOccupancyFrame(deviceId, frame, handle);
|
|
6151
|
+
if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5879
6152
|
count++;
|
|
5880
6153
|
if (count >= frames) {
|
|
5881
6154
|
clearTimeout(safetyTimer);
|