@camstack/addon-pipeline 1.1.31 → 1.1.32
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/decoder-ffmpeg/index.js +4 -4
- package/dist/decoder-ffmpeg/index.mjs +4 -4
- package/dist/decoder-nodeav/index.js +10 -8
- package/dist/decoder-nodeav/index.mjs +10 -8
- package/dist/detection-pipeline/index.js +33 -2
- package/dist/detection-pipeline/index.mjs +33 -2
- package/dist/{frame-ring-sink-8LLV-cvH.js → frame-ring-sink-DvdhCFr3.js} +104 -0
- package/dist/{frame-ring-sink-ClEWjiRU.mjs → frame-ring-sink-sY-8XUsH.mjs} +100 -2
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_dist = require("../dist-Cwc0TUQr.js");
|
|
6
|
-
const require_frame_ring_sink = require("../frame-ring-sink-
|
|
6
|
+
const require_frame_ring_sink = require("../frame-ring-sink-DvdhCFr3.js");
|
|
7
7
|
const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
|
|
8
8
|
const require_frame_dropper = require("../frame-dropper-DKLM6pMz.js");
|
|
9
9
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
@@ -706,6 +706,9 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
|
|
|
706
706
|
}] });
|
|
707
707
|
}
|
|
708
708
|
async onInitialize() {
|
|
709
|
+
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
710
|
+
this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
711
|
+
});
|
|
709
712
|
const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
|
|
710
713
|
if (backend !== "ffmpeg") {
|
|
711
714
|
this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
@@ -719,9 +722,6 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
|
|
|
719
722
|
filters: [...this.probedGpuScaleFilters],
|
|
720
723
|
ffmpeg: this.ffmpegPath
|
|
721
724
|
} });
|
|
722
|
-
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
723
|
-
this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
724
|
-
});
|
|
725
725
|
return [{
|
|
726
726
|
capability: require_dist.decoderCapability,
|
|
727
727
|
provider: this
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
|
|
2
|
-
import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB,
|
|
2
|
+
import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, s as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-sY-8XUsH.mjs";
|
|
3
3
|
import { a as codecToInputFormat, c as maxDecodeWidth, d as pickDecodeHwAccel, f as rankDecodeHwAccels, i as channelsForPixel, l as parseAvailableGpuScaleFilters, m as resolveGpuScaleFilter, n as DEFAULT_VAAPI_RENDER_DEVICE, o as computeOutputGeometry, p as rawPixelForFormat, r as buildFfmpegDecodeArgs, s as isSoftwareHwAccel, t as DECODE_HWACCEL_RANK, u as parseFfmpegOutputDims } from "../ffmpeg-args-C5GPp8Cw.mjs";
|
|
4
4
|
import { t as FrameDropper } from "../frame-dropper-AjheBGMG.mjs";
|
|
5
5
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
@@ -702,6 +702,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
702
702
|
}] });
|
|
703
703
|
}
|
|
704
704
|
async onInitialize() {
|
|
705
|
+
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
706
|
+
this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
707
|
+
});
|
|
705
708
|
const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
|
|
706
709
|
if (backend !== "ffmpeg") {
|
|
707
710
|
this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
@@ -715,9 +718,6 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
715
718
|
filters: [...this.probedGpuScaleFilters],
|
|
716
719
|
ffmpeg: this.ffmpegPath
|
|
717
720
|
} });
|
|
718
|
-
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
719
|
-
this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
720
|
-
});
|
|
721
721
|
return [{
|
|
722
722
|
capability: decoderCapability,
|
|
723
723
|
provider: this
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_dist = require("../dist-Cwc0TUQr.js");
|
|
6
|
-
const require_frame_ring_sink = require("../frame-ring-sink-
|
|
6
|
+
const require_frame_ring_sink = require("../frame-ring-sink-DvdhCFr3.js");
|
|
7
7
|
const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
|
|
8
8
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
@@ -955,14 +955,16 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
955
955
|
}];
|
|
956
956
|
}
|
|
957
957
|
/**
|
|
958
|
-
* Resolve the effective hwaccel backend for a new session. Reads
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
* is
|
|
958
|
+
* Resolve the effective hwaccel backend for a new session. Reads the SHARED
|
|
959
|
+
* per-node override from the `decoder-ffmpeg` OWNER — the single value the
|
|
960
|
+
* Pipeline UI edits — exactly as `resolveDecoderBackend` reads the owner's
|
|
961
|
+
* `backend`. This makes a manual hwaccel override take effect even when
|
|
962
|
+
* node-av is the active backend (its OWN `hwaccel@node` is never surfaced by
|
|
963
|
+
* the UI). `'auto'` defers to the session's local resolver
|
|
964
|
+
* (`ctx.kernel.hwaccel`). Fails safe to `'auto'`.
|
|
963
965
|
*/
|
|
964
966
|
resolveHwAccelPref() {
|
|
965
|
-
return this.
|
|
967
|
+
return require_frame_ring_sink.resolveDecoderHwAccel(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
|
|
966
968
|
}
|
|
967
969
|
/**
|
|
968
970
|
* Re-run the platform probe on this host and persist the detected
|
|
@@ -1021,7 +1023,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
|
|
|
1021
1023
|
}
|
|
1022
1024
|
async createSession(config) {
|
|
1023
1025
|
const sessionId = (0, node_crypto.randomUUID)();
|
|
1024
|
-
const hwaccel = this.resolveHwAccelPref();
|
|
1026
|
+
const hwaccel = await this.resolveHwAccelPref();
|
|
1025
1027
|
const { frameSink } = config;
|
|
1026
1028
|
const nodeId = this.resolveLocalNodeId();
|
|
1027
1029
|
const session = new NodeAvDecoderSession(config, this.ctx.logger, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
|
|
2
|
-
import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-
|
|
2
|
+
import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, o as resolveDecoderHwAccel, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-sY-8XUsH.mjs";
|
|
3
3
|
import { f as rankDecodeHwAccels } from "../ffmpeg-args-C5GPp8Cw.mjs";
|
|
4
4
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -951,14 +951,16 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
951
951
|
}];
|
|
952
952
|
}
|
|
953
953
|
/**
|
|
954
|
-
* Resolve the effective hwaccel backend for a new session. Reads
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
* is
|
|
954
|
+
* Resolve the effective hwaccel backend for a new session. Reads the SHARED
|
|
955
|
+
* per-node override from the `decoder-ffmpeg` OWNER — the single value the
|
|
956
|
+
* Pipeline UI edits — exactly as `resolveDecoderBackend` reads the owner's
|
|
957
|
+
* `backend`. This makes a manual hwaccel override take effect even when
|
|
958
|
+
* node-av is the active backend (its OWN `hwaccel@node` is never surfaced by
|
|
959
|
+
* the UI). `'auto'` defers to the session's local resolver
|
|
960
|
+
* (`ctx.kernel.hwaccel`). Fails safe to `'auto'`.
|
|
959
961
|
*/
|
|
960
962
|
resolveHwAccelPref() {
|
|
961
|
-
return this.
|
|
963
|
+
return resolveDecoderHwAccel(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
|
|
962
964
|
}
|
|
963
965
|
/**
|
|
964
966
|
* Re-run the platform probe on this host and persist the detected
|
|
@@ -1017,7 +1019,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
1017
1019
|
}
|
|
1018
1020
|
async createSession(config) {
|
|
1019
1021
|
const sessionId = randomUUID();
|
|
1020
|
-
const hwaccel = this.resolveHwAccelPref();
|
|
1022
|
+
const hwaccel = await this.resolveHwAccelPref();
|
|
1021
1023
|
const { frameSink } = config;
|
|
1022
1024
|
const nodeId = this.resolveLocalNodeId();
|
|
1023
1025
|
const session = new NodeAvDecoderSession(config, this.ctx.logger, {
|
|
@@ -185,6 +185,35 @@ function pickBestRuntime(env, bestBackendHint) {
|
|
|
185
185
|
device: defaultDeviceFor(chosen)
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The ordered device candidates a runtime will try for an `'auto'`/`'all'`
|
|
190
|
+
* device pick — mirrors what the python inference pool actually resolves
|
|
191
|
+
* (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
|
|
192
|
+
* ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
|
|
193
|
+
* accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
|
|
194
|
+
* tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
|
|
195
|
+
* caller then keeps the plain `backend/device` label.
|
|
196
|
+
*/
|
|
197
|
+
function engineDeviceSequence(runtimeId, env) {
|
|
198
|
+
const hw = env.hardware;
|
|
199
|
+
switch (runtimeId) {
|
|
200
|
+
case "openvino": {
|
|
201
|
+
const seq = [];
|
|
202
|
+
if (hw?.npu?.type === "intel-npu") seq.push("NPU");
|
|
203
|
+
if (hw?.gpu?.type === "intel") seq.push("GPU");
|
|
204
|
+
seq.push("CPU");
|
|
205
|
+
return seq;
|
|
206
|
+
}
|
|
207
|
+
case "coreml": {
|
|
208
|
+
const seq = [];
|
|
209
|
+
if (hw?.npu?.type === "apple-ane") seq.push("ANE");
|
|
210
|
+
if (hw?.gpu != null) seq.push("GPU");
|
|
211
|
+
seq.push("CPU");
|
|
212
|
+
return seq;
|
|
213
|
+
}
|
|
214
|
+
default: return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
188
217
|
//#endregion
|
|
189
218
|
//#region src/detection-pipeline/engine-store-keys.ts
|
|
190
219
|
/**
|
|
@@ -6423,14 +6452,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6423
6452
|
try {
|
|
6424
6453
|
hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
|
|
6425
6454
|
} catch {}
|
|
6426
|
-
const
|
|
6455
|
+
const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
|
|
6456
|
+
const pick = pickBestRuntime(env, null);
|
|
6427
6457
|
const best = {
|
|
6428
6458
|
runtime: "python",
|
|
6429
6459
|
backend: pick.runtimeId,
|
|
6430
6460
|
format: modelFormatFor(pick.runtimeId),
|
|
6431
6461
|
device: pick.device
|
|
6432
6462
|
};
|
|
6433
|
-
const
|
|
6463
|
+
const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
|
|
6464
|
+
const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
|
|
6434
6465
|
const rpNode = this.localProbeNodeId();
|
|
6435
6466
|
if (pick.runtimeId !== "onnx" || hardware !== null) {
|
|
6436
6467
|
await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
|
|
@@ -178,6 +178,35 @@ function pickBestRuntime(env, bestBackendHint) {
|
|
|
178
178
|
device: defaultDeviceFor(chosen)
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The ordered device candidates a runtime will try for an `'auto'`/`'all'`
|
|
183
|
+
* device pick — mirrors what the python inference pool actually resolves
|
|
184
|
+
* (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
|
|
185
|
+
* ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
|
|
186
|
+
* accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
|
|
187
|
+
* tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
|
|
188
|
+
* caller then keeps the plain `backend/device` label.
|
|
189
|
+
*/
|
|
190
|
+
function engineDeviceSequence(runtimeId, env) {
|
|
191
|
+
const hw = env.hardware;
|
|
192
|
+
switch (runtimeId) {
|
|
193
|
+
case "openvino": {
|
|
194
|
+
const seq = [];
|
|
195
|
+
if (hw?.npu?.type === "intel-npu") seq.push("NPU");
|
|
196
|
+
if (hw?.gpu?.type === "intel") seq.push("GPU");
|
|
197
|
+
seq.push("CPU");
|
|
198
|
+
return seq;
|
|
199
|
+
}
|
|
200
|
+
case "coreml": {
|
|
201
|
+
const seq = [];
|
|
202
|
+
if (hw?.npu?.type === "apple-ane") seq.push("ANE");
|
|
203
|
+
if (hw?.gpu != null) seq.push("GPU");
|
|
204
|
+
seq.push("CPU");
|
|
205
|
+
return seq;
|
|
206
|
+
}
|
|
207
|
+
default: return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
181
210
|
//#endregion
|
|
182
211
|
//#region src/detection-pipeline/engine-store-keys.ts
|
|
183
212
|
/**
|
|
@@ -6416,14 +6445,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6416
6445
|
try {
|
|
6417
6446
|
hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
|
|
6418
6447
|
} catch {}
|
|
6419
|
-
const
|
|
6448
|
+
const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
|
|
6449
|
+
const pick = pickBestRuntime(env, null);
|
|
6420
6450
|
const best = {
|
|
6421
6451
|
runtime: "python",
|
|
6422
6452
|
backend: pick.runtimeId,
|
|
6423
6453
|
format: modelFormatFor(pick.runtimeId),
|
|
6424
6454
|
device: pick.device
|
|
6425
6455
|
};
|
|
6426
|
-
const
|
|
6456
|
+
const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
|
|
6457
|
+
const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
|
|
6427
6458
|
const rpNode = this.localProbeNodeId();
|
|
6428
6459
|
if (pick.runtimeId !== "onnx" || hardware !== null) {
|
|
6429
6460
|
await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
|
|
@@ -18,8 +18,75 @@ function normalizeDecoderNodeId(rawNodeId) {
|
|
|
18
18
|
}
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region src/shared/decoder-backend.ts
|
|
21
|
+
/**
|
|
22
|
+
* Decoder backend selection — decides which decoder addon registers the
|
|
23
|
+
* `decoder` cap on a node.
|
|
24
|
+
*
|
|
25
|
+
* Two decoder addons ship in this bundle: `decoder-ffmpeg` (subprocess
|
|
26
|
+
* decode; a decode crash is isolated to the child) and `decoder-nodeav`
|
|
27
|
+
* (in-process decode via FFmpeg native bindings; no subprocess lifecycle).
|
|
28
|
+
* Both are installed on every node, but only ONE may provide the `decoder`
|
|
29
|
+
* cap at a time.
|
|
30
|
+
*
|
|
31
|
+
* ## Why selection happens at REGISTRATION time (race-free by construction)
|
|
32
|
+
*
|
|
33
|
+
* The `decoder` cap is a singleton. Historically both decoder addons
|
|
34
|
+
* registered a provider and the active slot was resolved AFTER the fact —
|
|
35
|
+
* which exposed a boot-order race: whichever addon's runner initialised
|
|
36
|
+
* first briefly held the active slot, and any consumer that resolved the
|
|
37
|
+
* cap inside that window was dispatched to the wrong backend (the
|
|
38
|
+
* cap-declared `preferredProvider` override only corrects the slot when
|
|
39
|
+
* the preferred addon's registration eventually lands).
|
|
40
|
+
*
|
|
41
|
+
* This module removes the race by construction instead of arbitrating it:
|
|
42
|
+
* each decoder addon calls {@link resolveDecoderBackend} at the START of
|
|
43
|
+
* its `onInitialize` and registers its provider ONLY when it is the
|
|
44
|
+
* selected backend — the other addon returns no registrations at all. At
|
|
45
|
+
* most one `decoder` provider ever exists per node, so no resolution layer
|
|
46
|
+
* (kernel `CapabilityRegistry`, route resolver, UDS child preference) can
|
|
47
|
+
* pick a wrong one, regardless of boot order or timing.
|
|
48
|
+
*
|
|
49
|
+
* ## The setting
|
|
50
|
+
*
|
|
51
|
+
* The per-node selection lives in the `decoder-ffmpeg` OWNER addon's global
|
|
52
|
+
* settings — the standard `addon-settings` surface, visible in the admin UI
|
|
53
|
+
* as the owner's `backend` select field. The store is hub-central and the
|
|
54
|
+
* value is persisted node-scoped as `backend@<nodeId>`
|
|
55
|
+
* (`decoder-backend-keys.ts`); the owner's `getGlobalSettings` projects the
|
|
56
|
+
* requested node's value onto the bare `backend` field.
|
|
57
|
+
*
|
|
58
|
+
* Both decoder addons read it the SAME way, placement-agnostically: via
|
|
59
|
+
* `ctx.api.addonSettings.getGlobalSettings({ addonId: 'decoder-ffmpeg',
|
|
60
|
+
* nodeId })`. The `addon-settings` cap is hub-routed at the system level
|
|
61
|
+
* (`AddonCallGateway.classify` sends settings calls to the addon's base
|
|
62
|
+
* node — the hub instance answers for every node), so an agent addon
|
|
63
|
+
* transparently reads the hub-central store with no placement branching.
|
|
64
|
+
* NOT used: `ctx.settings.getSection` (node-local — empty on an agent, the
|
|
65
|
+
* original bug), the raw settings-store cap, or any custom side channel.
|
|
66
|
+
*
|
|
67
|
+
* Resolution: the node's scoped value → the built-in default
|
|
68
|
+
* {@link DEFAULT_DECODER_BACKEND} (`'ffmpeg'`). There is deliberately NO
|
|
69
|
+
* bare-key fallback (see `decoder-backend-keys.ts`), and any read failure
|
|
70
|
+
* resolves to the default so the safe backend still comes up.
|
|
71
|
+
*
|
|
72
|
+
* ## Switching backends
|
|
73
|
+
*
|
|
74
|
+
* Because registration is decided once at addon init, changing the setting
|
|
75
|
+
* takes effect on the next restart of the two decoder addons on the
|
|
76
|
+
* affected node (addon restart / redeploy / node reboot). This is the
|
|
77
|
+
* price of race-freedom: there is deliberately NO live re-arbitration
|
|
78
|
+
* path — a live hand-off would reintroduce a window with two providers.
|
|
79
|
+
*/
|
|
21
80
|
/** The addon whose global settings OWN the per-node `backend` field. */
|
|
22
81
|
var DECODER_OWNER_ADDON_ID = "decoder-ffmpeg";
|
|
82
|
+
/** Fail-safe when the owner's hwaccel can't be read: defer to the local probe. */
|
|
83
|
+
var DEFAULT_DECODER_HWACCEL = "auto";
|
|
84
|
+
/** Narrow an arbitrary stored value to a known {@link HwAccelChoice}, else null. */
|
|
85
|
+
function parseDecoderHwAccel(raw) {
|
|
86
|
+
if (typeof raw !== "string") return null;
|
|
87
|
+
const match = require_dist.HWACCEL_OPTIONS.find((o) => o.value === raw);
|
|
88
|
+
return match ? match.value : null;
|
|
89
|
+
}
|
|
23
90
|
function isHydratedField(entry) {
|
|
24
91
|
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
25
92
|
}
|
|
@@ -72,6 +139,37 @@ async function resolveDecoderBackend(api, nodeId, logger) {
|
|
|
72
139
|
return DEFAULT_DECODER_BACKEND;
|
|
73
140
|
}
|
|
74
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Pure selection of the owner's `hwaccel` field from an already-read hydrated
|
|
144
|
+
* settings payload. Missing/invalid/null → {@link DEFAULT_DECODER_HWACCEL}.
|
|
145
|
+
*/
|
|
146
|
+
function pickDecoderHwAccelFromSettings(view) {
|
|
147
|
+
if (view === null) return DEFAULT_DECODER_HWACCEL;
|
|
148
|
+
for (const section of view.sections) for (const entry of section.fields) {
|
|
149
|
+
if (!isHydratedField(entry) || entry.key !== "hwaccel") continue;
|
|
150
|
+
return parseDecoderHwAccel(entry.value) ?? "auto";
|
|
151
|
+
}
|
|
152
|
+
return DEFAULT_DECODER_HWACCEL;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the effective decoder hwaccel override for a node by reading the
|
|
156
|
+
* `decoder-ffmpeg` OWNER's `hwaccel@<node>` setting — the exact same
|
|
157
|
+
* hub-routed, placement-agnostic read as {@link resolveDecoderBackend}, so
|
|
158
|
+
* node-av and ffmpeg honour ONE per-node value (the one the UI edits). Fails
|
|
159
|
+
* safe to {@link DEFAULT_DECODER_HWACCEL} (`'auto'` → the session's local probe).
|
|
160
|
+
*/
|
|
161
|
+
async function resolveDecoderHwAccel(api, nodeId, logger) {
|
|
162
|
+
if (!api) return DEFAULT_DECODER_HWACCEL;
|
|
163
|
+
try {
|
|
164
|
+
return pickDecoderHwAccelFromSettings(await api.addonSettings.getGlobalSettings.query({
|
|
165
|
+
addonId: DECODER_OWNER_ADDON_ID,
|
|
166
|
+
nodeId: normalizeDecoderNodeId(nodeId)
|
|
167
|
+
}));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
logger.warn("decoder-hwaccel: owner settings read failed — deferring to local probe", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
170
|
+
return DEFAULT_DECODER_HWACCEL;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
75
173
|
//#endregion
|
|
76
174
|
//#region src/shared/notifying-ring-buffer.ts
|
|
77
175
|
/**
|
|
@@ -442,3 +540,9 @@ Object.defineProperty(exports, "resolveDecoderBackend", {
|
|
|
442
540
|
return resolveDecoderBackend;
|
|
443
541
|
}
|
|
444
542
|
});
|
|
543
|
+
Object.defineProperty(exports, "resolveDecoderHwAccel", {
|
|
544
|
+
enumerable: true,
|
|
545
|
+
get: function() {
|
|
546
|
+
return resolveDecoderHwAccel;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as RingBuffer } from "./dist-DjuGmyG9.mjs";
|
|
1
|
+
import { d as HWACCEL_OPTIONS, p as RingBuffer } from "./dist-DjuGmyG9.mjs";
|
|
2
2
|
import { FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
|
|
3
3
|
//#region src/shared/decoder-backend-keys.ts
|
|
4
4
|
/** Built-in default when a node has no selection — the subprocess decoder. */
|
|
@@ -18,8 +18,75 @@ function normalizeDecoderNodeId(rawNodeId) {
|
|
|
18
18
|
}
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region src/shared/decoder-backend.ts
|
|
21
|
+
/**
|
|
22
|
+
* Decoder backend selection — decides which decoder addon registers the
|
|
23
|
+
* `decoder` cap on a node.
|
|
24
|
+
*
|
|
25
|
+
* Two decoder addons ship in this bundle: `decoder-ffmpeg` (subprocess
|
|
26
|
+
* decode; a decode crash is isolated to the child) and `decoder-nodeav`
|
|
27
|
+
* (in-process decode via FFmpeg native bindings; no subprocess lifecycle).
|
|
28
|
+
* Both are installed on every node, but only ONE may provide the `decoder`
|
|
29
|
+
* cap at a time.
|
|
30
|
+
*
|
|
31
|
+
* ## Why selection happens at REGISTRATION time (race-free by construction)
|
|
32
|
+
*
|
|
33
|
+
* The `decoder` cap is a singleton. Historically both decoder addons
|
|
34
|
+
* registered a provider and the active slot was resolved AFTER the fact —
|
|
35
|
+
* which exposed a boot-order race: whichever addon's runner initialised
|
|
36
|
+
* first briefly held the active slot, and any consumer that resolved the
|
|
37
|
+
* cap inside that window was dispatched to the wrong backend (the
|
|
38
|
+
* cap-declared `preferredProvider` override only corrects the slot when
|
|
39
|
+
* the preferred addon's registration eventually lands).
|
|
40
|
+
*
|
|
41
|
+
* This module removes the race by construction instead of arbitrating it:
|
|
42
|
+
* each decoder addon calls {@link resolveDecoderBackend} at the START of
|
|
43
|
+
* its `onInitialize` and registers its provider ONLY when it is the
|
|
44
|
+
* selected backend — the other addon returns no registrations at all. At
|
|
45
|
+
* most one `decoder` provider ever exists per node, so no resolution layer
|
|
46
|
+
* (kernel `CapabilityRegistry`, route resolver, UDS child preference) can
|
|
47
|
+
* pick a wrong one, regardless of boot order or timing.
|
|
48
|
+
*
|
|
49
|
+
* ## The setting
|
|
50
|
+
*
|
|
51
|
+
* The per-node selection lives in the `decoder-ffmpeg` OWNER addon's global
|
|
52
|
+
* settings — the standard `addon-settings` surface, visible in the admin UI
|
|
53
|
+
* as the owner's `backend` select field. The store is hub-central and the
|
|
54
|
+
* value is persisted node-scoped as `backend@<nodeId>`
|
|
55
|
+
* (`decoder-backend-keys.ts`); the owner's `getGlobalSettings` projects the
|
|
56
|
+
* requested node's value onto the bare `backend` field.
|
|
57
|
+
*
|
|
58
|
+
* Both decoder addons read it the SAME way, placement-agnostically: via
|
|
59
|
+
* `ctx.api.addonSettings.getGlobalSettings({ addonId: 'decoder-ffmpeg',
|
|
60
|
+
* nodeId })`. The `addon-settings` cap is hub-routed at the system level
|
|
61
|
+
* (`AddonCallGateway.classify` sends settings calls to the addon's base
|
|
62
|
+
* node — the hub instance answers for every node), so an agent addon
|
|
63
|
+
* transparently reads the hub-central store with no placement branching.
|
|
64
|
+
* NOT used: `ctx.settings.getSection` (node-local — empty on an agent, the
|
|
65
|
+
* original bug), the raw settings-store cap, or any custom side channel.
|
|
66
|
+
*
|
|
67
|
+
* Resolution: the node's scoped value → the built-in default
|
|
68
|
+
* {@link DEFAULT_DECODER_BACKEND} (`'ffmpeg'`). There is deliberately NO
|
|
69
|
+
* bare-key fallback (see `decoder-backend-keys.ts`), and any read failure
|
|
70
|
+
* resolves to the default so the safe backend still comes up.
|
|
71
|
+
*
|
|
72
|
+
* ## Switching backends
|
|
73
|
+
*
|
|
74
|
+
* Because registration is decided once at addon init, changing the setting
|
|
75
|
+
* takes effect on the next restart of the two decoder addons on the
|
|
76
|
+
* affected node (addon restart / redeploy / node reboot). This is the
|
|
77
|
+
* price of race-freedom: there is deliberately NO live re-arbitration
|
|
78
|
+
* path — a live hand-off would reintroduce a window with two providers.
|
|
79
|
+
*/
|
|
21
80
|
/** The addon whose global settings OWN the per-node `backend` field. */
|
|
22
81
|
var DECODER_OWNER_ADDON_ID = "decoder-ffmpeg";
|
|
82
|
+
/** Fail-safe when the owner's hwaccel can't be read: defer to the local probe. */
|
|
83
|
+
var DEFAULT_DECODER_HWACCEL = "auto";
|
|
84
|
+
/** Narrow an arbitrary stored value to a known {@link HwAccelChoice}, else null. */
|
|
85
|
+
function parseDecoderHwAccel(raw) {
|
|
86
|
+
if (typeof raw !== "string") return null;
|
|
87
|
+
const match = HWACCEL_OPTIONS.find((o) => o.value === raw);
|
|
88
|
+
return match ? match.value : null;
|
|
89
|
+
}
|
|
23
90
|
function isHydratedField(entry) {
|
|
24
91
|
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
25
92
|
}
|
|
@@ -72,6 +139,37 @@ async function resolveDecoderBackend(api, nodeId, logger) {
|
|
|
72
139
|
return DEFAULT_DECODER_BACKEND;
|
|
73
140
|
}
|
|
74
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Pure selection of the owner's `hwaccel` field from an already-read hydrated
|
|
144
|
+
* settings payload. Missing/invalid/null → {@link DEFAULT_DECODER_HWACCEL}.
|
|
145
|
+
*/
|
|
146
|
+
function pickDecoderHwAccelFromSettings(view) {
|
|
147
|
+
if (view === null) return DEFAULT_DECODER_HWACCEL;
|
|
148
|
+
for (const section of view.sections) for (const entry of section.fields) {
|
|
149
|
+
if (!isHydratedField(entry) || entry.key !== "hwaccel") continue;
|
|
150
|
+
return parseDecoderHwAccel(entry.value) ?? "auto";
|
|
151
|
+
}
|
|
152
|
+
return DEFAULT_DECODER_HWACCEL;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the effective decoder hwaccel override for a node by reading the
|
|
156
|
+
* `decoder-ffmpeg` OWNER's `hwaccel@<node>` setting — the exact same
|
|
157
|
+
* hub-routed, placement-agnostic read as {@link resolveDecoderBackend}, so
|
|
158
|
+
* node-av and ffmpeg honour ONE per-node value (the one the UI edits). Fails
|
|
159
|
+
* safe to {@link DEFAULT_DECODER_HWACCEL} (`'auto'` → the session's local probe).
|
|
160
|
+
*/
|
|
161
|
+
async function resolveDecoderHwAccel(api, nodeId, logger) {
|
|
162
|
+
if (!api) return DEFAULT_DECODER_HWACCEL;
|
|
163
|
+
try {
|
|
164
|
+
return pickDecoderHwAccelFromSettings(await api.addonSettings.getGlobalSettings.query({
|
|
165
|
+
addonId: DECODER_OWNER_ADDON_ID,
|
|
166
|
+
nodeId: normalizeDecoderNodeId(nodeId)
|
|
167
|
+
}));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
logger.warn("decoder-hwaccel: owner settings read failed — deferring to local probe", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
170
|
+
return DEFAULT_DECODER_HWACCEL;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
75
173
|
//#endregion
|
|
76
174
|
//#region src/shared/notifying-ring-buffer.ts
|
|
77
175
|
/**
|
|
@@ -406,4 +504,4 @@ var DecoderFrameRingSink = class {
|
|
|
406
504
|
}
|
|
407
505
|
};
|
|
408
506
|
//#endregion
|
|
409
|
-
export { resolveDecoderBackend as a, NotifyingRingBuffer as i, RING_BUDGET_MB as n,
|
|
507
|
+
export { resolveDecoderBackend as a, NotifyingRingBuffer as i, RING_BUDGET_MB as n, resolveDecoderHwAccel as o, makeSegmentName as r, DEFAULT_DECODER_BACKEND as s, DecoderFrameRingSink as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.32",
|
|
4
4
|
"description": "CamStack Pipeline bundle — runner, detection, motion, decoders, audio + stream broker. Multi-entry npm package shipping 7 addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|