@camstack/addon-pipeline 1.2.49 → 1.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-J2DeRZAN.js → dist-BsBEe_rX.js} +67 -1
- package/dist/{dist-CLJKoYEP.mjs → dist-Dkvk5ykl.mjs} +62 -2
- package/dist/{event-loop-stall-monitor-B6dqQjiX.mjs → event-loop-stall-monitor-BvtjRcal.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-Bkbiw20z.js → event-loop-stall-monitor-DqJXi0_B.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +127 -114
- package/dist/pipeline-runner/index.mjs +127 -114
- package/dist/recorder/index.js +91 -4
- package/dist/recorder/index.mjs +91 -4
- package/dist/session-decode/decode-worker-child.js +57 -13
- package/dist/session-decode/decode-worker-child.mjs +57 -13
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DZikMEgD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CUZ0nS_r.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Mv3UeZUE.mjs +26 -0
- package/dist/stream-broker/{hostInit-i24FVz-3.mjs → hostInit-B8O23a6l.mjs} +2 -2
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BxPNiino.mjs → worker-protocol-B0c3i2km.mjs} +1 -1
- package/dist/{worker-protocol-CNi5srgV.js → worker-protocol-CWhkLkHx.js} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-m7YpaY98.js → MaskShapeCanvas-DI4BY7W2-kAtChdz2.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-619YTOjy.js → MotionZonesSettings-NcxxQN8r-DEwkvjzt.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-B-k8v0O5.js → PrivacyMaskSettings-APgPLF7p-Brv9VL0h.js} +1 -1
- package/embed-dist/assets/{index-CYO4OY8_.js → index-Dv7fSuDA.js} +12 -12
- package/embed-dist/index.html +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-Cj2xMupA.mjs +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-
|
|
3
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { A as defineCustomActions, Dt as object, Et as number, G as pipelineRunnerCapability, H as pickDetailCropConvention, O as customAction, St as boolean, Tt as literal, U as pickNativeLeaseOverride, bt as _enum, et as errMsg, ht as nodePin, j as deriveDetailCropRect, jt as EventCategory, kt as string, ot as BaseAddon, s as DEFAULT_DETAIL_CROP_CONVENTION, u as DEVICE_BACKEND_TO_FORMAT, ut as createEvent, wt as lazy, xt as array } from "../dist-Dkvk5ykl.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-BvtjRcal.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-B0c3i2km.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
6
6
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
@@ -1816,116 +1816,6 @@ var CropConventionSource = class {
|
|
|
1816
1816
|
}
|
|
1817
1817
|
};
|
|
1818
1818
|
//#endregion
|
|
1819
|
-
//#region src/pipeline-runner/native-lease-settings-source.ts
|
|
1820
|
-
/**
|
|
1821
|
-
* Where the runner gets the cluster-wide native-frame LEASE knobs.
|
|
1822
|
-
*
|
|
1823
|
-
* ## Why a settings read and not the attach config
|
|
1824
|
-
*
|
|
1825
|
-
* Identical reasoning to `crop-convention-source.ts`, and worth restating
|
|
1826
|
-
* because the temptation is the same: `RunnerCameraConfig` is the obvious
|
|
1827
|
-
* orchestrator→runner channel and it is the wrong one. Eleven call sites build
|
|
1828
|
-
* one; a cluster-wide constant threaded through eleven builders eventually
|
|
1829
|
-
* reaches ten of them, and the cameras attached by the forgotten builder just
|
|
1830
|
-
* quietly run a different lease window. Instead the runner reads the OWNER
|
|
1831
|
-
* addon's global settings directly, hub-routed by `AddonCallGateway`, so an
|
|
1832
|
-
* agent runner transparently reads the hub-central store with no placement
|
|
1833
|
-
* branching.
|
|
1834
|
-
*
|
|
1835
|
-
* ## Deliberately NOT node-scoped
|
|
1836
|
-
*
|
|
1837
|
-
* The read passes no `nodeId`. With one, `addon-settings` would project that
|
|
1838
|
-
* node's scoped value and two nodes could hold different lease windows for the
|
|
1839
|
-
* same camera — so whether a stored crop is native pixels or the ≤640 fallback
|
|
1840
|
-
* would depend on where the balancer happened to place it, which is invisible
|
|
1841
|
-
* from the stored media.
|
|
1842
|
-
*
|
|
1843
|
-
* ## What it hands out
|
|
1844
|
-
*
|
|
1845
|
-
* A {@link NativeLeaseSettingsOverride} — only the knobs the operator SET. The
|
|
1846
|
-
* decode worker resolves the rest (env var, then the shipped default); see
|
|
1847
|
-
* `session-decode/native-lease-config.ts` for the precedence. Handing out a
|
|
1848
|
-
* fully-resolved triple here would erase the "operator set nothing" state and
|
|
1849
|
-
* with it the env-var escape hatch.
|
|
1850
|
-
*/
|
|
1851
|
-
/**
|
|
1852
|
-
* The addon whose GLOBAL settings own the knobs. `pipeline-orchestrator` is
|
|
1853
|
-
* hub-resident and neutral — the same owner as the detail-crop convention and
|
|
1854
|
-
* the per-node decoder-backend selector.
|
|
1855
|
-
*/
|
|
1856
|
-
var NATIVE_LEASE_OWNER_ADDON_ID = "pipeline-orchestrator";
|
|
1857
|
-
/**
|
|
1858
|
-
* Whether two overrides name the same knobs with the same values.
|
|
1859
|
-
*
|
|
1860
|
-
* **Every knob has to appear here.** This is the equality that decides whether
|
|
1861
|
-
* a re-read is ADOPTED at all: a knob missing from the comparison is read from
|
|
1862
|
-
* the hub, judged identical, and discarded, so the operator's change never
|
|
1863
|
-
* reaches a decode worker and nothing anywhere says so. `admission` shipped
|
|
1864
|
-
* missing from this list and did exactly that — the setting was stored, the
|
|
1865
|
-
* hub served it, the runner fetched it, and every worker kept spawning with the
|
|
1866
|
-
* old value.
|
|
1867
|
-
*/
|
|
1868
|
-
function sameOverride(a, b) {
|
|
1869
|
-
return a.ttlMs === b.ttlMs && a.budgetMb === b.budgetMb && a.activityMs === b.activityMs && a.admission === b.admission;
|
|
1870
|
-
}
|
|
1871
|
-
/**
|
|
1872
|
-
* TTL-cached read of the cluster native-lease knobs.
|
|
1873
|
-
*
|
|
1874
|
-
* {@link current} is synchronous because the fork that consumes it is;
|
|
1875
|
-
* {@link refresh} is what talks to the hub, and the runner awaits it on the
|
|
1876
|
-
* path that precedes a decode-worker spawn.
|
|
1877
|
-
*/
|
|
1878
|
-
var NativeLeaseSettingsSource = class {
|
|
1879
|
-
override = {};
|
|
1880
|
-
lastReadAtMs = Number.NEGATIVE_INFINITY;
|
|
1881
|
-
inFlight = null;
|
|
1882
|
-
logger;
|
|
1883
|
-
now;
|
|
1884
|
-
ttlMs;
|
|
1885
|
-
constructor(options) {
|
|
1886
|
-
this.logger = options.logger;
|
|
1887
|
-
this.now = options.now ?? (() => Date.now());
|
|
1888
|
-
this.ttlMs = options.ttlMs ?? 3e4;
|
|
1889
|
-
}
|
|
1890
|
-
/** The operator's overrides as last read — `{}` until a read lands. */
|
|
1891
|
-
current() {
|
|
1892
|
-
return this.override;
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* Re-read if the cached value has expired. Concurrent callers share one
|
|
1896
|
-
* in-flight read; a failure keeps the LAST KNOWN value rather than reverting
|
|
1897
|
-
* to `{}`, because a revert would silently hand the next-spawned workers a
|
|
1898
|
-
* different RAM profile than the ones already running.
|
|
1899
|
-
*/
|
|
1900
|
-
async refresh(api) {
|
|
1901
|
-
if (api === void 0) return this.override;
|
|
1902
|
-
if (this.now() - this.lastReadAtMs < this.ttlMs) return this.override;
|
|
1903
|
-
this.inFlight ??= this.read(api).finally(() => {
|
|
1904
|
-
this.inFlight = null;
|
|
1905
|
-
});
|
|
1906
|
-
await this.inFlight;
|
|
1907
|
-
return this.override;
|
|
1908
|
-
}
|
|
1909
|
-
async read(api) {
|
|
1910
|
-
try {
|
|
1911
|
-
const next = pickNativeLeaseOverride(await api.addonSettings.getGlobalSettings.query({ addonId: NATIVE_LEASE_OWNER_ADDON_ID }));
|
|
1912
|
-
this.lastReadAtMs = this.now();
|
|
1913
|
-
if (sameOverride(next, this.override)) return;
|
|
1914
|
-
this.logger.info("native lease settings changed — applies to NEW decode sessions", { meta: {
|
|
1915
|
-
from: { ...this.override },
|
|
1916
|
-
to: { ...next }
|
|
1917
|
-
} });
|
|
1918
|
-
this.override = next;
|
|
1919
|
-
} catch (err) {
|
|
1920
|
-
this.logger.warn("native lease settings read failed — keeping the last known value", { meta: {
|
|
1921
|
-
owner: NATIVE_LEASE_OWNER_ADDON_ID,
|
|
1922
|
-
inUse: { ...this.override },
|
|
1923
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1924
|
-
} });
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
};
|
|
1928
|
-
//#endregion
|
|
1929
1819
|
//#region src/pipeline-runner/detail-subtree.ts
|
|
1930
1820
|
/** Did the native-lease fetch report a miss (rather than return pixels)? */
|
|
1931
1821
|
function isNativeCropMiss(fetch) {
|
|
@@ -2500,6 +2390,116 @@ function resolveNativeCropSource(isFullFrame, nativeHit) {
|
|
|
2500
2390
|
return isFullFrame ? "ram-fullframe" : "miss";
|
|
2501
2391
|
}
|
|
2502
2392
|
//#endregion
|
|
2393
|
+
//#region src/pipeline-runner/native-lease-settings-source.ts
|
|
2394
|
+
/**
|
|
2395
|
+
* Where the runner gets the cluster-wide native-frame LEASE knobs.
|
|
2396
|
+
*
|
|
2397
|
+
* ## Why a settings read and not the attach config
|
|
2398
|
+
*
|
|
2399
|
+
* Identical reasoning to `crop-convention-source.ts`, and worth restating
|
|
2400
|
+
* because the temptation is the same: `RunnerCameraConfig` is the obvious
|
|
2401
|
+
* orchestrator→runner channel and it is the wrong one. Eleven call sites build
|
|
2402
|
+
* one; a cluster-wide constant threaded through eleven builders eventually
|
|
2403
|
+
* reaches ten of them, and the cameras attached by the forgotten builder just
|
|
2404
|
+
* quietly run a different lease window. Instead the runner reads the OWNER
|
|
2405
|
+
* addon's global settings directly, hub-routed by `AddonCallGateway`, so an
|
|
2406
|
+
* agent runner transparently reads the hub-central store with no placement
|
|
2407
|
+
* branching.
|
|
2408
|
+
*
|
|
2409
|
+
* ## Deliberately NOT node-scoped
|
|
2410
|
+
*
|
|
2411
|
+
* The read passes no `nodeId`. With one, `addon-settings` would project that
|
|
2412
|
+
* node's scoped value and two nodes could hold different lease windows for the
|
|
2413
|
+
* same camera — so whether a stored crop is native pixels or the ≤640 fallback
|
|
2414
|
+
* would depend on where the balancer happened to place it, which is invisible
|
|
2415
|
+
* from the stored media.
|
|
2416
|
+
*
|
|
2417
|
+
* ## What it hands out
|
|
2418
|
+
*
|
|
2419
|
+
* A {@link NativeLeaseSettingsOverride} — only the knobs the operator SET. The
|
|
2420
|
+
* decode worker resolves the rest (env var, then the shipped default); see
|
|
2421
|
+
* `session-decode/native-lease-config.ts` for the precedence. Handing out a
|
|
2422
|
+
* fully-resolved triple here would erase the "operator set nothing" state and
|
|
2423
|
+
* with it the env-var escape hatch.
|
|
2424
|
+
*/
|
|
2425
|
+
/**
|
|
2426
|
+
* The addon whose GLOBAL settings own the knobs. `pipeline-orchestrator` is
|
|
2427
|
+
* hub-resident and neutral — the same owner as the detail-crop convention and
|
|
2428
|
+
* the per-node decoder-backend selector.
|
|
2429
|
+
*/
|
|
2430
|
+
var NATIVE_LEASE_OWNER_ADDON_ID = "pipeline-orchestrator";
|
|
2431
|
+
/**
|
|
2432
|
+
* Whether two overrides name the same knobs with the same values.
|
|
2433
|
+
*
|
|
2434
|
+
* **Every knob has to appear here.** This is the equality that decides whether
|
|
2435
|
+
* a re-read is ADOPTED at all: a knob missing from the comparison is read from
|
|
2436
|
+
* the hub, judged identical, and discarded, so the operator's change never
|
|
2437
|
+
* reaches a decode worker and nothing anywhere says so. `admission` shipped
|
|
2438
|
+
* missing from this list and did exactly that — the setting was stored, the
|
|
2439
|
+
* hub served it, the runner fetched it, and every worker kept spawning with the
|
|
2440
|
+
* old value.
|
|
2441
|
+
*/
|
|
2442
|
+
function sameOverride(a, b) {
|
|
2443
|
+
return a.ttlMs === b.ttlMs && a.budgetMb === b.budgetMb && a.activityMs === b.activityMs && a.admission === b.admission;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* TTL-cached read of the cluster native-lease knobs.
|
|
2447
|
+
*
|
|
2448
|
+
* {@link current} is synchronous because the fork that consumes it is;
|
|
2449
|
+
* {@link refresh} is what talks to the hub, and the runner awaits it on the
|
|
2450
|
+
* path that precedes a decode-worker spawn.
|
|
2451
|
+
*/
|
|
2452
|
+
var NativeLeaseSettingsSource = class {
|
|
2453
|
+
override = {};
|
|
2454
|
+
lastReadAtMs = Number.NEGATIVE_INFINITY;
|
|
2455
|
+
inFlight = null;
|
|
2456
|
+
logger;
|
|
2457
|
+
now;
|
|
2458
|
+
ttlMs;
|
|
2459
|
+
constructor(options) {
|
|
2460
|
+
this.logger = options.logger;
|
|
2461
|
+
this.now = options.now ?? (() => Date.now());
|
|
2462
|
+
this.ttlMs = options.ttlMs ?? 3e4;
|
|
2463
|
+
}
|
|
2464
|
+
/** The operator's overrides as last read — `{}` until a read lands. */
|
|
2465
|
+
current() {
|
|
2466
|
+
return this.override;
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Re-read if the cached value has expired. Concurrent callers share one
|
|
2470
|
+
* in-flight read; a failure keeps the LAST KNOWN value rather than reverting
|
|
2471
|
+
* to `{}`, because a revert would silently hand the next-spawned workers a
|
|
2472
|
+
* different RAM profile than the ones already running.
|
|
2473
|
+
*/
|
|
2474
|
+
async refresh(api) {
|
|
2475
|
+
if (api === void 0) return this.override;
|
|
2476
|
+
if (this.now() - this.lastReadAtMs < this.ttlMs) return this.override;
|
|
2477
|
+
this.inFlight ??= this.read(api).finally(() => {
|
|
2478
|
+
this.inFlight = null;
|
|
2479
|
+
});
|
|
2480
|
+
await this.inFlight;
|
|
2481
|
+
return this.override;
|
|
2482
|
+
}
|
|
2483
|
+
async read(api) {
|
|
2484
|
+
try {
|
|
2485
|
+
const next = pickNativeLeaseOverride(await api.addonSettings.getGlobalSettings.query({ addonId: NATIVE_LEASE_OWNER_ADDON_ID }));
|
|
2486
|
+
this.lastReadAtMs = this.now();
|
|
2487
|
+
if (sameOverride(next, this.override)) return;
|
|
2488
|
+
this.logger.info("native lease settings changed — applies to NEW decode sessions", { meta: {
|
|
2489
|
+
from: { ...this.override },
|
|
2490
|
+
to: { ...next }
|
|
2491
|
+
} });
|
|
2492
|
+
this.override = next;
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
this.logger.warn("native lease settings read failed — keeping the last known value", { meta: {
|
|
2495
|
+
owner: NATIVE_LEASE_OWNER_ADDON_ID,
|
|
2496
|
+
inUse: { ...this.override },
|
|
2497
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2498
|
+
} });
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
//#endregion
|
|
2503
2503
|
//#region src/pipeline-runner/frame-governor.ts
|
|
2504
2504
|
var FrameGovernor = class {
|
|
2505
2505
|
config;
|
|
@@ -4978,7 +4978,20 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4978
4978
|
if (resolved.tier === "native" && resolved.pixels !== null) return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
|
|
4979
4979
|
if (resolved.tier !== "ram-fullframe") return null;
|
|
4980
4980
|
const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
|
|
4981
|
-
if (!crop)
|
|
4981
|
+
if (!crop) {
|
|
4982
|
+
this.ctx?.logger.warn("native full-frame miss — the retained store no longer holds this frame", {
|
|
4983
|
+
tags: { deviceId: this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
|
|
4984
|
+
meta: {
|
|
4985
|
+
handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
|
|
4986
|
+
handleNodeId: input.handle.nodeId,
|
|
4987
|
+
nativeMissReason: resolved.missReason,
|
|
4988
|
+
nativeHandleAgeMs: resolved.handleAgeMs,
|
|
4989
|
+
maxWidth: input.maxWidth ?? null,
|
|
4990
|
+
hint: "both the native lease and the retained-frame window have closed for this frame — the detail dispatch that asked for it produces NO crop"
|
|
4991
|
+
}
|
|
4992
|
+
});
|
|
4993
|
+
return null;
|
|
4994
|
+
}
|
|
4982
4995
|
return this.encodeNativeCropResult({
|
|
4983
4996
|
bytes: crop.bytes,
|
|
4984
4997
|
width: crop.width,
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-BsBEe_rX.js");
|
|
3
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
4
|
const require_addon_utils = require("../addon-utils-DveoBR6G.js");
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
@@ -3489,12 +3489,56 @@ function activeBandAt(config, date) {
|
|
|
3489
3489
|
//#endregion
|
|
3490
3490
|
//#region src/recorder/addon/band-decision.ts
|
|
3491
3491
|
/**
|
|
3492
|
+
* Pure glue between a device's `RecordingConfig` and the v2 band engine: decide
|
|
3493
|
+
* whether recording should currently be running, and which finalized segments
|
|
3494
|
+
* an `events` band retains.
|
|
3495
|
+
*
|
|
3496
|
+
* The decision is enabled AND the band active at `now` (per the v2
|
|
3497
|
+
* `activeBandAt`/`bandActiveAt`) demands recording: a `continuous` band always
|
|
3498
|
+
* demands; an `events` band demands only within `postBufferSec` of a qualifying
|
|
3499
|
+
* trigger (B3). No bands / disabled => not recording.
|
|
3500
|
+
*
|
|
3501
|
+
* `preBufferSec` is retroactive — it is NOT a live-demand input; it retains
|
|
3502
|
+
* already-recorded segments BEFORE a trigger at keep/discard time
|
|
3503
|
+
* ({@link segmentRetainedForEvents}). This mirrors the old recorder's proven
|
|
3504
|
+
* policy split (start-on-trigger live gate + retroactive pre-buffer keep).
|
|
3505
|
+
*
|
|
3506
|
+
* Kept pure (clock + trigger state injected) so the attach/detach decision is
|
|
3507
|
+
* unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
|
|
3508
|
+
*/
|
|
3509
|
+
/**
|
|
3510
|
+
* THE one place a band's pre/post buffers are read. An ABSENT field takes
|
|
3511
|
+
* {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
|
|
3512
|
+
* is honoured verbatim.
|
|
3513
|
+
*
|
|
3514
|
+
* Both callers below used to inline `?? 0`, which read "the operator omitted the
|
|
3515
|
+
* field" as "the operator asked for a zero-length window". For the live gate
|
|
3516
|
+
* that is not a policy, it is an impossibility: nothing can ever be recorded.
|
|
3517
|
+
* Two cameras sat in exactly that state.
|
|
3518
|
+
*/
|
|
3519
|
+
function resolveBandBufferMs(band) {
|
|
3520
|
+
return {
|
|
3521
|
+
preMs: (band.preBufferSec ?? require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec) * 1e3,
|
|
3522
|
+
postMs: (band.postBufferSec ?? require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec) * 1e3
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Can this band EVER open a live recording window? False for an `events` band
|
|
3527
|
+
* whose resolved post-buffer is zero (only reachable by authoring an explicit
|
|
3528
|
+
* `0`) or which lists no trigger at all: both are "enabled, and records nothing,
|
|
3529
|
+
* forever". The recorder warns on it rather than dropping the device silently.
|
|
3530
|
+
*/
|
|
3531
|
+
function eventsBandCanEverDemand(band) {
|
|
3532
|
+
if (band.mode !== "events") return true;
|
|
3533
|
+
return (band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0) && resolveBandBufferMs(band).postMs > 0;
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3492
3536
|
* Is an `events` band's live demand window open at `atMs`? True iff a trigger
|
|
3493
3537
|
* the band listens for fired within `postBufferSec` of now. `preBufferSec` is
|
|
3494
3538
|
* deliberately NOT consulted here (see file header).
|
|
3495
3539
|
*/
|
|
3496
3540
|
function eventsBandDemanded(band, triggers, atMs) {
|
|
3497
|
-
const postMs = (band
|
|
3541
|
+
const { postMs } = resolveBandBufferMs(band);
|
|
3498
3542
|
const fresh = (lastMs) => lastMs != null && atMs - lastMs <= postMs;
|
|
3499
3543
|
if (band.triggers?.motion && fresh(triggers.lastMotionMs)) return true;
|
|
3500
3544
|
if (band.triggers?.audioThresholdDbfs != null && fresh(triggers.lastAudioMs)) return true;
|
|
@@ -3521,8 +3565,7 @@ function shouldRecordAt(config, triggers, at) {
|
|
|
3521
3565
|
* This is where `preBufferSec` retroactively retains pre-trigger footage.
|
|
3522
3566
|
*/
|
|
3523
3567
|
function segmentRetainedForEvents(segStartMs, segEndMs, band, triggers) {
|
|
3524
|
-
const preMs = (band
|
|
3525
|
-
const postMs = (band.postBufferSec ?? 0) * 1e3;
|
|
3568
|
+
const { preMs, postMs } = resolveBandBufferMs(band);
|
|
3526
3569
|
const overlaps = (lastMs) => lastMs != null && segStartMs <= lastMs + postMs && segEndMs >= lastMs - preMs;
|
|
3527
3570
|
if (band.triggers?.motion && overlaps(triggers.lastMotionMs)) return true;
|
|
3528
3571
|
if (band.triggers?.audioThresholdDbfs != null && overlaps(triggers.lastAudioMs)) return true;
|
|
@@ -4055,6 +4098,12 @@ var RecordingController = class {
|
|
|
4055
4098
|
* alive, which is the only question here.
|
|
4056
4099
|
*/
|
|
4057
4100
|
lastSegmentAt = /* @__PURE__ */ new Map();
|
|
4101
|
+
/**
|
|
4102
|
+
* Devices already named by {@link reportUnrecordable} — one warn per device
|
|
4103
|
+
* per "enabled but can never record" episode, re-armed as soon as the device
|
|
4104
|
+
* records again.
|
|
4105
|
+
*/
|
|
4106
|
+
unrecordableReported = /* @__PURE__ */ new Set();
|
|
4058
4107
|
restore = null;
|
|
4059
4108
|
tickTimer = null;
|
|
4060
4109
|
stopped = false;
|
|
@@ -4231,10 +4280,12 @@ var RecordingController = class {
|
|
|
4231
4280
|
if (this.stopped) return;
|
|
4232
4281
|
const config = await this.deps.loadConfig(deviceId);
|
|
4233
4282
|
if (!shouldRecordAt(config, this.triggersFor(deviceId), new Date(this.now()))) {
|
|
4283
|
+
this.reportUnrecordable(deviceId, config);
|
|
4234
4284
|
await this.detachDevice(deviceId);
|
|
4235
4285
|
this.restore?.remove(deviceId);
|
|
4236
4286
|
return;
|
|
4237
4287
|
}
|
|
4288
|
+
this.unrecordableReported.delete(deviceId);
|
|
4238
4289
|
if (this.active.has(deviceId)) return;
|
|
4239
4290
|
if (this.attaching.has(deviceId)) return;
|
|
4240
4291
|
this.attaching.add(deviceId);
|
|
@@ -4245,6 +4296,42 @@ var RecordingController = class {
|
|
|
4245
4296
|
}
|
|
4246
4297
|
}
|
|
4247
4298
|
/**
|
|
4299
|
+
* Name the `!wantRecording` drop when the device is ENABLED but its config can
|
|
4300
|
+
* never produce footage — the branch that hid this bug. Two shapes qualify,
|
|
4301
|
+
* and both mean "the operator asked for recording and will get none, forever":
|
|
4302
|
+
* an enabled config with no bands at all, and an `events` band that cannot
|
|
4303
|
+
* open a live window (no trigger listed, or an explicit `postBufferSec: 0`).
|
|
4304
|
+
*
|
|
4305
|
+
* NOT reported: a band that simply does not cover `now` (a night-only band at
|
|
4306
|
+
* noon), or an events band waiting for its next trigger. Those are the
|
|
4307
|
+
* feature working.
|
|
4308
|
+
*
|
|
4309
|
+
* One line per device until it records again ({@link unrecordableReported} is
|
|
4310
|
+
* cleared the moment `wantRecording` turns true), so the 30 s tick cannot turn
|
|
4311
|
+
* this into a log flood.
|
|
4312
|
+
*/
|
|
4313
|
+
reportUnrecordable(deviceId, config) {
|
|
4314
|
+
if (!config.enabled) return;
|
|
4315
|
+
if (this.unrecordableReported.has(deviceId)) return;
|
|
4316
|
+
const bands = config.bands ?? [];
|
|
4317
|
+
if (bands.length === 0) {
|
|
4318
|
+
this.unrecordableReported.add(deviceId);
|
|
4319
|
+
this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
const band = activeBandAt({ bands }, new Date(this.now()));
|
|
4323
|
+
if (band === null || eventsBandCanEverDemand(band)) return;
|
|
4324
|
+
this.unrecordableReported.add(deviceId);
|
|
4325
|
+
this.deps.logger.warn("recorder: events band can never open a recording window — nothing will ever be recorded", {
|
|
4326
|
+
tags: { deviceId },
|
|
4327
|
+
meta: {
|
|
4328
|
+
postBufferSec: band.postBufferSec,
|
|
4329
|
+
triggers: band.triggers,
|
|
4330
|
+
reason: band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0 ? "postBufferSec is 0" : "no trigger configured"
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
/**
|
|
4248
4335
|
* The attach body — extracted so the {@link attaching} in-flight guard in
|
|
4249
4336
|
* `evaluateDevice` wraps it cleanly. Resolves the profiles, spawns a
|
|
4250
4337
|
* SegmentWriter + watcher per profile, and records the set in `active`. A throw
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { K as recordingCapability, M as deriveRecordingMode, Ot as record, X as storageEvictableCapability, _ as OpsLogEntrySchema, b as RecordingConfigSchema, c as DEFAULT_EVENTS_BAND_BUFFER_SEC, d as EVENT_PAD_MS, dt as hydrateSchema, et as errMsg, ht as nodePin, jt as EventCategory, kt as string, lt as DeviceType, ot as BaseAddon, p as ExportRecordSchema, q as recordingExportCapability, vt as selectAssignedProfileSlots, xt as array } from "../dist-Dkvk5ykl.mjs";
|
|
2
2
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
3
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-QuxtOdK1.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -3487,12 +3487,56 @@ function activeBandAt(config, date) {
|
|
|
3487
3487
|
//#endregion
|
|
3488
3488
|
//#region src/recorder/addon/band-decision.ts
|
|
3489
3489
|
/**
|
|
3490
|
+
* Pure glue between a device's `RecordingConfig` and the v2 band engine: decide
|
|
3491
|
+
* whether recording should currently be running, and which finalized segments
|
|
3492
|
+
* an `events` band retains.
|
|
3493
|
+
*
|
|
3494
|
+
* The decision is enabled AND the band active at `now` (per the v2
|
|
3495
|
+
* `activeBandAt`/`bandActiveAt`) demands recording: a `continuous` band always
|
|
3496
|
+
* demands; an `events` band demands only within `postBufferSec` of a qualifying
|
|
3497
|
+
* trigger (B3). No bands / disabled => not recording.
|
|
3498
|
+
*
|
|
3499
|
+
* `preBufferSec` is retroactive — it is NOT a live-demand input; it retains
|
|
3500
|
+
* already-recorded segments BEFORE a trigger at keep/discard time
|
|
3501
|
+
* ({@link segmentRetainedForEvents}). This mirrors the old recorder's proven
|
|
3502
|
+
* policy split (start-on-trigger live gate + retroactive pre-buffer keep).
|
|
3503
|
+
*
|
|
3504
|
+
* Kept pure (clock + trigger state injected) so the attach/detach decision is
|
|
3505
|
+
* unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
|
|
3506
|
+
*/
|
|
3507
|
+
/**
|
|
3508
|
+
* THE one place a band's pre/post buffers are read. An ABSENT field takes
|
|
3509
|
+
* {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
|
|
3510
|
+
* is honoured verbatim.
|
|
3511
|
+
*
|
|
3512
|
+
* Both callers below used to inline `?? 0`, which read "the operator omitted the
|
|
3513
|
+
* field" as "the operator asked for a zero-length window". For the live gate
|
|
3514
|
+
* that is not a policy, it is an impossibility: nothing can ever be recorded.
|
|
3515
|
+
* Two cameras sat in exactly that state.
|
|
3516
|
+
*/
|
|
3517
|
+
function resolveBandBufferMs(band) {
|
|
3518
|
+
return {
|
|
3519
|
+
preMs: (band.preBufferSec ?? DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec) * 1e3,
|
|
3520
|
+
postMs: (band.postBufferSec ?? DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec) * 1e3
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Can this band EVER open a live recording window? False for an `events` band
|
|
3525
|
+
* whose resolved post-buffer is zero (only reachable by authoring an explicit
|
|
3526
|
+
* `0`) or which lists no trigger at all: both are "enabled, and records nothing,
|
|
3527
|
+
* forever". The recorder warns on it rather than dropping the device silently.
|
|
3528
|
+
*/
|
|
3529
|
+
function eventsBandCanEverDemand(band) {
|
|
3530
|
+
if (band.mode !== "events") return true;
|
|
3531
|
+
return (band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0) && resolveBandBufferMs(band).postMs > 0;
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3490
3534
|
* Is an `events` band's live demand window open at `atMs`? True iff a trigger
|
|
3491
3535
|
* the band listens for fired within `postBufferSec` of now. `preBufferSec` is
|
|
3492
3536
|
* deliberately NOT consulted here (see file header).
|
|
3493
3537
|
*/
|
|
3494
3538
|
function eventsBandDemanded(band, triggers, atMs) {
|
|
3495
|
-
const postMs = (band
|
|
3539
|
+
const { postMs } = resolveBandBufferMs(band);
|
|
3496
3540
|
const fresh = (lastMs) => lastMs != null && atMs - lastMs <= postMs;
|
|
3497
3541
|
if (band.triggers?.motion && fresh(triggers.lastMotionMs)) return true;
|
|
3498
3542
|
if (band.triggers?.audioThresholdDbfs != null && fresh(triggers.lastAudioMs)) return true;
|
|
@@ -3519,8 +3563,7 @@ function shouldRecordAt(config, triggers, at) {
|
|
|
3519
3563
|
* This is where `preBufferSec` retroactively retains pre-trigger footage.
|
|
3520
3564
|
*/
|
|
3521
3565
|
function segmentRetainedForEvents(segStartMs, segEndMs, band, triggers) {
|
|
3522
|
-
const preMs = (band
|
|
3523
|
-
const postMs = (band.postBufferSec ?? 0) * 1e3;
|
|
3566
|
+
const { preMs, postMs } = resolveBandBufferMs(band);
|
|
3524
3567
|
const overlaps = (lastMs) => lastMs != null && segStartMs <= lastMs + postMs && segEndMs >= lastMs - preMs;
|
|
3525
3568
|
if (band.triggers?.motion && overlaps(triggers.lastMotionMs)) return true;
|
|
3526
3569
|
if (band.triggers?.audioThresholdDbfs != null && overlaps(triggers.lastAudioMs)) return true;
|
|
@@ -4053,6 +4096,12 @@ var RecordingController = class {
|
|
|
4053
4096
|
* alive, which is the only question here.
|
|
4054
4097
|
*/
|
|
4055
4098
|
lastSegmentAt = /* @__PURE__ */ new Map();
|
|
4099
|
+
/**
|
|
4100
|
+
* Devices already named by {@link reportUnrecordable} — one warn per device
|
|
4101
|
+
* per "enabled but can never record" episode, re-armed as soon as the device
|
|
4102
|
+
* records again.
|
|
4103
|
+
*/
|
|
4104
|
+
unrecordableReported = /* @__PURE__ */ new Set();
|
|
4056
4105
|
restore = null;
|
|
4057
4106
|
tickTimer = null;
|
|
4058
4107
|
stopped = false;
|
|
@@ -4229,10 +4278,12 @@ var RecordingController = class {
|
|
|
4229
4278
|
if (this.stopped) return;
|
|
4230
4279
|
const config = await this.deps.loadConfig(deviceId);
|
|
4231
4280
|
if (!shouldRecordAt(config, this.triggersFor(deviceId), new Date(this.now()))) {
|
|
4281
|
+
this.reportUnrecordable(deviceId, config);
|
|
4232
4282
|
await this.detachDevice(deviceId);
|
|
4233
4283
|
this.restore?.remove(deviceId);
|
|
4234
4284
|
return;
|
|
4235
4285
|
}
|
|
4286
|
+
this.unrecordableReported.delete(deviceId);
|
|
4236
4287
|
if (this.active.has(deviceId)) return;
|
|
4237
4288
|
if (this.attaching.has(deviceId)) return;
|
|
4238
4289
|
this.attaching.add(deviceId);
|
|
@@ -4243,6 +4294,42 @@ var RecordingController = class {
|
|
|
4243
4294
|
}
|
|
4244
4295
|
}
|
|
4245
4296
|
/**
|
|
4297
|
+
* Name the `!wantRecording` drop when the device is ENABLED but its config can
|
|
4298
|
+
* never produce footage — the branch that hid this bug. Two shapes qualify,
|
|
4299
|
+
* and both mean "the operator asked for recording and will get none, forever":
|
|
4300
|
+
* an enabled config with no bands at all, and an `events` band that cannot
|
|
4301
|
+
* open a live window (no trigger listed, or an explicit `postBufferSec: 0`).
|
|
4302
|
+
*
|
|
4303
|
+
* NOT reported: a band that simply does not cover `now` (a night-only band at
|
|
4304
|
+
* noon), or an events band waiting for its next trigger. Those are the
|
|
4305
|
+
* feature working.
|
|
4306
|
+
*
|
|
4307
|
+
* One line per device until it records again ({@link unrecordableReported} is
|
|
4308
|
+
* cleared the moment `wantRecording` turns true), so the 30 s tick cannot turn
|
|
4309
|
+
* this into a log flood.
|
|
4310
|
+
*/
|
|
4311
|
+
reportUnrecordable(deviceId, config) {
|
|
4312
|
+
if (!config.enabled) return;
|
|
4313
|
+
if (this.unrecordableReported.has(deviceId)) return;
|
|
4314
|
+
const bands = config.bands ?? [];
|
|
4315
|
+
if (bands.length === 0) {
|
|
4316
|
+
this.unrecordableReported.add(deviceId);
|
|
4317
|
+
this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
const band = activeBandAt({ bands }, new Date(this.now()));
|
|
4321
|
+
if (band === null || eventsBandCanEverDemand(band)) return;
|
|
4322
|
+
this.unrecordableReported.add(deviceId);
|
|
4323
|
+
this.deps.logger.warn("recorder: events band can never open a recording window — nothing will ever be recorded", {
|
|
4324
|
+
tags: { deviceId },
|
|
4325
|
+
meta: {
|
|
4326
|
+
postBufferSec: band.postBufferSec,
|
|
4327
|
+
triggers: band.triggers,
|
|
4328
|
+
reason: band.triggers?.motion === true || band.triggers?.audioThresholdDbfs !== void 0 ? "postBufferSec is 0" : "no trigger configured"
|
|
4329
|
+
}
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4246
4333
|
* The attach body — extracted so the {@link attaching} in-flight guard in
|
|
4247
4334
|
* `evaluateDevice` wraps it cleanly. Resolves the profiles, spawns a
|
|
4248
4335
|
* SegmentWriter + watcher per profile, and records the set in `active`. A throw
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
3
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
3
|
+
const require_worker_protocol = require("../worker-protocol-CWhkLkHx.js");
|
|
4
4
|
let node_url = require("node:url");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
node_fs = require_chunk.__toESM(node_fs);
|
|
@@ -343,7 +343,7 @@ var LeaseActivityGate = class {
|
|
|
343
343
|
windowMs;
|
|
344
344
|
now;
|
|
345
345
|
armedUntil = 0;
|
|
346
|
-
|
|
346
|
+
disabledUntil = 0;
|
|
347
347
|
constructor(windowMs, now = Date.now) {
|
|
348
348
|
this.windowMs = windowMs;
|
|
349
349
|
this.now = now;
|
|
@@ -354,23 +354,44 @@ var LeaseActivityGate = class {
|
|
|
354
354
|
this.armedUntil = this.now() + this.windowMs;
|
|
355
355
|
}
|
|
356
356
|
/**
|
|
357
|
-
* Force the gate closed regardless of demand — for a
|
|
358
|
-
* PROVEN broken
|
|
359
|
-
* filtergraph per failing frame both leaks (node-av reclaims hw
|
|
360
|
-
* by process exit) and burns GPU churn, so a broken path
|
|
361
|
-
*
|
|
357
|
+
* Force the gate closed regardless of demand for `cooldownMs` — for a
|
|
358
|
+
* download path that has PROVEN broken (an add/receive failure). Rebuilding
|
|
359
|
+
* the vaapi filtergraph per failing frame both leaks (node-av reclaims hw
|
|
360
|
+
* pools only by process exit) and burns GPU churn, so a broken path must stay
|
|
361
|
+
* off. Overrides `windowMs: 0` too.
|
|
362
|
+
*
|
|
363
|
+
* ## Why this is a COOLDOWN and not a latch
|
|
364
|
+
*
|
|
365
|
+
* It was a latch (`disable()`), held until {@link reset} — i.e. until the next
|
|
366
|
+
* DIAL. On a continuously recording camera a dial lasts hours, so two EIO
|
|
367
|
+
* `hwdownload` failures killed the native lease for the whole session and
|
|
368
|
+
* every detail dispatch's rung-1 crop missed for as long as it ran. That is
|
|
369
|
+
* the 93-97% `nativeCropMisses` measured on 2026-08-08: dev 618 had 24 dials
|
|
370
|
+
* disabled in a single day, and dev 592 serviced ~15 crop requests/s for
|
|
371
|
+
* 500 s with a `leaseOffered` delta of exactly 0 — `arm()` firing constantly
|
|
372
|
+
* against a gate that could never reopen.
|
|
373
|
+
*
|
|
374
|
+
* The OOM guard the latch existed for is preserved by the cooldown's LENGTH,
|
|
375
|
+
* not by its permanence: one filtergraph rebuild per cooldown instead of one
|
|
376
|
+
* per frame. The caller escalates `cooldownMs` on repeated failures, so a
|
|
377
|
+
* genuinely broken dial converges on the old behaviour while a transient GPU
|
|
378
|
+
* fault recovers on its own.
|
|
379
|
+
*
|
|
380
|
+
* A later call always wins, even a shorter one: the caller escalates
|
|
381
|
+
* monotonically, and "the most recent failure decides" is the only rule that
|
|
382
|
+
* stays true if it ever stops.
|
|
362
383
|
*/
|
|
363
|
-
|
|
364
|
-
this.
|
|
384
|
+
disableFor(cooldownMs) {
|
|
385
|
+
this.disabledUntil = this.now() + cooldownMs;
|
|
365
386
|
}
|
|
366
|
-
/** New dial: clear the
|
|
387
|
+
/** New dial: clear the cooldown AND any stale demand (caller re-arms explicitly). */
|
|
367
388
|
reset() {
|
|
368
|
-
this.
|
|
389
|
+
this.disabledUntil = 0;
|
|
369
390
|
this.armedUntil = 0;
|
|
370
391
|
}
|
|
371
392
|
/** Whether lease capture should run for the current frame. */
|
|
372
393
|
get active() {
|
|
373
|
-
if (this.
|
|
394
|
+
if (this.now() < this.disabledUntil) return false;
|
|
374
395
|
if (this.windowMs === 0) return true;
|
|
375
396
|
return this.now() < this.armedUntil;
|
|
376
397
|
}
|
|
@@ -856,6 +877,25 @@ var NATIVE_LEASE_TTL_MS = NATIVE_LEASE_KNOBS.values.ttlMs;
|
|
|
856
877
|
*/
|
|
857
878
|
var NATIVE_LEASE_ACTIVITY_WINDOW_MS = NATIVE_LEASE_KNOBS.values.activityMs;
|
|
858
879
|
/**
|
|
880
|
+
* How long lease capture stays off after the download path proves broken, and
|
|
881
|
+
* the ceiling that backoff climbs to. Doubling from the base, so the Nth
|
|
882
|
+
* failure in a dial costs `base × 2^(N-2)` — 60 s, 120 s, 240 s … capped.
|
|
883
|
+
*
|
|
884
|
+
* These replace a latch that was held for the REST OF THE DIAL. On a
|
|
885
|
+
* continuously recording camera a dial is hours, and the 2026-08-08 harvest
|
|
886
|
+
* measured what that costs: dev 618 at 93% `nativeCropMisses` across 24
|
|
887
|
+
* disabled dials, dev 592 with a `leaseOffered` delta of 0 over 500 s while
|
|
888
|
+
* answering ~15 crop requests/s. Every one of those misses is a detail
|
|
889
|
+
* dispatch whose rung-1 native crop could never resolve.
|
|
890
|
+
*
|
|
891
|
+
* The bound that mattered is still bounded: the OOM the latch was written for
|
|
892
|
+
* came from rebuilding a vaapi filtergraph PER FAILING FRAME (~25/s), and one
|
|
893
|
+
* rebuild per cooldown is three orders of magnitude below that — while a
|
|
894
|
+
* transient GPU fault now recovers without waiting for a re-dial.
|
|
895
|
+
*/
|
|
896
|
+
var LEASE_DOWNLOAD_COOLDOWN_BASE_MS = 6e4;
|
|
897
|
+
var LEASE_DOWNLOAD_COOLDOWN_MAX_MS = 6e5;
|
|
898
|
+
/**
|
|
859
899
|
* `pinnedRgbCrop` — pinned-color RGB24 native-crop retention. DEFAULT **ON**
|
|
860
900
|
* (2026-07-20 rollout); set `CAMSTACK_SESSION_PINNED_RGB_CROP=0` to opt out.
|
|
861
901
|
*
|
|
@@ -1549,7 +1589,11 @@ var DecodeWorkerChild = class {
|
|
|
1549
1589
|
this.nativeLeaseDownloadFilter = null;
|
|
1550
1590
|
this.nativeLeaseDownloadFilterKey = "";
|
|
1551
1591
|
this.leaseDownloadFailuresThisDial++;
|
|
1552
|
-
if (this.leaseDownloadFailuresThisDial >= 2)
|
|
1592
|
+
if (this.leaseDownloadFailuresThisDial >= 2) {
|
|
1593
|
+
const cooldownMs = Math.min(LEASE_DOWNLOAD_COOLDOWN_MAX_MS, LEASE_DOWNLOAD_COOLDOWN_BASE_MS * 2 ** (this.leaseDownloadFailuresThisDial - 2));
|
|
1594
|
+
this.leaseGate.disableFor(cooldownMs);
|
|
1595
|
+
this.emitStderr(`decode-worker-child: native lease capture OFF for ${cooldownMs}ms — ${this.leaseDownloadFailuresThisDial} download failures this dial; native crops will MISS until it expires (detail dispatches fall back to the retained full frame)\n`);
|
|
1596
|
+
}
|
|
1553
1597
|
return null;
|
|
1554
1598
|
}
|
|
1555
1599
|
}
|