@camstack/addon-pipeline 1.1.28 → 1.1.29

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 (39) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/audio-codec-ffmpeg/index.js +1 -5
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -5
  5. package/dist/decoder-ffmpeg/index.js +102 -352
  6. package/dist/decoder-ffmpeg/index.mjs +82 -332
  7. package/dist/decoder-nodeav/index.js +6 -4
  8. package/dist/decoder-nodeav/index.mjs +6 -4
  9. package/dist/detection-pipeline/index.js +204 -180
  10. package/dist/detection-pipeline/index.mjs +204 -180
  11. package/dist/{dist-Biq62zt4.js → dist-Cwc0TUQr.js} +7 -42
  12. package/dist/{dist-C6_wgXqF.mjs → dist-DjuGmyG9.mjs} +7 -42
  13. package/dist/ffmpeg-args-C5GPp8Cw.mjs +323 -0
  14. package/dist/ffmpeg-args-D6h1edXK.js +418 -0
  15. package/dist/frame-dropper-AjheBGMG.mjs +22 -0
  16. package/dist/frame-dropper-DKLM6pMz.js +27 -0
  17. package/dist/{frame-handle-plane-BIoY6nRV.mjs → frame-handle-plane-Bkxz-TTD.mjs} +1 -1
  18. package/dist/{frame-handle-plane-D6BzyEgy.js → frame-handle-plane-DQNCTrpC.js} +1 -1
  19. package/dist/{frame-ring-sink-9J0wCdLF.js → frame-ring-sink-8LLV-cvH.js} +1 -1
  20. package/dist/{frame-ring-sink-Cs9vby6v.mjs → frame-ring-sink-ClEWjiRU.mjs} +1 -1
  21. package/dist/motion-wasm/index.js +1 -1
  22. package/dist/motion-wasm/index.mjs +1 -1
  23. package/dist/pipeline-runner/index.js +2 -2
  24. package/dist/pipeline-runner/index.mjs +2 -2
  25. package/dist/recorder/index.js +1 -1
  26. package/dist/recorder/index.mjs +1 -1
  27. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
  28. package/dist/stream-broker/{hostInit-C0SuwQhL.mjs → hostInit-B_b3PIZB.mjs} +3 -3
  29. package/dist/stream-broker/index.js +168 -121
  30. package/dist/stream-broker/index.mjs +159 -112
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BSCcPdPf.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
  35. package/embed-dist/assets/{index-CeZV1B-2.js → index-CpUy8OIE.js} +9 -9
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/dist/frame-dropper-7RTo_YyG.js +0 -68
  39. package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
@@ -1,302 +1,10 @@
1
- import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, j as nodePin, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-C6_wgXqF.mjs";
2
- import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, o as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-Cs9vby6v.mjs";
3
- import { r as logBannerArgs, t as FrameDropper } from "../frame-dropper-CwkBTPGV.mjs";
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, o as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-ClEWjiRU.mjs";
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
+ import { t as FrameDropper } from "../frame-dropper-AjheBGMG.mjs";
4
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
5
6
  import { randomUUID } from "node:crypto";
6
7
  import { spawn } from "node:child_process";
7
- //#region src/decoder-ffmpeg/ffmpeg-args.ts
8
- /**
9
- * Pure, side-effect-free ffmpeg argument construction for the
10
- * `decoder-ffmpeg` addon.
11
- *
12
- * The addon decodes video in an **ffmpeg subprocess** (unlike `decoder-nodeav`
13
- * which decodes in-process via node-av's native bindings). On the Intel hub,
14
- * node-av's VA-API decode path SIGBUSes (uncatchable) on 8MP h264 and takes the
15
- * whole decoder runner down; ffmpeg-as-subprocess decodes the same hardware
16
- * cleanly because a crash is isolated to the child process.
17
- *
18
- * These helpers are extracted from the session so the argument shape is
19
- * unit-testable without spawning a real ffmpeg binary.
20
- *
21
- * Validated working command on the Intel hub (renderD128, 8MP h264, ~50fps,
22
- * exit 0):
23
- *
24
- * ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 \
25
- * -hwaccel_output_format vaapi -f h264 -i <in> \
26
- * -vf hwdownload,format=nv12,scale=640:360,format=gray \
27
- * -f rawvideo -y /dev/null
28
- */
29
- /** Default DRM render node used for the VA-API / QSV hardware decode path. */
30
- var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
31
- /** Whether a resolved hwaccel value means "decode in software" (no `-hwaccel`). */
32
- function isSoftwareHwAccel(hwaccel) {
33
- return hwaccel === null || hwaccel === "" || hwaccel === "none";
34
- }
35
- /** Backends whose decoded frames stay on the GPU and need an explicit
36
- * `-hwaccel_output_format <m>` + `hwdownload,format=nv12` chain. */
37
- function needsHwDownload(hwaccel) {
38
- return hwaccel === "vaapi" || hwaccel === "qsv";
39
- }
40
- /** Decode backend → its GPU scaler filter. */
41
- var GPU_SCALE_FILTER_BY_BACKEND = {
42
- vaapi: "scale_vaapi",
43
- qsv: "scale_qsv",
44
- videotoolbox: "scale_vt"
45
- };
46
- /** Every GPU scaler this addon knows how to drive — used to scan `-filters`. */
47
- var KNOWN_GPU_SCALE_FILTERS = [
48
- "scale_vaapi",
49
- "scale_qsv",
50
- "scale_vt"
51
- ];
52
- /**
53
- * The GPU scaler that pairs with a decode backend, ignoring build support — a
54
- * static hwaccel → filter map. Returns null for software / a backend with no
55
- * GPU scaler (e.g. `cuda`, `drm`). Case-insensitive.
56
- */
57
- function gpuScaleFilterForBackend(hwaccel) {
58
- if (hwaccel === null) return null;
59
- return GPU_SCALE_FILTER_BY_BACKEND[hwaccel.toLowerCase()] ?? null;
60
- }
61
- /**
62
- * Resolve the GPU scaler to use for a decode backend GIVEN the filters the
63
- * configured ffmpeg build actually offers ({@link parseAvailableGpuScaleFilters}).
64
- * Returns the filter when the backend maps to one AND the build has it; null
65
- * otherwise — the caller then emits the CPU-scale chain (graceful fallback).
66
- */
67
- function resolveGpuScaleFilter(hwaccel, availableFilters) {
68
- const filter = gpuScaleFilterForBackend(hwaccel);
69
- if (filter === null) return null;
70
- return availableFilters.has(filter) ? filter : null;
71
- }
72
- /**
73
- * Parse `ffmpeg -filters` stdout into the set of {@link KNOWN_GPU_SCALE_FILTERS}
74
- * the build offers. Matches whole filter tokens (word-boundary) so the plain
75
- * `scale` filter is never mistaken for a GPU scaler. PURE — the spawn lives in
76
- * the addon; only the parse is here so it is unit-tested.
77
- */
78
- function parseAvailableGpuScaleFilters(filtersStdout) {
79
- const present = /* @__PURE__ */ new Set();
80
- for (const filter of KNOWN_GPU_SCALE_FILTERS) if (new RegExp(`\\b${filter}\\b`).test(filtersStdout)) present.add(filter);
81
- return present;
82
- }
83
- /**
84
- * Whether the decode keeps frames on the GPU — needs `-hwaccel_output_format`
85
- * and an `hwdownload` in the filtergraph. VA-API / QSV always do (their decoded
86
- * surfaces live on the GPU); another backend only when a GPU scaler will run on
87
- * those surfaces (e.g. `videotoolbox` + `scale_vt`).
88
- */
89
- function keepFramesOnGpu(hwaccel, gpuScaleFilter) {
90
- return needsHwDownload(hwaccel) || gpuScaleFilter !== null;
91
- }
92
- /** Map a cap codec string to the ffmpeg `-f` input demuxer/format. */
93
- function codecToInputFormat(codec) {
94
- switch (codec.toLowerCase()) {
95
- case "h265":
96
- case "hevc": return "hevc";
97
- default: return "h264";
98
- }
99
- }
100
- /** Map the cap-level output format to a raw ffmpeg pixel format. */
101
- function rawPixelForFormat(outputFormat) {
102
- return outputFormat === "gray" ? "gray" : "rgb24";
103
- }
104
- /** Packed bytes-per-pixel for a raw output format (rgb24 = 3, gray = 1). */
105
- function channelsForPixel(pixel) {
106
- return pixel === "gray" ? 1 : 3;
107
- }
108
- /**
109
- * The maximum output width for a given `scale` divisor — matches the node-av
110
- * scaler's `maxW = floor(640 / scale)` rule so both decoders produce
111
- * comparably-sized frames for the motion / detection pipeline.
112
- */
113
- function maxDecodeWidth(scale) {
114
- return Math.max(2, Math.floor(640 / (scale > 1 ? scale : 1)));
115
- }
116
- /**
117
- * Compute exact output geometry from a known source resolution — the node-av
118
- * rule (`outWidth = min(srcW, floor(640/scale))`, `outHeight =
119
- * round(outWidth*srcH/srcW)`). Provided for callers that DO know the source
120
- * dimensions; the live broker path leaves them to ffmpeg (`-2` height).
121
- */
122
- function computeOutputGeometry(srcWidth, srcHeight, scale) {
123
- const width = Math.min(srcWidth, maxDecodeWidth(scale));
124
- return {
125
- width,
126
- height: Math.round(width * srcHeight / srcWidth)
127
- };
128
- }
129
- /** Build the `-vf` filtergraph string for the requested hwaccel + pixel format. */
130
- function buildVideoFilter(hwaccel, pixel, outWidth, outHeight, maxW, gpuScaleFilter) {
131
- const exact = outWidth !== void 0 && outWidth > 0 && outHeight !== void 0 && outHeight > 0;
132
- if (gpuScaleFilter !== null) return `${gpuScaleFilter}=${exact ? `w=${outWidth}:h=${outHeight}` : `w=min(iw\\,${maxW}):h=-2`},hwdownload,format=nv12,format=${pixel}`;
133
- const scaleAndFormat = `scale=${exact ? `${outWidth}:${outHeight}` : `min(iw\\,${maxW}):-2`},format=${pixel}`;
134
- return needsHwDownload(hwaccel) ? `hwdownload,format=nv12,${scaleAndFormat}` : scaleAndFormat;
135
- }
136
- /**
137
- * Build the full ffmpeg argv for a decode session. PURE — no spawn, no env
138
- * access — so the argument shape is asserted in unit tests.
139
- *
140
- * Notes:
141
- * - `-loglevel info -nostats`: the session parses the muxer's `Output #0`
142
- * stream line from stderr to learn the exact output W×H (the broker does not
143
- * pass source geometry for the shm path). `-nostats` suppresses the periodic
144
- * `frame= …` counter while keeping the one-shot Output block.
145
- * - The input is `pipe:0` (Annex-B packets written to stdin) in push mode, or
146
- * `options.inputUrl` (the owner's restream, dialed by ffmpeg itself —
147
- * `-rtsp_transport tcp` for rtsp URLs) in pull mode; the output is raw video
148
- * on `pipe:1` either way.
149
- */
150
- function buildFfmpegDecodeArgs(config, options) {
151
- const inputFormat = codecToInputFormat(config.codec);
152
- const pixel = rawPixelForFormat(config.outputFormat);
153
- const maxW = maxDecodeWidth(config.scale);
154
- const { hwaccel } = options;
155
- const gpuScaleFilter = options.gpuScaleFilter ?? null;
156
- const vf = buildVideoFilter(hwaccel, pixel, options.outWidth, options.outHeight, maxW, gpuScaleFilter);
157
- let hwaccelArgs;
158
- if (isSoftwareHwAccel(hwaccel) || hwaccel === null) hwaccelArgs = [];
159
- else {
160
- hwaccelArgs = ["-hwaccel", hwaccel];
161
- if (needsHwDownload(hwaccel)) hwaccelArgs.push("-hwaccel_device", options.renderDevice ?? "/dev/dri/renderD128");
162
- if (keepFramesOnGpu(hwaccel, gpuScaleFilter)) hwaccelArgs.push("-hwaccel_output_format", hwaccel);
163
- }
164
- const inputArgs = options.inputUrl !== void 0 ? [
165
- ...options.inputUrl.startsWith("rtsp") ? ["-rtsp_transport", "tcp"] : [],
166
- "-i",
167
- options.inputUrl
168
- ] : [
169
- "-f",
170
- inputFormat,
171
- "-i",
172
- "pipe:0"
173
- ];
174
- return [
175
- ...logBannerArgs("info"),
176
- "-nostats",
177
- "-fflags",
178
- "+nobuffer+flush_packets",
179
- "-flags",
180
- "low_delay",
181
- "-probesize",
182
- "1M",
183
- "-analyzeduration",
184
- "0",
185
- ...hwaccelArgs,
186
- ...inputArgs,
187
- "-vf",
188
- vf,
189
- "-f",
190
- "rawvideo",
191
- "-pix_fmt",
192
- pixel,
193
- "pipe:1"
194
- ];
195
- }
196
- /**
197
- * Parse the exact output geometry from ffmpeg's stderr. The muxer prints an
198
- * `Output #0, rawvideo …` block whose `Stream #0:0: Video: rawvideo …, <fmt>,
199
- * WIDTHxHEIGHT …` line carries the scaled dimensions. Gating on the `Output #`
200
- * marker avoids picking up the INPUT stream's (source) resolution, which is
201
- * printed earlier.
202
- *
203
- * Returns `null` until the Output block with a `WxH` token has been seen.
204
- */
205
- function parseFfmpegOutputDims(stderr) {
206
- const outputIdx = stderr.indexOf("Output #");
207
- if (outputIdx < 0) return null;
208
- const match = stderr.slice(outputIdx).match(/(\d{2,5})x(\d{2,5})/);
209
- if (!match) return null;
210
- const width = Number(match[1]);
211
- const height = Number(match[2]);
212
- if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) return null;
213
- return {
214
- width,
215
- height
216
- };
217
- }
218
- /**
219
- * Pick the first kernel-preferred `-hwaccel` method the configured ffmpeg build
220
- * actually supports, or `null` (software) when it supports none. Mirrors the
221
- * broker's `pickDecodeHwAccel` (`ffmpeg-invocation.ts`) so the decoder gates
222
- * decode-hwaccel against the same probed evidence:
223
- * - empty `preferred` → `null` (software);
224
- * - empty `supportedMethods` (probe miss) → keep the top preference and let the
225
- * session's per-child software fallback cover a genuine failure;
226
- * - otherwise the first preference the build supports, or `null`.
227
- */
228
- function pickDecodeHwAccel(preferred, supportedMethods) {
229
- if (preferred.length === 0) return null;
230
- if (supportedMethods.length === 0) return preferred[0] ?? null;
231
- const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
232
- return preferred.find((name) => supported.has(name.toLowerCase())) ?? null;
233
- }
234
- /**
235
- * Canonical decode-hwaccel preference order for the ffmpeg-subprocess decoder.
236
- *
237
- * `vaapi` is ranked ABOVE `qsv` deliberately: on the Intel hub the `qsv` decode
238
- * child exits early (`code=171`, no frames) while the `vaapi` path decodes 8MP
239
- * h264 cleanly at ~50fps. The kernel probe (`resolveHwAccel`) tends to surface
240
- * `qsv` first on Intel, so we re-rank its result through this table before
241
- * building the attempt chain. `videotoolbox` leads for macOS; `cuda` / `nvdec`
242
- * trail for NVIDIA. A method not listed here keeps its incoming relative order,
243
- * placed AFTER every ranked one.
244
- */
245
- var DECODE_HWACCEL_RANK = [
246
- "videotoolbox",
247
- "vaapi",
248
- "qsv",
249
- "cuda",
250
- "nvdec",
251
- "d3d11va",
252
- "dxva2",
253
- "amf",
254
- "vdpau",
255
- "drm"
256
- ];
257
- /** Rank index for {@link DECODE_HWACCEL_RANK} — unranked methods sort last. */
258
- function decodeHwAccelRankIndex(method) {
259
- const idx = DECODE_HWACCEL_RANK.indexOf(method.toLowerCase());
260
- return idx < 0 ? DECODE_HWACCEL_RANK.length : idx;
261
- }
262
- /**
263
- * Build the ORDERED list of hardware `-hwaccel` methods to attempt for an
264
- * `'auto'` decode session, best-first. The session walks this chain on an
265
- * early-exit-with-no-frames and only falls to software once it is exhausted.
266
- *
267
- * Strategy:
268
- * - Seed candidates from the kernel-preferred order UNION the canonical rank
269
- * UNION the build's supported list, so a method the kernel omitted but the
270
- * build supports (the Intel `vaapi` case — kernel surfaces only `qsv`) is
271
- * still attempted.
272
- * - When the build's supported-method list is known (non-empty), keep only
273
- * methods it actually offers; on a probe miss (empty) keep just the kernel
274
- * preferences (avoid blindly spawning every backend).
275
- * - Re-rank the survivors by {@link DECODE_HWACCEL_RANK} so `vaapi` precedes
276
- * `qsv`. A supported method not in the rank table keeps its position AFTER
277
- * every ranked one (stable sort). Returns lowercased method names; empty ⇒
278
- * pure software.
279
- */
280
- function rankDecodeHwAccels(preferred, supportedMethods) {
281
- const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
282
- const hasSupportList = supported.size > 0;
283
- const pool = hasSupportList ? [
284
- ...preferred,
285
- ...DECODE_HWACCEL_RANK,
286
- ...supportedMethods
287
- ] : [...preferred];
288
- const seen = /* @__PURE__ */ new Set();
289
- const candidates = [];
290
- for (const raw of pool) {
291
- const method = raw.toLowerCase();
292
- if (seen.has(method)) continue;
293
- seen.add(method);
294
- if (hasSupportList && !supported.has(method)) continue;
295
- candidates.push(method);
296
- }
297
- return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
298
- }
299
- //#endregion
300
8
  //#region src/decoder-ffmpeg/frame-stream-splitter.ts
301
9
  var FrameStreamSplitter = class {
302
10
  frameSize;
@@ -894,7 +602,8 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
894
602
  var FRAME_BUFFER_CAPACITY = 32;
895
603
  var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
896
604
  ...DEFAULT_DECODER_HWACCEL_CONFIG,
897
- backend: DEFAULT_DECODER_BACKEND
605
+ backend: DEFAULT_DECODER_BACKEND,
606
+ binaryPath: ""
898
607
  };
899
608
  /** The two selectable decoder backends, in default-first order. */
900
609
  var DECODER_BACKEND_OPTIONS = [{
@@ -956,27 +665,40 @@ var DecoderFfmpegAddon = class extends BaseAddon {
956
665
  title: "Hardware acceleration",
957
666
  tab: "decoder",
958
667
  description: "Backend used by ffmpeg-subprocess decoder sessions. \"Auto\" defers to the probed best (VA-API on Intel); \"Off\" forces pure software. Only the VA-API hardware path is validated — other concrete backends degrade to software. Changes apply to NEW sessions.",
959
- fields: [this.field({
960
- type: "select",
961
- key: "hwaccel",
962
- label: "Preferred backend",
963
- options: [...HWACCEL_OPTIONS],
964
- default: "auto",
965
- immediate: true
966
- }), this.field({
967
- type: "text",
968
- key: "probedBestHwaccel",
969
- label: "Probed best",
970
- description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
971
- readonlyField: true,
972
- default: "",
973
- perNode: true,
974
- actions: [{
975
- action: "reprobe-hwaccel",
976
- icon: "refresh-cw",
977
- tooltip: "Re-probe hwaccel"
978
- }]
979
- })]
668
+ fields: [
669
+ this.field({
670
+ type: "select",
671
+ key: "hwaccel",
672
+ label: "Preferred backend",
673
+ options: [...HWACCEL_OPTIONS],
674
+ default: "auto",
675
+ immediate: true,
676
+ perNode: true
677
+ }),
678
+ this.field({
679
+ type: "text",
680
+ key: "probedBestHwaccel",
681
+ label: "Probed best",
682
+ description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
683
+ readonlyField: true,
684
+ default: "",
685
+ perNode: true,
686
+ actions: [{
687
+ action: "reprobe-hwaccel",
688
+ icon: "refresh-cw",
689
+ tooltip: "Re-probe hwaccel"
690
+ }]
691
+ }),
692
+ this.field({
693
+ type: "text",
694
+ key: "binaryPath",
695
+ label: "ffmpeg binary path (override)",
696
+ description: "Optional per-node override for the ffmpeg binary. Leave empty to auto-provision a portable static ffmpeg on this node (recommended). Per-node because the path differs per OS/arch.",
697
+ placeholder: "ffmpeg",
698
+ default: "",
699
+ perNode: true
700
+ })
701
+ ]
980
702
  }] });
981
703
  }
982
704
  async onInitialize() {
@@ -1018,10 +740,8 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1018
740
  * decoder cap and a later session spawn re-runs the ensure.
1019
741
  */
1020
742
  async resolveFfmpegBinaryPath() {
1021
- try {
1022
- const bp = (await this.ctx.settings?.getSection("ffmpeg") ?? {})["binaryPath"];
1023
- if (typeof bp === "string" && bp.trim().length > 0) return bp;
1024
- } catch {}
743
+ const override = this.config.binaryPath;
744
+ if (typeof override === "string" && override.trim().length > 0) return override;
1025
745
  try {
1026
746
  return await this.ctx.deps.ensureFfmpeg();
1027
747
  } catch (err) {
@@ -1037,21 +757,51 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1037
757
  * - `'none'` → `[]` (pure software).
1038
758
  * - explicit backend → `[<backend>]` (forced; the session still falls to
1039
759
  * software if that one child fails).
1040
- * - `'auto'` → asks `platform-probe.resolveHwAccel` for the kernel-preferred
1041
- * order + the node's supported `-hwaccel` methods
1042
- * (`getHardwareDecodeAccels`), then ranks them via {@link rankDecodeHwAccels}
1043
- * (vaapi ABOVE qsv, since qsv is broken on the Intel stack while vaapi
1044
- * works). On a probe error `[]` (software).
760
+ * - `'auto'` → the LOCAL node's preferred order from `ctx.kernel.hwaccel`
761
+ * the in-process resolver every forked child (hub / worker / agent) is
762
+ * given, which derives the backend from `os.platform()` + a GPU-vendor probe
763
+ * (darwin `videotoolbox`; Intel → `qsv,vaapi`; NVIDIA `cuda,nvdec`).
764
+ * Ranked via {@link rankDecodeHwAccels} (vaapi ABOVE qsv, since qsv is broken
765
+ * on the Intel stack while vaapi works). On a resolver error → `[]`
766
+ * (software).
767
+ *
768
+ * Two things this deliberately does NOT do:
769
+ * - It does NOT call the `platform-probe` singleton cap. Its per-node
770
+ * `nodePin` does not survive a forked agent child's link chain, so a pinned
771
+ * read resolves to the HUB's elected singleton and returns the hub's
772
+ * hardware — a Mac decoder was handed the hub's Intel `vaapi/qsv/...` chain
773
+ * (no `videotoolbox`) and fell all the way to software h265.
774
+ * - It does NOT probe `ffmpeg -hwaccels` (nor node-av) to intersect the list.
775
+ * The accelerator is deterministic from the host, which the kernel resolver
776
+ * already knows in-process — no subprocess, no cross-process round-trip. The
777
+ * rare case where the resolved backend is not usable by the actual ffmpeg
778
+ * build is covered by the session's per-child runtime fallback (advance to
779
+ * the next chain entry, else software). This mirrors how Scrypted resolves
780
+ * hwaccel (static `os.platform()` map + try-then-fall-back), not a native
781
+ * capability enumeration.
782
+ *
783
+ * macOS special-case: the resolved `videotoolbox` is emitted as `-hwaccel
784
+ * auto`, NOT explicit `-hwaccel videotoolbox`. Explicit videotoolbox tears the
785
+ * ffmpeg child down under MULTIPLE concurrent decode streams (documented by
786
+ * Scrypted NVR's ffmpeg path; our Mac decodes many cameras at once — exactly
787
+ * that case). `auto` lets ffmpeg pick VideoToolbox internally and auto-download
788
+ * frames to software (no GPU surface to dispose, no `scale_vt`). The in-process
789
+ * node-av decoder (`decoder-nodeav`) still uses explicit videotoolbox — this
790
+ * substitution is specific to the ffmpeg-subprocess decode.
1045
791
  */
1046
792
  async resolveDecodeHwaccelChain() {
1047
793
  const pref = this.config.hwaccel;
1048
794
  if (pref === "none") return [];
1049
795
  if (pref !== "auto") return [pref];
1050
796
  try {
1051
- const pin = nodePin(this.resolveLocalNodeId());
1052
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: null }, pin);
1053
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query(void 0, pin);
1054
- return rankDecodeHwAccels(res.preferred, supported.methods);
797
+ const resolver = this.ctx.kernel.hwaccel;
798
+ if (!resolver) {
799
+ this.ctx.logger.warn("ffmpeg: no local kernel hwaccel resolver — software decode");
800
+ return [];
801
+ }
802
+ const chain = rankDecodeHwAccels((await resolver.resolve(null)).preferred, []);
803
+ if (process.platform === "darwin") return chain.map((method) => method === "videotoolbox" ? "auto" : method);
804
+ return chain;
1055
805
  } catch (err) {
1056
806
  this.ctx.logger.warn("ffmpeg: decode-hwaccel probe failed — software decode", { meta: { error: err instanceof Error ? err.message : String(err) } });
1057
807
  return [];
@@ -1066,7 +816,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1066
816
  }
1067
817
  try {
1068
818
  const res = await resolver.resolve();
1069
- const backend = res.preferred[0] ?? "none";
819
+ const backend = rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
1070
820
  await this.updateGlobalSettings({ probedBestHwaccel: backend });
1071
821
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
1072
822
  backend,
@@ -2,8 +2,9 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Biq62zt4.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-9J0wCdLF.js");
5
+ const require_dist = require("../dist-Cwc0TUQr.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-8LLV-cvH.js");
7
+ const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
7
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
8
9
  let node_crypto = require("node:crypto");
9
10
  //#region src/decoder-nodeav/scaler-geometry.ts
@@ -919,7 +920,8 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
919
920
  label: "Preferred backend",
920
921
  options: [...require_dist.HWACCEL_OPTIONS],
921
922
  default: "auto",
922
- immediate: true
923
+ immediate: true,
924
+ perNode: true
923
925
  }), this.field({
924
926
  type: "text",
925
927
  key: "probedBestHwaccel",
@@ -976,7 +978,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
976
978
  }
977
979
  try {
978
980
  const res = await resolver.resolve();
979
- const backend = res.preferred[0] ?? "none";
981
+ const backend = require_ffmpeg_args.rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
980
982
  await this.updateGlobalSettings({ probedBestHwaccel: backend });
981
983
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
982
984
  backend,
@@ -1,5 +1,6 @@
1
- import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-C6_wgXqF.mjs";
2
- import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-Cs9vby6v.mjs";
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-ClEWjiRU.mjs";
3
+ import { f as rankDecodeHwAccels } from "../ffmpeg-args-C5GPp8Cw.mjs";
3
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
4
5
  import { randomUUID } from "node:crypto";
5
6
  //#region src/decoder-nodeav/scaler-geometry.ts
@@ -915,7 +916,8 @@ var DecoderNodeAvAddon = class extends BaseAddon {
915
916
  label: "Preferred backend",
916
917
  options: [...HWACCEL_OPTIONS],
917
918
  default: "auto",
918
- immediate: true
919
+ immediate: true,
920
+ perNode: true
919
921
  }), this.field({
920
922
  type: "text",
921
923
  key: "probedBestHwaccel",
@@ -972,7 +974,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
972
974
  }
973
975
  try {
974
976
  const res = await resolver.resolve();
975
- const backend = res.preferred[0] ?? "none";
977
+ const backend = rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
976
978
  await this.updateGlobalSettings({ probedBestHwaccel: backend });
977
979
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
978
980
  backend,