@camstack/addon-pipeline 1.2.317 → 1.2.321
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 +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-CILrD3sC.mjs → default-detection-model-DBqAX4cL.mjs} +853 -14
- package/dist/{default-detection-model-DdIJSdQB.js → default-detection-model-SkL7rw1J.js} +864 -13
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-3isAnyIX.mjs → dist-CTLBxswm.mjs} +3189 -2634
- package/dist/{dist-DXU3Fu7j.js → dist-DMwFmudw.js} +3189 -2634
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-PY3N7_ym.mjs → node-BO3NIRH9.mjs} +1 -1
- package/dist/{node-DEpHS48Q.js → node-CJqa8iTR.js} +1 -1
- package/dist/pipeline-runner/index.js +13 -821
- package/dist/pipeline-runner/index.mjs +12 -820
- package/dist/{process-memory-DAGs0yZp.js → process-memory-BLd-PX9x.js} +1 -1
- package/dist/{process-memory-DE0M2AiK.mjs → process-memory-qCTMcKBo.mjs} +1 -1
- package/dist/recorder/index.js +15 -8
- package/dist/recorder/index.mjs +15 -8
- package/dist/{segment-demux-js-k3D5kt3W.mjs → segment-demux-js-6HsaPjoo.mjs} +1 -1
- package/dist/{segment-demux-js-BZhn3qOl.js → segment-demux-js-DXHU1xgc.js} +1 -1
- package/dist/session-decode/decode-worker-main.js +194 -13
- package/dist/session-decode/decode-worker-main.mjs +194 -13
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DqP6Ev3L.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C1yya1ze.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-gPsQTvgB.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-DGvtO8aA.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-BRiacIOY.mjs → hostInit-BHlXLPez.mjs} +2 -2
- package/dist/stream-broker/index.js +46 -12
- package/dist/stream-broker/index.mjs +46 -12
- package/dist/stream-broker/remoteEntry.js +1 -1
- 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-CVT3OmxC.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DHNf76xj.mjs +0 -26
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { D as NativeLeaseAdmissionSchema, En as nodePin, Fn as lazy, In as literal, It as pipelineRunnerCapability, Ln as number, Mn as array, Nn as boolean, Nt as pickDetailCropConvention, O as NativeLeaseSettingsSchema, Pt as pickNativeLeaseOverride, Rn as object, Un as EventCategory, Vn as string, a as BackgroundRunRegistry, bn as defineCustomActions, bt as loadContributionCapability, fn as errMsg, ft as failureContributionCapability, g as DEFAULT_DETAIL_CROP_CONVENTION, jn as _enum, mn as BaseAddon, nt as deriveDetailCropRect, q as cameraOwnsMotionViaSensor, v as DEFAULT_NATIVE_LEASE_SETTINGS, vn as createEvent, w as FailureCounters, yn as customAction } from "../dist-
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
3
|
-
import { c as macroGateVerdict, d as
|
|
4
|
-
import { t as isWorkerReply } from "../worker-protocol-D_NzPcnh.mjs";
|
|
1
|
+
import { D as NativeLeaseAdmissionSchema, En as nodePin, Fn as lazy, In as literal, It as pipelineRunnerCapability, Ln as number, Mn as array, Nn as boolean, Nt as pickDetailCropConvention, O as NativeLeaseSettingsSchema, Pt as pickNativeLeaseOverride, Rn as object, Un as EventCategory, Vn as string, a as BackgroundRunRegistry, bn as defineCustomActions, bt as loadContributionCapability, fn as errMsg, ft as failureContributionCapability, g as DEFAULT_DETAIL_CROP_CONVENTION, jn as _enum, mn as BaseAddon, nt as deriveDetailCropRect, q as cameraOwnsMotionViaSensor, v as DEFAULT_NATIVE_LEASE_SETTINGS, vn as createEvent, w as FailureCounters, yn as customAction } from "../dist-CTLBxswm.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-BO3NIRH9.mjs";
|
|
3
|
+
import { b as RETIRED_MODEL_IDS, c as macroGateVerdict, d as isSessionDecodeReadGone, f as createSessionDecodeCoordinator, l as rootObjectGate, o as placeDetailStep, p as resolveFrameViewGeometry, r as CustomModelCatalog, s as stepsForElectedPool, u as localFrameRegistry, v as getStepDefinition } from "../default-detection-model-DBqAX4cL.mjs";
|
|
5
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
6
5
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
7
6
|
import { c as withDetectionIntent, o as preRollRequestOf, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
|
|
@@ -1751,822 +1750,6 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
1751
1750
|
};
|
|
1752
1751
|
}
|
|
1753
1752
|
//#endregion
|
|
1754
|
-
//#region src/session-decode/session-decode-coordinator.ts
|
|
1755
|
-
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
1756
|
-
var SIGKILL_FALLBACK_MS = 1e3;
|
|
1757
|
-
/**
|
|
1758
|
-
* The longest a planned teardown keeps a session's worker alive so the frames
|
|
1759
|
-
* it already handed out can be read (D652).
|
|
1760
|
-
*
|
|
1761
|
-
* What is being waited for is the REST of a frame's trip through the runner —
|
|
1762
|
-
* semaphore wait + inference + result — for frames that were admitted before
|
|
1763
|
-
* the detach. Measured on the live hub 2026-09-26 over one hour of
|
|
1764
|
-
* `pipeline stats` (2 254 per-camera 10 s windows): the WORST enqueue→emit in a
|
|
1765
|
-
* window was ≤1 000 ms in 96.7% of windows and ≤1 500 ms in 98.0%; going to
|
|
1766
|
-
* 5 000 ms buys only another 0.9%, and that tail is the inference-pool GPU hang,
|
|
1767
|
-
* which no wait here can rescue. The cost of waiting is one idle worker (its
|
|
1768
|
-
* decode already stopped by `drain`) for at most this long, per detach.
|
|
1769
|
-
*
|
|
1770
|
-
* A frame still held when this expires is not dropped silently: the
|
|
1771
|
-
* coordinator logs the count, tagged with the camera, and then kills.
|
|
1772
|
-
*/
|
|
1773
|
-
var SESSION_DECODE_DRAIN_BOUND_MS = 1500;
|
|
1774
|
-
/**
|
|
1775
|
-
* The miss reason for a native crop requested after its session's teardown.
|
|
1776
|
-
* Only crops already SENT when the drain began are waited for (D652); a later
|
|
1777
|
-
* one would ask a worker that is about to be killed.
|
|
1778
|
-
*/
|
|
1779
|
-
var NATIVE_CROP_REFUSED_DRAINING = "session draining — crop requested after teardown";
|
|
1780
|
-
/**
|
|
1781
|
-
* The refusal for a `toBuffer` asked for once the worker has been told to exit
|
|
1782
|
-
* (D656). A read during a DRAIN is served — that is what the drain is for — but
|
|
1783
|
-
* after the kill no reply can come, and a raw rejection read as a worker fault.
|
|
1784
|
-
*/
|
|
1785
|
-
var TO_BUFFER_REFUSED_KILLED = "session killed — read requested after the worker was told to exit";
|
|
1786
|
-
/** The refusal for a `toBuffer` the worker is no longer there to answer. */
|
|
1787
|
-
var TO_BUFFER_REFUSED_EXITED = "worker already exited";
|
|
1788
|
-
/** A read the session declined to send, by name — never a worker fault. */
|
|
1789
|
-
var SessionDecodeReadRefusedError = class extends Error {
|
|
1790
|
-
reason;
|
|
1791
|
-
frameId;
|
|
1792
|
-
constructor(reason, frameId) {
|
|
1793
|
-
super(`session-decode-coordinator: ${reason} (frameId=${frameId})`);
|
|
1794
|
-
this.reason = reason;
|
|
1795
|
-
this.frameId = frameId;
|
|
1796
|
-
this.name = "SessionDecodeReadRefusedError";
|
|
1797
|
-
}
|
|
1798
|
-
};
|
|
1799
|
-
/**
|
|
1800
|
-
* How often the cold-start wait REPORTS that it is still waiting. Task 0
|
|
1801
|
-
* live-measured cold-start (restream dial + hwaccel context + keyframe wait) at
|
|
1802
|
-
* 5-8s, so this must tolerate a slow-but-healthy start.
|
|
1803
|
-
*
|
|
1804
|
-
* It is no longer the point at which the session is abandoned — see
|
|
1805
|
-
* {@link DEFAULT_FIRST_FRAME_MAX_WAIT_MS}.
|
|
1806
|
-
*/
|
|
1807
|
-
var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
|
|
1808
|
-
/**
|
|
1809
|
-
* Hard ceiling on the TOTAL wait for the FIRST frame of a dial.
|
|
1810
|
-
*
|
|
1811
|
-
* The first decodable frame is not a fixed cost: it is one IDR interval plus
|
|
1812
|
-
* every GOP the decoder has to discard. Measured on device 618 (2560x1440
|
|
1813
|
-
* h264, `Invalid NAL unit` / `corrupt decoded frame` in its bitstream), dial →
|
|
1814
|
-
* first decoded frame over the broker's own muted restream was 1.8 / 6.5 / 7.0
|
|
1815
|
-
* / 7.2 / 9.2 / 10.0 / 10.8 / 12.9 / 14.9 / 20.0 / 30.3 / >40 s across 12
|
|
1816
|
-
* trials — against a healthy 587's 2.0 s ± 0.02.
|
|
1817
|
-
*
|
|
1818
|
-
* **The "8.0 s IDR interval" this used to attribute 618's spread to was wrong,
|
|
1819
|
-
* and is corrected here rather than deleted** (2026-08-28): a stated number
|
|
1820
|
-
* nobody re-derived reads as verification, and this one was used to argue that
|
|
1821
|
-
* 618 was an encoder outlier. The fleet runs 100-frame GOPs at 25 fps — a
|
|
1822
|
-
* **4 000 ms** IDR interval — and two independent readings of 6 h of live logs
|
|
1823
|
-
* agree with the operator rather than with this docblock. First, the join-burst
|
|
1824
|
-
* span logged on `restream prime (lat-probe)` is the time since the last IDR,
|
|
1825
|
-
* uniform in `[0, IDR]` and censored at `PRIME_MAX_SPAN_MS`: its median is
|
|
1826
|
-
* ~1 000 ms on eleven cameras, which is what `uniform[0, 4000]` conditioned on
|
|
1827
|
-
* `≤2000` predicts. Second, `detectionIntentServed` over detection dials puts
|
|
1828
|
-
* the SERVED fraction at 44-61 % on the bulk of the fleet, i.e. the ~50 %
|
|
1829
|
-
* `P(span > 2000)` a 4 000 ms interval implies. 618 measures 51 %, which is the
|
|
1830
|
-
* fleet norm. The genuine long-GOP outlier is **637** at 30 % served (⇒ ~6.7 s).
|
|
1831
|
-
*
|
|
1832
|
-
* So the spread is not this camera's encoder. It is D57's fixed 2 000 ms bound
|
|
1833
|
-
* withholding the join burst WHOLE on roughly half of every 4 s-GOP camera's
|
|
1834
|
-
* dials — see `join-burst-bound.ts`, which states the arithmetic, and
|
|
1835
|
-
* `docs/architecture/load-ledger.md` (2026-08-28) for the measurement.
|
|
1836
|
-
*
|
|
1837
|
-
* Abandoning at 20 s was therefore not a timeout, it was a coin flip, and the
|
|
1838
|
-
* recovery made it worse: ending the session RE-DIALS, which throws the
|
|
1839
|
-
* accumulated wait away and re-pays the RTSP setup + probe from zero, and two
|
|
1840
|
-
* ends inside 60 s DETACH the camera. That loop cost 618 25.9% of its assigned
|
|
1841
|
-
* decode airtime in one day (325 of 1257 ten-second windows delivered nothing).
|
|
1842
|
-
*
|
|
1843
|
-
* The wait is bounded instead of deadlined: the worker keeps the outstanding
|
|
1844
|
-
* `pull` latched, so re-arming costs nothing but the wait, and the ceiling is
|
|
1845
|
-
* what stops a genuinely dead input from holding a spawn permit forever.
|
|
1846
|
-
*/
|
|
1847
|
-
var DEFAULT_FIRST_FRAME_MAX_WAIT_MS = 45e3;
|
|
1848
|
-
/** Default timeout for every `pull` round-trip AFTER the first frame arrives. */
|
|
1849
|
-
var DEFAULT_FRAME_TIMEOUT_MS = 1e4;
|
|
1850
|
-
/** Narrow an 'exit' payload to {@link WorkerExitInfo} (fakes may emit undefined). */
|
|
1851
|
-
function toWorkerExitInfo(v) {
|
|
1852
|
-
const isInfo = (x) => typeof x === "object" && x !== null && "code" in x && "signal" in x;
|
|
1853
|
-
return isInfo(v) ? v : void 0;
|
|
1854
|
-
}
|
|
1855
|
-
function toError(err) {
|
|
1856
|
-
return err instanceof Error ? err : new Error(String(err));
|
|
1857
|
-
}
|
|
1858
|
-
/**
|
|
1859
|
-
* Per-session mutable state + the message-handling logic that adapts the
|
|
1860
|
-
* child's `WorkerReply` stream into resolved/rejected waiter promises. One
|
|
1861
|
-
* instance is created per `generateVideoFrames()` call (i.e. per forked
|
|
1862
|
-
* child) — never shared across sessions.
|
|
1863
|
-
*/
|
|
1864
|
-
var SessionDecodeSession = class {
|
|
1865
|
-
child;
|
|
1866
|
-
logger;
|
|
1867
|
-
deviceId;
|
|
1868
|
-
firstFrameTimeoutMs;
|
|
1869
|
-
frameTimeoutMs;
|
|
1870
|
-
firstFrameMaxWaitMs;
|
|
1871
|
-
hooks;
|
|
1872
|
-
pendingPull = null;
|
|
1873
|
-
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
1874
|
-
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
1875
|
-
pendingNativeCrops = /* @__PURE__ */ new Map();
|
|
1876
|
-
nextNativeCropId = 1;
|
|
1877
|
-
exited = false;
|
|
1878
|
-
/** Set by teardown() — the session is ending, whether or not it drains first. */
|
|
1879
|
-
stopRequested = false;
|
|
1880
|
-
/** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
|
|
1881
|
-
killSent = false;
|
|
1882
|
-
/**
|
|
1883
|
-
* A SIGKILL already went out — an `'immediate'` teardown from the pump
|
|
1884
|
-
* (D666) and the generator's later `finally` both escalate; one kill is enough.
|
|
1885
|
-
*/
|
|
1886
|
-
sigkillSent = false;
|
|
1887
|
-
/**
|
|
1888
|
-
* Images yielded and not yet closed that a CONSUMER still holds — queued,
|
|
1889
|
-
* in flight, or being read. Each dies with the worker and somebody was going
|
|
1890
|
-
* to read it first. This is what a drain waits on.
|
|
1891
|
-
*/
|
|
1892
|
-
consumerImages = 0;
|
|
1893
|
-
/**
|
|
1894
|
-
* Images yielded and not yet closed that only a RETENTION holder keeps —
|
|
1895
|
-
* parked for a read that may come later (the on-demand JPEG fallback, D656).
|
|
1896
|
-
* They die with the worker too, but nobody reads them inside any bound worth
|
|
1897
|
-
* holding a worker for, so a drain does NOT wait on them.
|
|
1898
|
-
*/
|
|
1899
|
-
retainedImages = 0;
|
|
1900
|
-
/** Armed while a drain waits; its expiry kills with frames still held. */
|
|
1901
|
-
drainTimer = null;
|
|
1902
|
-
sigkillTimer = null;
|
|
1903
|
-
pullTimeoutTimer = null;
|
|
1904
|
-
firstFrameReceived = false;
|
|
1905
|
-
/** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
|
|
1906
|
-
firstPullAt = 0;
|
|
1907
|
-
constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, hooks) {
|
|
1908
|
-
this.child = child;
|
|
1909
|
-
this.logger = logger;
|
|
1910
|
-
this.deviceId = deviceId;
|
|
1911
|
-
this.firstFrameTimeoutMs = firstFrameTimeoutMs;
|
|
1912
|
-
this.frameTimeoutMs = frameTimeoutMs;
|
|
1913
|
-
this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
|
|
1914
|
-
this.hooks = hooks;
|
|
1915
|
-
this.child.on("message", (m) => this.handleMessage(m));
|
|
1916
|
-
this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
|
|
1917
|
-
}
|
|
1918
|
-
start(source, opts, lease) {
|
|
1919
|
-
this.trySend({
|
|
1920
|
-
kind: "start",
|
|
1921
|
-
source,
|
|
1922
|
-
opts,
|
|
1923
|
-
lease
|
|
1924
|
-
});
|
|
1925
|
-
}
|
|
1926
|
-
/**
|
|
1927
|
-
* Send `pull` and resolve with the next `frame` or `ended` reply. Arms a
|
|
1928
|
-
* bounded liveness timeout for the round-trip: if the child stops
|
|
1929
|
-
* producing frames WITHOUT exiting, the timeout settles this pull as
|
|
1930
|
-
* `ended` so the caller's async generator resumes, runs its `finally`,
|
|
1931
|
-
* and tears the child down — see {@link handlePullTimeout}.
|
|
1932
|
-
*/
|
|
1933
|
-
pullNext() {
|
|
1934
|
-
if (this.exited) return Promise.resolve({ kind: "ended" });
|
|
1935
|
-
if (this.firstPullAt === 0) this.firstPullAt = Date.now();
|
|
1936
|
-
return new Promise((resolve, reject) => {
|
|
1937
|
-
this.pendingPull = {
|
|
1938
|
-
resolve,
|
|
1939
|
-
reject
|
|
1940
|
-
};
|
|
1941
|
-
this.armPullTimeout();
|
|
1942
|
-
try {
|
|
1943
|
-
this.child.send({ kind: "pull" });
|
|
1944
|
-
} catch (err) {
|
|
1945
|
-
this.clearPullTimeout();
|
|
1946
|
-
this.pendingPull = null;
|
|
1947
|
-
reject(toError(err));
|
|
1948
|
-
}
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
/** Send `toBuffer` for `frameId` and resolve with the matching `buffer` reply's bytes. */
|
|
1952
|
-
requestBuffer(frameId, opts) {
|
|
1953
|
-
if (this.exited) return Promise.reject(new SessionDecodeReadRefusedError(TO_BUFFER_REFUSED_EXITED, frameId));
|
|
1954
|
-
if (this.killSent) return Promise.reject(new SessionDecodeReadRefusedError(TO_BUFFER_REFUSED_KILLED, frameId));
|
|
1955
|
-
return new Promise((resolve, reject) => {
|
|
1956
|
-
const waiter = {
|
|
1957
|
-
resolve,
|
|
1958
|
-
reject
|
|
1959
|
-
};
|
|
1960
|
-
this.queueToBufferWaiter(frameId, waiter);
|
|
1961
|
-
try {
|
|
1962
|
-
this.child.send({
|
|
1963
|
-
kind: "toBuffer",
|
|
1964
|
-
frameId,
|
|
1965
|
-
opts
|
|
1966
|
-
});
|
|
1967
|
-
} catch (err) {
|
|
1968
|
-
this.removeToBufferWaiter(frameId, waiter);
|
|
1969
|
-
reject(toError(err));
|
|
1970
|
-
}
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
/**
|
|
1974
|
-
* Send `nativeCrop` for `frameId` and resolve with the crop result, or `null`
|
|
1975
|
-
* on any miss (frame aged out, worker error, or worker already exited). Never
|
|
1976
|
-
* rejects — native crops are strictly best-effort with a free fallback.
|
|
1977
|
-
*/
|
|
1978
|
-
requestNativeCrop(frameId, bbox, maxWidth) {
|
|
1979
|
-
if (this.exited) return Promise.resolve({ kind: "miss" });
|
|
1980
|
-
if (this.stopRequested) return Promise.resolve({
|
|
1981
|
-
kind: "miss",
|
|
1982
|
-
workerReason: NATIVE_CROP_REFUSED_DRAINING
|
|
1983
|
-
});
|
|
1984
|
-
const requestId = this.nextNativeCropId++;
|
|
1985
|
-
return new Promise((resolve) => {
|
|
1986
|
-
this.pendingNativeCrops.set(requestId, { resolve });
|
|
1987
|
-
try {
|
|
1988
|
-
this.child.send({
|
|
1989
|
-
kind: "nativeCrop",
|
|
1990
|
-
requestId,
|
|
1991
|
-
frameId,
|
|
1992
|
-
bbox,
|
|
1993
|
-
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
1994
|
-
});
|
|
1995
|
-
} catch {
|
|
1996
|
-
this.pendingNativeCrops.delete(requestId);
|
|
1997
|
-
resolve({ kind: "miss" });
|
|
1998
|
-
this.settleDrainIfDone();
|
|
1999
|
-
}
|
|
2000
|
-
});
|
|
2001
|
-
}
|
|
2002
|
-
/**
|
|
2003
|
-
* The runner has this frame's detections: cut one native subject tile per box
|
|
2004
|
-
* and END the hold on `frameId`. Fire and forget (no reply) so the encode can
|
|
2005
|
-
* never delay the runner; a dropped message costs only the tiles.
|
|
2006
|
-
*
|
|
2007
|
-
* An EMPTY box list is still sent — it is how a frame that detected nothing
|
|
2008
|
-
* releases its hold, and that is the common case.
|
|
2009
|
-
*/
|
|
2010
|
-
cutTiles(frameId, bboxes) {
|
|
2011
|
-
if (this.exited) return;
|
|
2012
|
-
this.trySend({
|
|
2013
|
-
kind: "cutTiles",
|
|
2014
|
-
frameId,
|
|
2015
|
-
bboxes
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
/**
|
|
2019
|
-
* Prompt-release the worker's RAM native-frame hold for `frameId`. Fire and
|
|
2020
|
-
* forget (no reply): the hold is also bounded by a frame count, so a dropped
|
|
2021
|
-
* release only defers reclaim. No-op once the worker has exited.
|
|
2022
|
-
*/
|
|
2023
|
-
releaseLease(frameId) {
|
|
2024
|
-
if (this.exited) return;
|
|
2025
|
-
this.trySend({
|
|
2026
|
-
kind: "releaseNativeLease",
|
|
2027
|
-
frameId
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
/**
|
|
2031
|
-
* Tell the worker `frameId` reached inference, so its native lease is worth
|
|
2032
|
-
* keeping. Fire and forget, no reply. Ordering matters and is free here: the
|
|
2033
|
-
* caller runs in the same turn that consumed the frame, and the pull that
|
|
2034
|
-
* decides this frame's admission is issued after that turn returns.
|
|
2035
|
-
*/
|
|
2036
|
-
markInferred(frameId) {
|
|
2037
|
-
if (this.exited) return;
|
|
2038
|
-
this.trySend({
|
|
2039
|
-
kind: "markInferred",
|
|
2040
|
-
frameId
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
|
-
/** A yielded image the consumer now holds — see {@link consumerImages}. */
|
|
2044
|
-
noteImageOpened() {
|
|
2045
|
-
this.consumerImages += 1;
|
|
2046
|
-
}
|
|
2047
|
-
/**
|
|
2048
|
-
* An open image moved between a consumer and a retention-only holder
|
|
2049
|
-
* (D656). Losing its last consumer may complete a waiting drain.
|
|
2050
|
-
*/
|
|
2051
|
-
noteImageHolder(pendingConsumer) {
|
|
2052
|
-
if (pendingConsumer) {
|
|
2053
|
-
this.retainedImages = Math.max(0, this.retainedImages - 1);
|
|
2054
|
-
this.consumerImages += 1;
|
|
2055
|
-
return;
|
|
2056
|
-
}
|
|
2057
|
-
this.consumerImages = Math.max(0, this.consumerImages - 1);
|
|
2058
|
-
this.retainedImages += 1;
|
|
2059
|
-
this.settleDrainIfDone();
|
|
2060
|
-
}
|
|
2061
|
-
/** A yielded image was closed; a waiting drain may now be done. */
|
|
2062
|
-
noteImageClosed(pendingConsumer) {
|
|
2063
|
-
if (pendingConsumer) this.consumerImages = Math.max(0, this.consumerImages - 1);
|
|
2064
|
-
else this.retainedImages = Math.max(0, this.retainedImages - 1);
|
|
2065
|
-
this.settleDrainIfDone();
|
|
2066
|
-
}
|
|
2067
|
-
/**
|
|
2068
|
-
* End the session.
|
|
2069
|
-
*
|
|
2070
|
-
* `'drain'` with frames still held: tell the worker to stop decoding (it
|
|
2071
|
-
* keeps every frame it holds), and kill only once every yielded image has
|
|
2072
|
-
* been closed and every read answered — or when
|
|
2073
|
-
* {@link SESSION_DECODE_DRAIN_BOUND_MS} expires, which is logged. Nothing
|
|
2074
|
-
* held: `stop` + SIGTERM now. `'immediate'`: SIGKILL now.
|
|
2075
|
-
*
|
|
2076
|
-
* A SECOND call that says `'immediate'` while a drain is in progress
|
|
2077
|
-
* ESCALATES (D652): addon shutdown lands on sessions already draining from
|
|
2078
|
-
* a detach, and the runner exits right after — a drain it does not wait for
|
|
2079
|
-
* is an orphaned worker holding its GPU pool. Any other repeat is ignored.
|
|
2080
|
-
*/
|
|
2081
|
-
teardown(mode) {
|
|
2082
|
-
if (this.stopRequested) {
|
|
2083
|
-
if (mode === "immediate") this.escalateToImmediate();
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
this.stopRequested = true;
|
|
2087
|
-
this.clearPullTimeout();
|
|
2088
|
-
if (this.exited) return;
|
|
2089
|
-
if (mode === "drain" && this.unsettledCount() > 0) {
|
|
2090
|
-
this.trySend({ kind: "drain" });
|
|
2091
|
-
this.drainTimer = setTimeout(() => this.handleDrainBoundExpired(), SESSION_DECODE_DRAIN_BOUND_MS);
|
|
2092
|
-
this.drainTimer.unref?.();
|
|
2093
|
-
return;
|
|
2094
|
-
}
|
|
2095
|
-
this.killNow(mode);
|
|
2096
|
-
}
|
|
2097
|
-
/** The drain (or a SIGTERM still waiting on its fallback) gives way to SIGKILL now. */
|
|
2098
|
-
escalateToImmediate() {
|
|
2099
|
-
if (this.exited || this.sigkillSent) return;
|
|
2100
|
-
if (this.drainTimer !== null) {
|
|
2101
|
-
clearTimeout(this.drainTimer);
|
|
2102
|
-
this.drainTimer = null;
|
|
2103
|
-
}
|
|
2104
|
-
if (!this.killSent) {
|
|
2105
|
-
this.killNow("immediate");
|
|
2106
|
-
return;
|
|
2107
|
-
}
|
|
2108
|
-
if (this.sigkillTimer !== null) {
|
|
2109
|
-
clearTimeout(this.sigkillTimer);
|
|
2110
|
-
this.sigkillTimer = null;
|
|
2111
|
-
}
|
|
2112
|
-
this.tryKill("SIGKILL");
|
|
2113
|
-
}
|
|
2114
|
-
/**
|
|
2115
|
-
* Frames a CONSUMER still holds, reads still waiting on the worker, and
|
|
2116
|
-
* native crops already REQUESTED. A crop asked for before the teardown
|
|
2117
|
-
* request is waited for; one asked for after it is refused by name — post-analysis's
|
|
2118
|
-
* ordinary crop arrives 2.2–9.9 s after the frame, past any bound worth
|
|
2119
|
-
* holding a worker for, and is served from subject tiles that die with the
|
|
2120
|
-
* worker either way.
|
|
2121
|
-
*
|
|
2122
|
-
* A frame only PARKED for such a late read ({@link retainedImages}) is not
|
|
2123
|
-
* counted, for the same reason (D656): counting it made nearly every
|
|
2124
|
-
* teardown wait the full bound for frames nobody was going to read. Its read,
|
|
2125
|
-
* once STARTED, is a pending read like any other and is waited for.
|
|
2126
|
-
*/
|
|
2127
|
-
unsettledCount() {
|
|
2128
|
-
return this.consumerImages + this.pendingReadCount() + this.pendingNativeCrops.size;
|
|
2129
|
-
}
|
|
2130
|
-
/** What the session still holds, per category — every drain line carries it. */
|
|
2131
|
-
heldCounts() {
|
|
2132
|
-
return {
|
|
2133
|
-
consumerFrames: this.consumerImages,
|
|
2134
|
-
retainedFrames: this.retainedImages,
|
|
2135
|
-
pendingReads: this.pendingReadCount(),
|
|
2136
|
-
pendingCrops: this.pendingNativeCrops.size
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
|
-
pendingReadCount() {
|
|
2140
|
-
let n = 0;
|
|
2141
|
-
for (const waiters of this.pendingToBuffers.values()) n += waiters.length;
|
|
2142
|
-
return n;
|
|
2143
|
-
}
|
|
2144
|
-
settleDrainIfDone() {
|
|
2145
|
-
if (this.drainTimer === null || this.unsettledCount() > 0) return;
|
|
2146
|
-
clearTimeout(this.drainTimer);
|
|
2147
|
-
this.drainTimer = null;
|
|
2148
|
-
this.killNow("drain");
|
|
2149
|
-
}
|
|
2150
|
-
/**
|
|
2151
|
-
* The drain ran out of time with frames still held. Those frames die with
|
|
2152
|
-
* the worker; that is a branch that DROPS work (D391), so it says how many,
|
|
2153
|
-
* on which camera.
|
|
2154
|
-
*/
|
|
2155
|
-
handleDrainBoundExpired() {
|
|
2156
|
-
this.drainTimer = null;
|
|
2157
|
-
if (this.exited) return;
|
|
2158
|
-
this.logger.warn("session-decode-coordinator: drain bound expired — killing the worker with frames still held", {
|
|
2159
|
-
tags: { deviceId: this.deviceId },
|
|
2160
|
-
meta: {
|
|
2161
|
-
...this.heldCounts(),
|
|
2162
|
-
boundMs: SESSION_DECODE_DRAIN_BOUND_MS
|
|
2163
|
-
}
|
|
2164
|
-
});
|
|
2165
|
-
this.killNow("drain");
|
|
2166
|
-
}
|
|
2167
|
-
/**
|
|
2168
|
-
* `stop`, then the kill — unless already exited.
|
|
2169
|
-
*
|
|
2170
|
-
* `'drain'`: SIGTERM, so the child flushes its last lines (`bounded-exit.ts`),
|
|
2171
|
-
* and a SIGKILL fallback. `'immediate'`: SIGKILL outright. Since D652 gave the
|
|
2172
|
-
* child a SIGTERM handler, SIGTERM is no longer instant death: a worker
|
|
2173
|
-
* blocked in a native call (a VAAPI hang) never runs the handler, and the
|
|
2174
|
-
* fallback was an `unref`'d timer the exiting parent did not wait for — so
|
|
2175
|
-
* on shutdown that worker was orphaned with its GPU pool. The fallback timer
|
|
2176
|
-
* is now ref'd: it exists only for a drain while the parent is alive (a
|
|
2177
|
-
* shutdown escalates to SIGKILL and clears it), and a timer that keeps the
|
|
2178
|
-
* process up for at most one second is the price of a kill that lands.
|
|
2179
|
-
*/
|
|
2180
|
-
killNow(mode) {
|
|
2181
|
-
if (this.killSent) return;
|
|
2182
|
-
this.killSent = true;
|
|
2183
|
-
if (this.retainedImages > 0) this.logger.debug("session-decode-coordinator: retained frames die with the worker", {
|
|
2184
|
-
tags: { deviceId: this.deviceId },
|
|
2185
|
-
meta: {
|
|
2186
|
-
mode,
|
|
2187
|
-
...this.heldCounts()
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
this.trySend({ kind: "stop" });
|
|
2191
|
-
if (mode === "immediate") {
|
|
2192
|
-
this.tryKill("SIGKILL");
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
this.tryKill(void 0);
|
|
2196
|
-
if (this.exited) return;
|
|
2197
|
-
this.sigkillTimer = setTimeout(() => {
|
|
2198
|
-
this.sigkillTimer = null;
|
|
2199
|
-
if (this.exited) return;
|
|
2200
|
-
this.logger.warn("session-decode-coordinator: child did not exit within SIGKILL fallback window — forcing", {
|
|
2201
|
-
tags: { deviceId: this.deviceId },
|
|
2202
|
-
meta: { fallbackMs: SIGKILL_FALLBACK_MS }
|
|
2203
|
-
});
|
|
2204
|
-
this.tryKill("SIGKILL");
|
|
2205
|
-
}, SIGKILL_FALLBACK_MS);
|
|
2206
|
-
}
|
|
2207
|
-
handleMessage(raw) {
|
|
2208
|
-
if (!isWorkerReply(raw)) {
|
|
2209
|
-
this.logger.warn("session-decode-coordinator: dropping malformed worker reply", { meta: { raw } });
|
|
2210
|
-
return;
|
|
2211
|
-
}
|
|
2212
|
-
switch (raw.kind) {
|
|
2213
|
-
case "frame":
|
|
2214
|
-
this.settlePendingPull({
|
|
2215
|
-
kind: "frame",
|
|
2216
|
-
reply: raw
|
|
2217
|
-
});
|
|
2218
|
-
return;
|
|
2219
|
-
case "ended":
|
|
2220
|
-
this.settlePendingPull({ kind: "ended" });
|
|
2221
|
-
return;
|
|
2222
|
-
case "buffer":
|
|
2223
|
-
this.settleToBuffer(raw.frameId, raw.bytes);
|
|
2224
|
-
return;
|
|
2225
|
-
case "nativeCropResult":
|
|
2226
|
-
this.settleNativeCrop(raw.requestId, {
|
|
2227
|
-
kind: "crop",
|
|
2228
|
-
crop: {
|
|
2229
|
-
bytes: raw.bytes,
|
|
2230
|
-
width: raw.width,
|
|
2231
|
-
height: raw.height,
|
|
2232
|
-
...raw.source ? { source: raw.source } : {}
|
|
2233
|
-
}
|
|
2234
|
-
});
|
|
2235
|
-
return;
|
|
2236
|
-
case "nativeCropMiss":
|
|
2237
|
-
if (raw.reason !== void 0) this.logger.debug("session-decode: native crop miss", { meta: {
|
|
2238
|
-
requestId: raw.requestId,
|
|
2239
|
-
reason: raw.reason
|
|
2240
|
-
} });
|
|
2241
|
-
this.settleNativeCrop(raw.requestId, {
|
|
2242
|
-
kind: "miss",
|
|
2243
|
-
...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
|
|
2244
|
-
});
|
|
2245
|
-
return;
|
|
2246
|
-
case "ready": return;
|
|
2247
|
-
case "error":
|
|
2248
|
-
this.settleError(raw.message, raw.frameId);
|
|
2249
|
-
return;
|
|
2250
|
-
case "log":
|
|
2251
|
-
this.logger[raw.level](raw.message);
|
|
2252
|
-
return;
|
|
2253
|
-
case "dialTiming":
|
|
2254
|
-
this.hooks.onDialTiming?.({
|
|
2255
|
-
hwContextMs: raw.hwContextMs,
|
|
2256
|
-
demuxerOpenMs: raw.demuxerOpenMs,
|
|
2257
|
-
decoderCreateMs: raw.decoderCreateMs,
|
|
2258
|
-
firstDecodeMs: raw.firstDecodeMs,
|
|
2259
|
-
streamInfoProbed: raw.streamInfoProbed
|
|
2260
|
-
});
|
|
2261
|
-
return;
|
|
2262
|
-
case "preRollSettled":
|
|
2263
|
-
this.hooks.onPreRollSettled?.({
|
|
2264
|
-
read: raw.read,
|
|
2265
|
-
released: raw.released,
|
|
2266
|
-
lostByReason: raw.lostByReason,
|
|
2267
|
-
unsettled: raw.unsettled
|
|
2268
|
-
});
|
|
2269
|
-
return;
|
|
2270
|
-
case "preRollReport":
|
|
2271
|
-
this.hooks.onPreRollReport?.({
|
|
2272
|
-
offered: raw.offered,
|
|
2273
|
-
inferred: raw.inferred,
|
|
2274
|
-
dropped: raw.dropped,
|
|
2275
|
-
droppedByReason: raw.droppedByReason,
|
|
2276
|
-
strideMs: raw.strideMs,
|
|
2277
|
-
subsampled: raw.subsampled,
|
|
2278
|
-
spanMs: raw.spanMs,
|
|
2279
|
-
budgetFrames: raw.budgetFrames
|
|
2280
|
-
});
|
|
2281
|
-
return;
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
handleExit(info) {
|
|
2285
|
-
if (!this.exited) this.hooks.onWorkerExit?.();
|
|
2286
|
-
if (!this.killSent && !this.exited && info?.killedByRunner === true) this.logger.debug("session-decode worker ended by the runner shutdown kill", {
|
|
2287
|
-
tags: { deviceId: this.deviceId },
|
|
2288
|
-
meta: {
|
|
2289
|
-
code: info.code,
|
|
2290
|
-
signal: info.signal
|
|
2291
|
-
}
|
|
2292
|
-
});
|
|
2293
|
-
else if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
|
|
2294
|
-
tags: { deviceId: this.deviceId },
|
|
2295
|
-
meta: {
|
|
2296
|
-
code: info?.code ?? null,
|
|
2297
|
-
signal: info?.signal ?? null,
|
|
2298
|
-
...this.stopRequested ? this.heldCounts() : {}
|
|
2299
|
-
}
|
|
2300
|
-
});
|
|
2301
|
-
this.exited = true;
|
|
2302
|
-
if (this.drainTimer) {
|
|
2303
|
-
clearTimeout(this.drainTimer);
|
|
2304
|
-
this.drainTimer = null;
|
|
2305
|
-
}
|
|
2306
|
-
if (this.sigkillTimer) {
|
|
2307
|
-
clearTimeout(this.sigkillTimer);
|
|
2308
|
-
this.sigkillTimer = null;
|
|
2309
|
-
}
|
|
2310
|
-
this.clearPullTimeout();
|
|
2311
|
-
if (this.pendingPull) {
|
|
2312
|
-
const waiter = this.pendingPull;
|
|
2313
|
-
this.pendingPull = null;
|
|
2314
|
-
waiter.resolve({ kind: "ended" });
|
|
2315
|
-
}
|
|
2316
|
-
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
|
|
2317
|
-
this.pendingToBuffers.clear();
|
|
2318
|
-
for (const waiter of this.pendingNativeCrops.values()) waiter.resolve({ kind: "miss" });
|
|
2319
|
-
this.pendingNativeCrops.clear();
|
|
2320
|
-
}
|
|
2321
|
-
/** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
|
|
2322
|
-
settleNativeCrop(requestId, attempt) {
|
|
2323
|
-
const waiter = this.pendingNativeCrops.get(requestId);
|
|
2324
|
-
if (!waiter) return;
|
|
2325
|
-
this.pendingNativeCrops.delete(requestId);
|
|
2326
|
-
waiter.resolve(attempt);
|
|
2327
|
-
this.settleDrainIfDone();
|
|
2328
|
-
}
|
|
2329
|
-
settlePendingPull(outcome) {
|
|
2330
|
-
const waiter = this.pendingPull;
|
|
2331
|
-
if (!waiter) {
|
|
2332
|
-
this.logger.debug("session-decode-coordinator: dropping unsolicited pull-shaped reply", { meta: { outcomeKind: outcome.kind } });
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
this.pendingPull = null;
|
|
2336
|
-
this.clearPullTimeout();
|
|
2337
|
-
if (outcome.kind === "frame") this.firstFrameReceived = true;
|
|
2338
|
-
waiter.resolve(outcome);
|
|
2339
|
-
}
|
|
2340
|
-
/**
|
|
2341
|
-
* Fires when a `pull` round-trip outlives its liveness timeout — the child
|
|
2342
|
-
* has not produced a frame and has not exited.
|
|
2343
|
-
*
|
|
2344
|
-
* BEFORE the first frame this is not yet a failure. The worker latches the
|
|
2345
|
-
* outstanding `pull` (`decode-worker-child.handlePull` sets `pendingPull` and
|
|
2346
|
-
* delivers on the next decoded frame), so re-arming the timer costs nothing
|
|
2347
|
-
* but the wait and the session keeps the dial, the RTSP setup and the probe
|
|
2348
|
-
* it has already paid for. Only the {@link firstFrameMaxWaitMs} ceiling ends
|
|
2349
|
-
* it — see that constant for the measurement that set it.
|
|
2350
|
-
*
|
|
2351
|
-
* AFTER the first frame the deadline is unchanged: a steady stream that stops
|
|
2352
|
-
* is a hung decode, and ending settles the outstanding pull as `ended` (not a
|
|
2353
|
-
* throw) so the consumer's async generator resumes cleanly, runs its
|
|
2354
|
-
* `finally`, and tears the child down via {@link teardown}.
|
|
2355
|
-
*
|
|
2356
|
-
* Every line here carries `tags: { deviceId }` and a `reason`, because
|
|
2357
|
-
* "detaching" a camera is the loudest dropped work in this path and an
|
|
2358
|
-
* operator has to be able to answer "why did 618 stop" from the logs alone.
|
|
2359
|
-
*/
|
|
2360
|
-
handlePullTimeout(timeoutMs) {
|
|
2361
|
-
this.pullTimeoutTimer = null;
|
|
2362
|
-
const waiter = this.pendingPull;
|
|
2363
|
-
if (!waiter) return;
|
|
2364
|
-
const waitedMs = this.firstPullAt === 0 ? timeoutMs : Date.now() - this.firstPullAt;
|
|
2365
|
-
if (!this.firstFrameReceived && waitedMs < this.firstFrameMaxWaitMs) {
|
|
2366
|
-
this.logger.warn("session-decode-coordinator: still waiting for its first decodable frame — holding the dial", {
|
|
2367
|
-
tags: { deviceId: this.deviceId },
|
|
2368
|
-
meta: {
|
|
2369
|
-
waitedMs,
|
|
2370
|
-
reportEveryMs: timeoutMs,
|
|
2371
|
-
maxWaitMs: this.firstFrameMaxWaitMs,
|
|
2372
|
-
hint: "long IDR interval or a discarded GOP looks exactly like this"
|
|
2373
|
-
}
|
|
2374
|
-
});
|
|
2375
|
-
this.armPullTimeout();
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
this.pendingPull = null;
|
|
2379
|
-
const reason = this.firstFrameReceived ? "stalled-after-first-frame" : "no-first-frame";
|
|
2380
|
-
this.logger.warn(this.firstFrameReceived ? "session-decode-coordinator: gave up waiting for the next frame — ending session" : "session-decode-coordinator: gave up waiting for the first decodable frame — ending session", {
|
|
2381
|
-
tags: { deviceId: this.deviceId },
|
|
2382
|
-
meta: {
|
|
2383
|
-
reason,
|
|
2384
|
-
waitedMs,
|
|
2385
|
-
timeoutMs,
|
|
2386
|
-
...this.firstFrameReceived ? {} : { maxWaitMs: this.firstFrameMaxWaitMs }
|
|
2387
|
-
}
|
|
2388
|
-
});
|
|
2389
|
-
waiter.resolve({ kind: "ended" });
|
|
2390
|
-
}
|
|
2391
|
-
/**
|
|
2392
|
-
* Arm the next round-trip deadline. Before the first frame the timer is
|
|
2393
|
-
* clamped to what is LEFT of the ceiling, so the give-up lands exactly on
|
|
2394
|
-
* {@link firstFrameMaxWaitMs} instead of on the next report boundary past it
|
|
2395
|
-
* — otherwise a 45 s ceiling reported every 20 s would really give up at 60 s.
|
|
2396
|
-
*/
|
|
2397
|
-
armPullTimeout() {
|
|
2398
|
-
const timeoutMs = this.firstFrameReceived ? this.frameTimeoutMs : Math.max(1, Math.min(this.firstFrameTimeoutMs, this.firstFrameMaxWaitMs - (this.firstPullAt === 0 ? 0 : Date.now() - this.firstPullAt)));
|
|
2399
|
-
this.pullTimeoutTimer = setTimeout(() => this.handlePullTimeout(timeoutMs), timeoutMs);
|
|
2400
|
-
this.pullTimeoutTimer.unref?.();
|
|
2401
|
-
}
|
|
2402
|
-
clearPullTimeout() {
|
|
2403
|
-
if (this.pullTimeoutTimer) {
|
|
2404
|
-
clearTimeout(this.pullTimeoutTimer);
|
|
2405
|
-
this.pullTimeoutTimer = null;
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
/**
|
|
2409
|
-
* Correlate by `frameId`: a `buffer` reply only ever settles waiters queued
|
|
2410
|
-
* under the SAME frameId — a late reply for a stale/superseded frame that
|
|
2411
|
-
* nobody is (still) awaiting is dropped, never mistakenly resolves a
|
|
2412
|
-
* different, newer frame's waiter.
|
|
2413
|
-
*/
|
|
2414
|
-
settleToBuffer(frameId, bytes) {
|
|
2415
|
-
const queue = this.pendingToBuffers.get(frameId);
|
|
2416
|
-
if (!queue || queue.length === 0) {
|
|
2417
|
-
this.logger.debug("session-decode-coordinator: dropping stale/unknown buffer reply", { meta: { frameId } });
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
const waiter = queue.shift();
|
|
2421
|
-
if (queue.length === 0) this.pendingToBuffers.delete(frameId);
|
|
2422
|
-
waiter?.resolve(bytes);
|
|
2423
|
-
this.settleDrainIfDone();
|
|
2424
|
-
}
|
|
2425
|
-
/**
|
|
2426
|
-
* `error` replies MAY carry a `frameId` (a `toBuffer`-scoped miss, e.g. a
|
|
2427
|
-
* stale/unknown frameId or a scale failure in `decode-worker-child.ts`) —
|
|
2428
|
-
* those fail ONLY that frame's `toBuffer` waiter(s) so the pull stays alive
|
|
2429
|
-
* and the generator keeps running (P2/I1: a single dropped frame must not
|
|
2430
|
-
* tear the whole session down). An error WITHOUT a `frameId` is a
|
|
2431
|
-
* session-level failure (decode-loop crash, dial error) and keeps the
|
|
2432
|
-
* original fatal behavior: fail the outstanding `pull` first (the more
|
|
2433
|
-
* common source), else fail every outstanding `toBuffer` waiter rather than
|
|
2434
|
-
* silently dropping the error and leaving a caller hanging forever.
|
|
2435
|
-
*/
|
|
2436
|
-
settleError(message, frameId) {
|
|
2437
|
-
const err = /* @__PURE__ */ new Error(`session-decode-coordinator: worker error — ${message}`);
|
|
2438
|
-
if (frameId !== void 0) {
|
|
2439
|
-
const queue = this.pendingToBuffers.get(frameId);
|
|
2440
|
-
if (queue && queue.length > 0) {
|
|
2441
|
-
for (const waiter of queue) waiter.reject(err);
|
|
2442
|
-
this.pendingToBuffers.delete(frameId);
|
|
2443
|
-
this.settleDrainIfDone();
|
|
2444
|
-
return;
|
|
2445
|
-
}
|
|
2446
|
-
this.logger.debug("session-decode-coordinator: dropping toBuffer error for unknown frameId", { meta: { frameId } });
|
|
2447
|
-
return;
|
|
2448
|
-
}
|
|
2449
|
-
if (this.pendingPull) {
|
|
2450
|
-
const waiter = this.pendingPull;
|
|
2451
|
-
this.pendingPull = null;
|
|
2452
|
-
this.clearPullTimeout();
|
|
2453
|
-
waiter.reject(err);
|
|
2454
|
-
return;
|
|
2455
|
-
}
|
|
2456
|
-
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(err);
|
|
2457
|
-
this.pendingToBuffers.clear();
|
|
2458
|
-
this.settleDrainIfDone();
|
|
2459
|
-
}
|
|
2460
|
-
queueToBufferWaiter(frameId, waiter) {
|
|
2461
|
-
const queue = this.pendingToBuffers.get(frameId);
|
|
2462
|
-
if (queue) queue.push(waiter);
|
|
2463
|
-
else this.pendingToBuffers.set(frameId, [waiter]);
|
|
2464
|
-
}
|
|
2465
|
-
removeToBufferWaiter(frameId, waiter) {
|
|
2466
|
-
const queue = this.pendingToBuffers.get(frameId);
|
|
2467
|
-
if (!queue) return;
|
|
2468
|
-
const idx = queue.indexOf(waiter);
|
|
2469
|
-
if (idx >= 0) queue.splice(idx, 1);
|
|
2470
|
-
if (queue.length === 0) this.pendingToBuffers.delete(frameId);
|
|
2471
|
-
}
|
|
2472
|
-
trySend(msg) {
|
|
2473
|
-
try {
|
|
2474
|
-
this.child.send(msg);
|
|
2475
|
-
} catch (err) {
|
|
2476
|
-
this.logger.warn("session-decode-coordinator: send to child failed", { meta: {
|
|
2477
|
-
requestKind: msg.kind,
|
|
2478
|
-
err: toError(err).message
|
|
2479
|
-
} });
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
tryKill(signal) {
|
|
2483
|
-
try {
|
|
2484
|
-
this.child.kill(signal);
|
|
2485
|
-
if (signal === "SIGKILL") this.sigkillSent = true;
|
|
2486
|
-
} catch (err) {
|
|
2487
|
-
this.logger.warn("session-decode-coordinator: kill() on child failed", {
|
|
2488
|
-
tags: { deviceId: this.deviceId },
|
|
2489
|
-
meta: {
|
|
2490
|
-
signal,
|
|
2491
|
-
err: toError(err).message
|
|
2492
|
-
}
|
|
2493
|
-
});
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
};
|
|
2497
|
-
/** Build the {@link FrameImage} adapter for one `frame` reply. */
|
|
2498
|
-
function buildFrameImage(session, reply) {
|
|
2499
|
-
let closed = false;
|
|
2500
|
-
let pendingConsumer = true;
|
|
2501
|
-
session.noteImageOpened();
|
|
2502
|
-
return {
|
|
2503
|
-
width: reply.width,
|
|
2504
|
-
height: reply.height,
|
|
2505
|
-
format: reply.format,
|
|
2506
|
-
frameId: reply.frameId,
|
|
2507
|
-
toBuffer: async (opts) => {
|
|
2508
|
-
if (closed) throw new Error(`session-decode-coordinator: toBuffer() called on a closed frame (frameId=${reply.frameId})`);
|
|
2509
|
-
return session.requestBuffer(reply.frameId, opts);
|
|
2510
|
-
},
|
|
2511
|
-
nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
|
|
2512
|
-
cutTiles: (bboxes) => session.cutTiles(reply.frameId, bboxes),
|
|
2513
|
-
releaseNativeLease: () => session.releaseLease(reply.frameId),
|
|
2514
|
-
markInferred: () => session.markInferred(reply.frameId),
|
|
2515
|
-
setPendingConsumer: (pending) => {
|
|
2516
|
-
if (closed || pending === pendingConsumer) return;
|
|
2517
|
-
pendingConsumer = pending;
|
|
2518
|
-
session.noteImageHolder(pending);
|
|
2519
|
-
},
|
|
2520
|
-
close: () => {
|
|
2521
|
-
if (closed) return;
|
|
2522
|
-
closed = true;
|
|
2523
|
-
session.noteImageClosed(pendingConsumer);
|
|
2524
|
-
}
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
/**
|
|
2528
|
-
* Create the `VideoFrameGenerator` adapter: forks one decode-worker child per
|
|
2529
|
-
* `generateVideoFrames()` call, drives it with `pull`/`toBuffer` requests
|
|
2530
|
-
* one at a time (no pre-fetch — back-pressure is the async generator's
|
|
2531
|
-
* natural suspend-on-yield), and guarantees the child is torn down
|
|
2532
|
-
* (bounded drain, then `stop` + `kill()` + SIGKILL fallback) in every exit
|
|
2533
|
-
* path via `finally`.
|
|
2534
|
-
*/
|
|
2535
|
-
function createSessionDecodeCoordinator(deps) {
|
|
2536
|
-
const firstFrameTimeoutMs = deps.firstFrameTimeoutMs ?? DEFAULT_FIRST_FRAME_TIMEOUT_MS;
|
|
2537
|
-
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
2538
|
-
const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
|
|
2539
|
-
return { async *generateVideoFrames(source, opts) {
|
|
2540
|
-
const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, {
|
|
2541
|
-
...deps.onDialTiming ? { onDialTiming: deps.onDialTiming } : {},
|
|
2542
|
-
...deps.onPreRollReport ? { onPreRollReport: deps.onPreRollReport } : {},
|
|
2543
|
-
...deps.onPreRollSettled ? { onPreRollSettled: deps.onPreRollSettled } : {},
|
|
2544
|
-
...deps.onWorkerExit ? { onWorkerExit: deps.onWorkerExit } : {}
|
|
2545
|
-
});
|
|
2546
|
-
session.start(source, opts, deps.lease());
|
|
2547
|
-
deps.onSessionTeardown?.((mode) => session.teardown(mode));
|
|
2548
|
-
let failed = false;
|
|
2549
|
-
try {
|
|
2550
|
-
while (true) {
|
|
2551
|
-
const outcome = await session.pullNext();
|
|
2552
|
-
if (outcome.kind === "ended") return;
|
|
2553
|
-
const { reply } = outcome;
|
|
2554
|
-
yield {
|
|
2555
|
-
timestamp: reply.timestamp,
|
|
2556
|
-
image: buildFrameImage(session, reply),
|
|
2557
|
-
...reply.capturedAt !== void 0 ? { capturedAt: reply.capturedAt } : {},
|
|
2558
|
-
...reply.preRoll === true ? { preRoll: true } : {}
|
|
2559
|
-
};
|
|
2560
|
-
}
|
|
2561
|
-
} catch (err) {
|
|
2562
|
-
failed = true;
|
|
2563
|
-
throw err;
|
|
2564
|
-
} finally {
|
|
2565
|
-
session.teardown(failed ? "immediate" : deps.teardownMode?.() ?? "drain");
|
|
2566
|
-
}
|
|
2567
|
-
} };
|
|
2568
|
-
}
|
|
2569
|
-
//#endregion
|
|
2570
1753
|
//#region src/session-decode/session-decode-pump.ts
|
|
2571
1754
|
/**
|
|
2572
1755
|
* Build a {@link DecodedFrame} from one pulled {@link VideoFrame}. Downscales to
|
|
@@ -11539,6 +10722,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
11539
10722
|
});
|
|
11540
10723
|
} catch (err) {
|
|
11541
10724
|
const msg = errMsg(err);
|
|
10725
|
+
if (isSessionDecodeReadGone(err)) {
|
|
10726
|
+
log.warn("runInference skipped — session decode frame gone", { meta: {
|
|
10727
|
+
error: msg,
|
|
10728
|
+
nodeId: this.nodeId,
|
|
10729
|
+
steps: dispatchSteps.map((s) => s.addonId).join(","),
|
|
10730
|
+
deviceKey: camConfig?.deviceKey ?? null
|
|
10731
|
+
} });
|
|
10732
|
+
return null;
|
|
10733
|
+
}
|
|
11542
10734
|
log.error("runInference failed", { meta: {
|
|
11543
10735
|
error: msg,
|
|
11544
10736
|
nodeId: this.nodeId,
|