@camstack/addon-pipeline 1.2.43 → 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 +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +157 -81
- package/dist/detection-pipeline/index.mjs +157 -81
- package/dist/{dist-UHbKZiSa.js → dist-BuFE5rOK.js} +724 -39
- package/dist/{dist-B-VVBzrL.mjs → dist-DOu93i_g.mjs} +659 -40
- package/dist/{event-loop-stall-monitor-DfKdC7G4.mjs → event-loop-stall-monitor-CShgQE6l.mjs} +40 -2
- package/dist/{event-loop-stall-monitor-C7_L6vHO.js → event-loop-stall-monitor-L08yG6bB.js} +40 -2
- 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-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-54IklEep.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-CKYS9yZO.mjs → hostInit-khRXMnjl.mjs} +2 -2
- package/dist/stream-broker/index.js +3952 -1103
- package/dist/stream-broker/index.mjs +3953 -1104
- 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-DxZ75vB8.js → MaskShapeCanvas-DI4BY7W2-DHZuYYjH.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BBY2Ztz5.js → MotionZonesSettings-NcxxQN8r-jFDOzPCD.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BC-3QmHa.js → PrivacyMaskSettings-APgPLF7p-BPBXgIX8.js} +1 -1
- package/embed-dist/assets/{index-on1wyOnd.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-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
|
@@ -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 {
|
|
@@ -1940,40 +2090,82 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
|
1940
2090
|
};
|
|
1941
2091
|
}
|
|
1942
2092
|
async function resolveCrop(deps, input, convention) {
|
|
2093
|
+
let nativeMiss = null;
|
|
1943
2094
|
if (input.frameHandle) {
|
|
1944
2095
|
const normalized = normalizeRect(require_dist.deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
|
|
1945
2096
|
const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
|
|
1946
|
-
if (native)
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2097
|
+
if (native !== null) {
|
|
2098
|
+
if (!isNativeCropMiss(native)) return {
|
|
2099
|
+
crop: {
|
|
2100
|
+
...native,
|
|
2101
|
+
source: "native"
|
|
2102
|
+
},
|
|
2103
|
+
nativeMiss
|
|
2104
|
+
};
|
|
2105
|
+
nativeMiss = native;
|
|
2106
|
+
}
|
|
1950
2107
|
}
|
|
1951
|
-
if (input.frameJpeg) return
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
+
};
|
|
1955
2115
|
if (input.cropJpeg) {
|
|
1956
2116
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
1957
2117
|
const meta = await (await require_lazy_sharp.getSharp())(jpeg).metadata();
|
|
1958
2118
|
const width = meta.width ?? 0;
|
|
1959
2119
|
const height = meta.height ?? 0;
|
|
1960
|
-
if (width <= 0 || height <= 0) return
|
|
2120
|
+
if (width <= 0 || height <= 0) return {
|
|
2121
|
+
crop: null,
|
|
2122
|
+
nativeMiss
|
|
2123
|
+
};
|
|
1961
2124
|
return {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2125
|
+
crop: {
|
|
2126
|
+
jpeg,
|
|
2127
|
+
width,
|
|
2128
|
+
height,
|
|
2129
|
+
frameSpace: input.parent.bbox,
|
|
2130
|
+
source: "fallback"
|
|
2131
|
+
},
|
|
2132
|
+
nativeMiss
|
|
1967
2133
|
};
|
|
1968
2134
|
}
|
|
1969
|
-
return
|
|
2135
|
+
return {
|
|
2136
|
+
crop: null,
|
|
2137
|
+
nativeMiss
|
|
2138
|
+
};
|
|
1970
2139
|
}
|
|
1971
2140
|
/**
|
|
1972
|
-
* Run the DETAIL subtree for a single tracked detection: resolve a crop
|
|
1973
|
-
*
|
|
1974
|
-
*
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
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.
|
|
1977
2169
|
*
|
|
1978
2170
|
* STRICT `steps` semantics: when the caller names an explicit `input.steps`
|
|
1979
2171
|
* set, each matched root's descendant chain is pruned to exactly those ids
|
|
@@ -2009,7 +2201,7 @@ async function runDetailSubtree(deps, input) {
|
|
|
2009
2201
|
}
|
|
2010
2202
|
const requested = input.steps;
|
|
2011
2203
|
const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
|
|
2012
|
-
const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
2204
|
+
const { crop, nativeMiss } = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
2013
2205
|
if (!crop) {
|
|
2014
2206
|
deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
|
|
2015
2207
|
tags: { deviceId: input.deviceId },
|
|
@@ -2017,7 +2209,9 @@ async function runDetailSubtree(deps, input) {
|
|
|
2017
2209
|
parentClass: input.parent.className,
|
|
2018
2210
|
hadFrameHandle: input.frameHandle !== void 0,
|
|
2019
2211
|
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
2020
|
-
hadCropJpeg: input.cropJpeg !== void 0
|
|
2212
|
+
hadCropJpeg: input.cropJpeg !== void 0,
|
|
2213
|
+
nativeMissReason: nativeMiss?.miss ?? null,
|
|
2214
|
+
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
|
|
2021
2215
|
}
|
|
2022
2216
|
});
|
|
2023
2217
|
return null;
|
|
@@ -2972,16 +3166,24 @@ var PipelineRunner = class {
|
|
|
2972
3166
|
handle
|
|
2973
3167
|
});
|
|
2974
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
|
+
*/
|
|
2975
3177
|
enqueueDetectionFrame(deviceId, frame, handle) {
|
|
2976
3178
|
const state = this.cameras.get(deviceId);
|
|
2977
|
-
if (!state) return;
|
|
3179
|
+
if (!state) return false;
|
|
2978
3180
|
this.arrivalCounts.set(deviceId, (this.arrivalCounts.get(deviceId) ?? 0) + 1);
|
|
2979
|
-
if (state.phase !== "active") return;
|
|
3181
|
+
if (state.phase !== "active") return false;
|
|
2980
3182
|
const interval = this.governor.minIntervalMs(deviceId);
|
|
2981
3183
|
if (interval > 0) {
|
|
2982
3184
|
const last = this.lastAdmittedAt.get(deviceId) ?? 0;
|
|
2983
3185
|
const now = Date.now();
|
|
2984
|
-
if (now - last < interval) return;
|
|
3186
|
+
if (now - last < interval) return false;
|
|
2985
3187
|
this.lastAdmittedAt.set(deviceId, now);
|
|
2986
3188
|
}
|
|
2987
3189
|
frame._enqueuedAt = Date.now();
|
|
@@ -2989,6 +3191,7 @@ var PipelineRunner = class {
|
|
|
2989
3191
|
frame,
|
|
2990
3192
|
handle
|
|
2991
3193
|
});
|
|
3194
|
+
return true;
|
|
2992
3195
|
}
|
|
2993
3196
|
/**
|
|
2994
3197
|
* `handle` is optional (mirrors {@link DetectionQueueEntry.handle}) — the
|
|
@@ -3000,13 +3203,14 @@ var PipelineRunner = class {
|
|
|
3000
3203
|
*/
|
|
3001
3204
|
enqueueOccupancyFrame(deviceId, frame, handle) {
|
|
3002
3205
|
const state = this.cameras.get(deviceId);
|
|
3003
|
-
if (!state) return;
|
|
3004
|
-
if (state.phase !== "watching") return;
|
|
3206
|
+
if (!state) return false;
|
|
3207
|
+
if (state.phase !== "watching") return false;
|
|
3005
3208
|
frame._enqueuedAt = Date.now();
|
|
3006
3209
|
state.detectionQueue.enqueue({
|
|
3007
3210
|
frame,
|
|
3008
3211
|
handle
|
|
3009
3212
|
});
|
|
3213
|
+
return true;
|
|
3010
3214
|
}
|
|
3011
3215
|
/**
|
|
3012
3216
|
* Report a motion event for a camera. Drives the unified phase
|
|
@@ -4769,13 +4973,38 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4769
4973
|
* resolve legitimately want the lower-RES honest frame).
|
|
4770
4974
|
*/
|
|
4771
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) {
|
|
4772
4999
|
const b = input.bbox;
|
|
4773
5000
|
const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
|
|
4774
5001
|
const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
|
|
4775
5002
|
let nativeResult = null;
|
|
4776
5003
|
let missReason = null;
|
|
5004
|
+
let handleAgeMs = null;
|
|
4777
5005
|
if (!isUnboundedFullFrame) {
|
|
4778
5006
|
const found = this.nativeCropRegistry.get(input.handle);
|
|
5007
|
+
handleAgeMs = found?.ageMs ?? null;
|
|
4779
5008
|
if (found) {
|
|
4780
5009
|
const result = await found.request(input.bbox, input.maxWidth);
|
|
4781
5010
|
if (result) nativeResult = {
|
|
@@ -4785,28 +5014,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4785
5014
|
};
|
|
4786
5015
|
else missReason = "worker-lease-gone";
|
|
4787
5016
|
} else missReason = "no-registry-entry";
|
|
4788
|
-
if (missReason !== null)
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
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
|
+
}
|
|
4799
5034
|
}
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
bytes: crop.bytes,
|
|
4807
|
-
width: crop.width,
|
|
4808
|
-
height: crop.height
|
|
4809
|
-
}, tier, input.encodeJpeg);
|
|
5035
|
+
return {
|
|
5036
|
+
pixels: nativeResult,
|
|
5037
|
+
tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
|
|
5038
|
+
missReason,
|
|
5039
|
+
handleAgeMs
|
|
5040
|
+
};
|
|
4810
5041
|
}
|
|
4811
5042
|
/**
|
|
4812
5043
|
* Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
|
|
@@ -4929,6 +5160,34 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4929
5160
|
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
4930
5161
|
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
4931
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
|
+
}
|
|
4932
5191
|
/** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
|
|
4933
5192
|
detailSubtreeLogger() {
|
|
4934
5193
|
return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
|
|
@@ -5011,18 +5270,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5011
5270
|
* lease (same path the `getNativeCrop` cap method above uses), encodes the
|
|
5012
5271
|
* raw RGB crop to JPEG, and denormalizes `paddedBbox` back to frame-space
|
|
5013
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.
|
|
5014
5278
|
*/
|
|
5015
5279
|
async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
|
|
5016
|
-
const
|
|
5280
|
+
const resolved = await this.resolveNativeCropPixels({
|
|
5017
5281
|
handle,
|
|
5018
|
-
bbox: paddedBbox
|
|
5019
|
-
encodeJpeg: true
|
|
5282
|
+
bbox: paddedBbox
|
|
5020
5283
|
});
|
|
5021
|
-
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
|
+
};
|
|
5022
5293
|
return {
|
|
5023
|
-
jpeg: Buffer.from(
|
|
5024
|
-
width:
|
|
5025
|
-
height:
|
|
5294
|
+
jpeg: Buffer.from(encoded.jpeg, "base64"),
|
|
5295
|
+
width: encoded.width,
|
|
5296
|
+
height: encoded.height,
|
|
5026
5297
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
5027
5298
|
};
|
|
5028
5299
|
}
|
|
@@ -5247,6 +5518,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5247
5518
|
* Kept as a method so the two call sites need no change.
|
|
5248
5519
|
*/
|
|
5249
5520
|
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
5521
|
+
await this.refreshNativeLeaseSettings();
|
|
5250
5522
|
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
5251
5523
|
deviceId,
|
|
5252
5524
|
streamId,
|
|
@@ -5288,8 +5560,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5288
5560
|
*/
|
|
5289
5561
|
enqueueSharedDetectionFrame(deviceId, frame, handle) {
|
|
5290
5562
|
const runner = this.runner;
|
|
5291
|
-
if (!runner) return;
|
|
5292
|
-
runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5563
|
+
if (!runner) return false;
|
|
5564
|
+
const admitted = runner.enqueueDetectionFrame(deviceId, frame, handle);
|
|
5293
5565
|
const sink = this.sharedMotionSinks.get(deviceId);
|
|
5294
5566
|
if (sink) {
|
|
5295
5567
|
const now = this.sharedSinkNow();
|
|
@@ -5298,6 +5570,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5298
5570
|
runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
|
|
5299
5571
|
}
|
|
5300
5572
|
}
|
|
5573
|
+
return admitted;
|
|
5301
5574
|
}
|
|
5302
5575
|
/**
|
|
5303
5576
|
* Epic C P1 detection path for a flagged camera: decode `rgb` in a
|
|
@@ -5353,7 +5626,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5353
5626
|
codec: source.codec
|
|
5354
5627
|
} });
|
|
5355
5628
|
return startSessionDecodePump({
|
|
5356
|
-
fork:
|
|
5629
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5357
5630
|
logger: log,
|
|
5358
5631
|
source,
|
|
5359
5632
|
opts: {
|
|
@@ -5365,7 +5638,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5365
5638
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
5366
5639
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5367
5640
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5368
|
-
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
5641
|
+
if (this.enqueueSharedDetectionFrame(config.deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5369
5642
|
}
|
|
5370
5643
|
});
|
|
5371
5644
|
}
|
|
@@ -5398,7 +5671,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5398
5671
|
codec: source.codec
|
|
5399
5672
|
} });
|
|
5400
5673
|
return startSessionDecodePump({
|
|
5401
|
-
fork:
|
|
5674
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5402
5675
|
logger: log,
|
|
5403
5676
|
source,
|
|
5404
5677
|
opts: {
|
|
@@ -5870,7 +6143,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5870
6143
|
logger: log,
|
|
5871
6144
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
5872
6145
|
startPump: (source) => startSessionDecodePump({
|
|
5873
|
-
fork:
|
|
6146
|
+
fork: this.forkDecodeWorkerWithLease,
|
|
5874
6147
|
logger: log,
|
|
5875
6148
|
source,
|
|
5876
6149
|
opts: {
|
|
@@ -5883,7 +6156,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5883
6156
|
if (settled) return;
|
|
5884
6157
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
5885
6158
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
|
|
5886
|
-
runner.enqueueOccupancyFrame(deviceId, frame, handle);
|
|
6159
|
+
if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
|
|
5887
6160
|
count++;
|
|
5888
6161
|
if (count >= frames) {
|
|
5889
6162
|
clearTimeout(safetyTimer);
|