@camstack/addon-pipeline 1.2.317 → 1.2.320

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