@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.
Files changed (35) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-CILrD3sC.mjs → default-detection-model-DBqAX4cL.mjs} +853 -14
  4. package/dist/{default-detection-model-DdIJSdQB.js → default-detection-model-SkL7rw1J.js} +864 -13
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-3isAnyIX.mjs → dist-CTLBxswm.mjs} +3189 -2634
  8. package/dist/{dist-DXU3Fu7j.js → dist-DMwFmudw.js} +3189 -2634
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-PY3N7_ym.mjs → node-BO3NIRH9.mjs} +1 -1
  12. package/dist/{node-DEpHS48Q.js → node-CJqa8iTR.js} +1 -1
  13. package/dist/pipeline-runner/index.js +13 -821
  14. package/dist/pipeline-runner/index.mjs +12 -820
  15. package/dist/{process-memory-DAGs0yZp.js → process-memory-BLd-PX9x.js} +1 -1
  16. package/dist/{process-memory-DE0M2AiK.mjs → process-memory-qCTMcKBo.mjs} +1 -1
  17. package/dist/recorder/index.js +15 -8
  18. package/dist/recorder/index.mjs +15 -8
  19. package/dist/{segment-demux-js-k3D5kt3W.mjs → segment-demux-js-6HsaPjoo.mjs} +1 -1
  20. package/dist/{segment-demux-js-BZhn3qOl.js → segment-demux-js-DXHU1xgc.js} +1 -1
  21. package/dist/session-decode/decode-worker-main.js +194 -13
  22. package/dist/session-decode/decode-worker-main.mjs +194 -13
  23. package/dist/stream-broker/_stub.js +2 -2
  24. 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
  25. 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
  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-DGvtO8aA.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-BRiacIOY.mjs → hostInit-BHlXLPez.mjs} +2 -2
  30. package/dist/stream-broker/index.js +46 -12
  31. package/dist/stream-broker/index.mjs +46 -12
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. 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
  35. 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-3isAnyIX.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-PY3N7_ym.mjs";
3
- import { c as macroGateVerdict, d as resolveFrameViewGeometry, g as getStepDefinition, l as rootObjectGate, o as placeDetailStep, r as CustomModelCatalog, s as stepsForElectedPool, u as localFrameRegistry, v as RETIRED_MODEL_IDS } from "../default-detection-model-CILrD3sC.mjs";
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,