@camstack/addon-pipeline 1.1.27 → 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 (49) hide show
  1. package/dist/audio-analyzer/index.js +7 -6
  2. package/dist/audio-analyzer/index.mjs +7 -6
  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 +138 -356
  6. package/dist/decoder-ffmpeg/index.mjs +118 -336
  7. package/dist/decoder-nodeav/index.js +17 -10
  8. package/dist/decoder-nodeav/index.mjs +17 -10
  9. package/dist/detection-pipeline/index.js +341 -379
  10. package/dist/detection-pipeline/index.mjs +341 -379
  11. package/dist/{dist-DAIlCdAx.js → dist-Cwc0TUQr.js} +378 -76
  12. package/dist/{dist-CgEP_0OL.mjs → dist-DjuGmyG9.mjs} +379 -65
  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-Dq20KtKL.mjs → frame-handle-plane-Bkxz-TTD.mjs} +13 -5
  18. package/dist/{frame-handle-plane-DtTRX_0n.js → frame-handle-plane-DQNCTrpC.js} +13 -5
  19. package/dist/{frame-ring-sink-BbzrqTWj.js → frame-ring-sink-8LLV-cvH.js} +128 -23
  20. package/dist/{frame-ring-sink-B_NvPTJZ.mjs → frame-ring-sink-ClEWjiRU.mjs} +117 -24
  21. package/dist/motion-wasm/index.js +55 -3
  22. package/dist/motion-wasm/index.mjs +55 -3
  23. package/dist/node-topology-platform-BkR_k6WT.mjs +15 -0
  24. package/dist/node-topology-platform-CFZ7F4xW.js +20 -0
  25. package/dist/pipeline-runner/index.js +50 -16
  26. package/dist/pipeline-runner/index.mjs +50 -16
  27. package/dist/recorder/index.js +36 -40
  28. package/dist/recorder/index.mjs +36 -40
  29. package/dist/stream-broker/_stub.js +2 -2
  30. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
  31. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +26 -0
  32. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs} +1 -1
  33. package/dist/stream-broker/{hostInit-DyLqyJaS.mjs → hostInit-B_b3PIZB.mjs} +3 -3
  34. package/dist/stream-broker/index.js +198 -115
  35. package/dist/stream-broker/index.mjs +189 -106
  36. package/dist/stream-broker/remoteEntry.js +1 -1
  37. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
  38. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CoLjNiUN.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
  39. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
  40. package/embed-dist/assets/index-C5UpuPr8.css +2 -0
  41. package/embed-dist/assets/{index-C-pL8ETk.js → index-CpUy8OIE.js} +10 -10
  42. package/embed-dist/index.html +2 -2
  43. package/package.json +1 -1
  44. package/python/inference_pool.py +93 -102
  45. package/python/test_inference_pool_device_selection.py +43 -38
  46. package/dist/frame-dropper-7RTo_YyG.js +0 -68
  47. package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
  48. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +0 -26
  49. package/embed-dist/assets/index-DrJ0ee3f.css +0 -2
@@ -2,305 +2,13 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DAIlCdAx.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-BbzrqTWj.js");
7
- const require_frame_dropper = require("../frame-dropper-7RTo_YyG.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");
8
+ const require_frame_dropper = require("../frame-dropper-DKLM6pMz.js");
8
9
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
10
  let node_crypto = require("node:crypto");
10
11
  let node_child_process = require("node:child_process");
11
- //#region src/decoder-ffmpeg/ffmpeg-args.ts
12
- /**
13
- * Pure, side-effect-free ffmpeg argument construction for the
14
- * `decoder-ffmpeg` addon.
15
- *
16
- * The addon decodes video in an **ffmpeg subprocess** (unlike `decoder-nodeav`
17
- * which decodes in-process via node-av's native bindings). On the Intel hub,
18
- * node-av's VA-API decode path SIGBUSes (uncatchable) on 8MP h264 and takes the
19
- * whole decoder runner down; ffmpeg-as-subprocess decodes the same hardware
20
- * cleanly because a crash is isolated to the child process.
21
- *
22
- * These helpers are extracted from the session so the argument shape is
23
- * unit-testable without spawning a real ffmpeg binary.
24
- *
25
- * Validated working command on the Intel hub (renderD128, 8MP h264, ~50fps,
26
- * exit 0):
27
- *
28
- * ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 \
29
- * -hwaccel_output_format vaapi -f h264 -i <in> \
30
- * -vf hwdownload,format=nv12,scale=640:360,format=gray \
31
- * -f rawvideo -y /dev/null
32
- */
33
- /** Default DRM render node used for the VA-API / QSV hardware decode path. */
34
- var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
35
- /** Whether a resolved hwaccel value means "decode in software" (no `-hwaccel`). */
36
- function isSoftwareHwAccel(hwaccel) {
37
- return hwaccel === null || hwaccel === "" || hwaccel === "none";
38
- }
39
- /** Backends whose decoded frames stay on the GPU and need an explicit
40
- * `-hwaccel_output_format <m>` + `hwdownload,format=nv12` chain. */
41
- function needsHwDownload(hwaccel) {
42
- return hwaccel === "vaapi" || hwaccel === "qsv";
43
- }
44
- /** Decode backend → its GPU scaler filter. */
45
- var GPU_SCALE_FILTER_BY_BACKEND = {
46
- vaapi: "scale_vaapi",
47
- qsv: "scale_qsv",
48
- videotoolbox: "scale_vt"
49
- };
50
- /** Every GPU scaler this addon knows how to drive — used to scan `-filters`. */
51
- var KNOWN_GPU_SCALE_FILTERS = [
52
- "scale_vaapi",
53
- "scale_qsv",
54
- "scale_vt"
55
- ];
56
- /**
57
- * The GPU scaler that pairs with a decode backend, ignoring build support — a
58
- * static hwaccel → filter map. Returns null for software / a backend with no
59
- * GPU scaler (e.g. `cuda`, `drm`). Case-insensitive.
60
- */
61
- function gpuScaleFilterForBackend(hwaccel) {
62
- if (hwaccel === null) return null;
63
- return GPU_SCALE_FILTER_BY_BACKEND[hwaccel.toLowerCase()] ?? null;
64
- }
65
- /**
66
- * Resolve the GPU scaler to use for a decode backend GIVEN the filters the
67
- * configured ffmpeg build actually offers ({@link parseAvailableGpuScaleFilters}).
68
- * Returns the filter when the backend maps to one AND the build has it; null
69
- * otherwise — the caller then emits the CPU-scale chain (graceful fallback).
70
- */
71
- function resolveGpuScaleFilter(hwaccel, availableFilters) {
72
- const filter = gpuScaleFilterForBackend(hwaccel);
73
- if (filter === null) return null;
74
- return availableFilters.has(filter) ? filter : null;
75
- }
76
- /**
77
- * Parse `ffmpeg -filters` stdout into the set of {@link KNOWN_GPU_SCALE_FILTERS}
78
- * the build offers. Matches whole filter tokens (word-boundary) so the plain
79
- * `scale` filter is never mistaken for a GPU scaler. PURE — the spawn lives in
80
- * the addon; only the parse is here so it is unit-tested.
81
- */
82
- function parseAvailableGpuScaleFilters(filtersStdout) {
83
- const present = /* @__PURE__ */ new Set();
84
- for (const filter of KNOWN_GPU_SCALE_FILTERS) if (new RegExp(`\\b${filter}\\b`).test(filtersStdout)) present.add(filter);
85
- return present;
86
- }
87
- /**
88
- * Whether the decode keeps frames on the GPU — needs `-hwaccel_output_format`
89
- * and an `hwdownload` in the filtergraph. VA-API / QSV always do (their decoded
90
- * surfaces live on the GPU); another backend only when a GPU scaler will run on
91
- * those surfaces (e.g. `videotoolbox` + `scale_vt`).
92
- */
93
- function keepFramesOnGpu(hwaccel, gpuScaleFilter) {
94
- return needsHwDownload(hwaccel) || gpuScaleFilter !== null;
95
- }
96
- /** Map a cap codec string to the ffmpeg `-f` input demuxer/format. */
97
- function codecToInputFormat(codec) {
98
- switch (codec.toLowerCase()) {
99
- case "h265":
100
- case "hevc": return "hevc";
101
- default: return "h264";
102
- }
103
- }
104
- /** Map the cap-level output format to a raw ffmpeg pixel format. */
105
- function rawPixelForFormat(outputFormat) {
106
- return outputFormat === "gray" ? "gray" : "rgb24";
107
- }
108
- /** Packed bytes-per-pixel for a raw output format (rgb24 = 3, gray = 1). */
109
- function channelsForPixel(pixel) {
110
- return pixel === "gray" ? 1 : 3;
111
- }
112
- /**
113
- * The maximum output width for a given `scale` divisor — matches the node-av
114
- * scaler's `maxW = floor(640 / scale)` rule so both decoders produce
115
- * comparably-sized frames for the motion / detection pipeline.
116
- */
117
- function maxDecodeWidth(scale) {
118
- return Math.max(2, Math.floor(640 / (scale > 1 ? scale : 1)));
119
- }
120
- /**
121
- * Compute exact output geometry from a known source resolution — the node-av
122
- * rule (`outWidth = min(srcW, floor(640/scale))`, `outHeight =
123
- * round(outWidth*srcH/srcW)`). Provided for callers that DO know the source
124
- * dimensions; the live broker path leaves them to ffmpeg (`-2` height).
125
- */
126
- function computeOutputGeometry(srcWidth, srcHeight, scale) {
127
- const width = Math.min(srcWidth, maxDecodeWidth(scale));
128
- return {
129
- width,
130
- height: Math.round(width * srcHeight / srcWidth)
131
- };
132
- }
133
- /** Build the `-vf` filtergraph string for the requested hwaccel + pixel format. */
134
- function buildVideoFilter(hwaccel, pixel, outWidth, outHeight, maxW, gpuScaleFilter) {
135
- const exact = outWidth !== void 0 && outWidth > 0 && outHeight !== void 0 && outHeight > 0;
136
- if (gpuScaleFilter !== null) return `${gpuScaleFilter}=${exact ? `w=${outWidth}:h=${outHeight}` : `w=min(iw\\,${maxW}):h=-2`},hwdownload,format=nv12,format=${pixel}`;
137
- const scaleAndFormat = `scale=${exact ? `${outWidth}:${outHeight}` : `min(iw\\,${maxW}):-2`},format=${pixel}`;
138
- return needsHwDownload(hwaccel) ? `hwdownload,format=nv12,${scaleAndFormat}` : scaleAndFormat;
139
- }
140
- /**
141
- * Build the full ffmpeg argv for a decode session. PURE — no spawn, no env
142
- * access — so the argument shape is asserted in unit tests.
143
- *
144
- * Notes:
145
- * - `-loglevel info -nostats`: the session parses the muxer's `Output #0`
146
- * stream line from stderr to learn the exact output W×H (the broker does not
147
- * pass source geometry for the shm path). `-nostats` suppresses the periodic
148
- * `frame= …` counter while keeping the one-shot Output block.
149
- * - The input is `pipe:0` (Annex-B packets written to stdin) in push mode, or
150
- * `options.inputUrl` (the owner's restream, dialed by ffmpeg itself —
151
- * `-rtsp_transport tcp` for rtsp URLs) in pull mode; the output is raw video
152
- * on `pipe:1` either way.
153
- */
154
- function buildFfmpegDecodeArgs(config, options) {
155
- const inputFormat = codecToInputFormat(config.codec);
156
- const pixel = rawPixelForFormat(config.outputFormat);
157
- const maxW = maxDecodeWidth(config.scale);
158
- const { hwaccel } = options;
159
- const gpuScaleFilter = options.gpuScaleFilter ?? null;
160
- const vf = buildVideoFilter(hwaccel, pixel, options.outWidth, options.outHeight, maxW, gpuScaleFilter);
161
- let hwaccelArgs;
162
- if (isSoftwareHwAccel(hwaccel) || hwaccel === null) hwaccelArgs = [];
163
- else {
164
- hwaccelArgs = ["-hwaccel", hwaccel];
165
- if (needsHwDownload(hwaccel)) hwaccelArgs.push("-hwaccel_device", options.renderDevice ?? "/dev/dri/renderD128");
166
- if (keepFramesOnGpu(hwaccel, gpuScaleFilter)) hwaccelArgs.push("-hwaccel_output_format", hwaccel);
167
- }
168
- const inputArgs = options.inputUrl !== void 0 ? [
169
- ...options.inputUrl.startsWith("rtsp") ? ["-rtsp_transport", "tcp"] : [],
170
- "-i",
171
- options.inputUrl
172
- ] : [
173
- "-f",
174
- inputFormat,
175
- "-i",
176
- "pipe:0"
177
- ];
178
- return [
179
- ...require_frame_dropper.logBannerArgs("info"),
180
- "-nostats",
181
- "-fflags",
182
- "+nobuffer+flush_packets",
183
- "-flags",
184
- "low_delay",
185
- "-probesize",
186
- "1M",
187
- "-analyzeduration",
188
- "0",
189
- ...hwaccelArgs,
190
- ...inputArgs,
191
- "-vf",
192
- vf,
193
- "-f",
194
- "rawvideo",
195
- "-pix_fmt",
196
- pixel,
197
- "pipe:1"
198
- ];
199
- }
200
- /**
201
- * Parse the exact output geometry from ffmpeg's stderr. The muxer prints an
202
- * `Output #0, rawvideo …` block whose `Stream #0:0: Video: rawvideo …, <fmt>,
203
- * WIDTHxHEIGHT …` line carries the scaled dimensions. Gating on the `Output #`
204
- * marker avoids picking up the INPUT stream's (source) resolution, which is
205
- * printed earlier.
206
- *
207
- * Returns `null` until the Output block with a `WxH` token has been seen.
208
- */
209
- function parseFfmpegOutputDims(stderr) {
210
- const outputIdx = stderr.indexOf("Output #");
211
- if (outputIdx < 0) return null;
212
- const match = stderr.slice(outputIdx).match(/(\d{2,5})x(\d{2,5})/);
213
- if (!match) return null;
214
- const width = Number(match[1]);
215
- const height = Number(match[2]);
216
- if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) return null;
217
- return {
218
- width,
219
- height
220
- };
221
- }
222
- /**
223
- * Pick the first kernel-preferred `-hwaccel` method the configured ffmpeg build
224
- * actually supports, or `null` (software) when it supports none. Mirrors the
225
- * broker's `pickDecodeHwAccel` (`ffmpeg-invocation.ts`) so the decoder gates
226
- * decode-hwaccel against the same probed evidence:
227
- * - empty `preferred` → `null` (software);
228
- * - empty `supportedMethods` (probe miss) → keep the top preference and let the
229
- * session's per-child software fallback cover a genuine failure;
230
- * - otherwise the first preference the build supports, or `null`.
231
- */
232
- function pickDecodeHwAccel(preferred, supportedMethods) {
233
- if (preferred.length === 0) return null;
234
- if (supportedMethods.length === 0) return preferred[0] ?? null;
235
- const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
236
- return preferred.find((name) => supported.has(name.toLowerCase())) ?? null;
237
- }
238
- /**
239
- * Canonical decode-hwaccel preference order for the ffmpeg-subprocess decoder.
240
- *
241
- * `vaapi` is ranked ABOVE `qsv` deliberately: on the Intel hub the `qsv` decode
242
- * child exits early (`code=171`, no frames) while the `vaapi` path decodes 8MP
243
- * h264 cleanly at ~50fps. The kernel probe (`resolveHwAccel`) tends to surface
244
- * `qsv` first on Intel, so we re-rank its result through this table before
245
- * building the attempt chain. `videotoolbox` leads for macOS; `cuda` / `nvdec`
246
- * trail for NVIDIA. A method not listed here keeps its incoming relative order,
247
- * placed AFTER every ranked one.
248
- */
249
- var DECODE_HWACCEL_RANK = [
250
- "videotoolbox",
251
- "vaapi",
252
- "qsv",
253
- "cuda",
254
- "nvdec",
255
- "d3d11va",
256
- "dxva2",
257
- "amf",
258
- "vdpau",
259
- "drm"
260
- ];
261
- /** Rank index for {@link DECODE_HWACCEL_RANK} — unranked methods sort last. */
262
- function decodeHwAccelRankIndex(method) {
263
- const idx = DECODE_HWACCEL_RANK.indexOf(method.toLowerCase());
264
- return idx < 0 ? DECODE_HWACCEL_RANK.length : idx;
265
- }
266
- /**
267
- * Build the ORDERED list of hardware `-hwaccel` methods to attempt for an
268
- * `'auto'` decode session, best-first. The session walks this chain on an
269
- * early-exit-with-no-frames and only falls to software once it is exhausted.
270
- *
271
- * Strategy:
272
- * - Seed candidates from the kernel-preferred order UNION the canonical rank
273
- * UNION the build's supported list, so a method the kernel omitted but the
274
- * build supports (the Intel `vaapi` case — kernel surfaces only `qsv`) is
275
- * still attempted.
276
- * - When the build's supported-method list is known (non-empty), keep only
277
- * methods it actually offers; on a probe miss (empty) keep just the kernel
278
- * preferences (avoid blindly spawning every backend).
279
- * - Re-rank the survivors by {@link DECODE_HWACCEL_RANK} so `vaapi` precedes
280
- * `qsv`. A supported method not in the rank table keeps its position AFTER
281
- * every ranked one (stable sort). Returns lowercased method names; empty ⇒
282
- * pure software.
283
- */
284
- function rankDecodeHwAccels(preferred, supportedMethods) {
285
- const supported = new Set(supportedMethods.map((m) => m.toLowerCase()));
286
- const hasSupportList = supported.size > 0;
287
- const pool = hasSupportList ? [
288
- ...preferred,
289
- ...DECODE_HWACCEL_RANK,
290
- ...supportedMethods
291
- ] : [...preferred];
292
- const seen = /* @__PURE__ */ new Set();
293
- const candidates = [];
294
- for (const raw of pool) {
295
- const method = raw.toLowerCase();
296
- if (seen.has(method)) continue;
297
- seen.add(method);
298
- if (hasSupportList && !supported.has(method)) continue;
299
- candidates.push(method);
300
- }
301
- return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
302
- }
303
- //#endregion
304
12
  //#region src/decoder-ffmpeg/frame-stream-splitter.ts
305
13
  var FrameStreamSplitter = class {
306
14
  frameSize;
@@ -490,13 +198,13 @@ var FfmpegDecoderSession = class {
490
198
  this.nodeId = options?.nodeId ?? "local";
491
199
  this.renderDevice = options?.renderDevice;
492
200
  this.ffmpegPath = options?.ffmpegPath ?? "ffmpeg";
493
- const seededChain = options?.hwaccelChain ?? (options?.hwaccel != null && !isSoftwareHwAccel(options.hwaccel) ? [options.hwaccel] : []);
494
- this.hwaccelChain = seededChain.filter((m) => !isSoftwareHwAccel(m));
201
+ const seededChain = options?.hwaccelChain ?? (options?.hwaccel != null && !require_ffmpeg_args.isSoftwareHwAccel(options.hwaccel) ? [options.hwaccel] : []);
202
+ this.hwaccelChain = seededChain.filter((m) => !require_ffmpeg_args.isSoftwareHwAccel(m));
495
203
  this.hwaccel = this.currentHwAccel();
496
204
  this.availableGpuScaleFilters = options?.availableGpuScaleFilters ?? /* @__PURE__ */ new Set();
497
205
  this.onGpuScaleUnsupported = options?.onGpuScaleUnsupported;
498
- this.pixel = rawPixelForFormat(config.outputFormat);
499
- this.channels = channelsForPixel(this.pixel);
206
+ this.pixel = require_ffmpeg_args.rawPixelForFormat(config.outputFormat);
207
+ this.channels = require_ffmpeg_args.channelsForPixel(this.pixel);
500
208
  this.frameFormat = this.pixel === "gray" ? "gray" : "rgb";
501
209
  this.frameDropper = new require_frame_dropper.FrameDropper(config.maxFps);
502
210
  }
@@ -551,8 +259,8 @@ var FfmpegDecoderSession = class {
551
259
  this.outWidth = 0;
552
260
  this.outHeight = 0;
553
261
  this.frameSize = 0;
554
- this.activeGpuScaleFilter = this.gpuScaleEnabled ? resolveGpuScaleFilter(this.hwaccel, this.availableGpuScaleFilters) : null;
555
- const args = buildFfmpegDecodeArgs({
262
+ this.activeGpuScaleFilter = this.gpuScaleEnabled ? require_ffmpeg_args.resolveGpuScaleFilter(this.hwaccel, this.availableGpuScaleFilters) : null;
263
+ const args = require_ffmpeg_args.buildFfmpegDecodeArgs({
556
264
  codec: this.config.codec,
557
265
  scale: this.config.scale,
558
266
  outputFormat: this.config.outputFormat
@@ -621,7 +329,7 @@ var FfmpegDecoderSession = class {
621
329
  handleExit(code, signal) {
622
330
  if (this.destroyed) return;
623
331
  const diedFast = Date.now() - this.spawnedAtMs < HWACCEL_FALLBACK_WINDOW_MS;
624
- if (!isSoftwareHwAccel(this.hwaccel) && this.outputFrames === 0 && diedFast) {
332
+ if (!require_ffmpeg_args.isSoftwareHwAccel(this.hwaccel) && this.outputFrames === 0 && diedFast) {
625
333
  if (this.activeGpuScaleFilter !== null) {
626
334
  const brokenFilter = this.activeGpuScaleFilter;
627
335
  this.gpuScaleEnabled = false;
@@ -640,7 +348,7 @@ var FfmpegDecoderSession = class {
640
348
  this.hwaccelIndex += 1;
641
349
  this.hwaccel = this.currentHwAccel();
642
350
  this.gpuScaleEnabled = true;
643
- const next = isSoftwareHwAccel(this.hwaccel) ? "software" : this.hwaccel;
351
+ const next = require_ffmpeg_args.isSoftwareHwAccel(this.hwaccel) ? "software" : this.hwaccel;
644
352
  this.logger.warn("ffmpeg decoder: hwaccel child exited early with no frames — trying next decode path", { meta: {
645
353
  failed,
646
354
  next,
@@ -889,13 +597,26 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
889
597
  done(/* @__PURE__ */ new Set());
890
598
  });
891
599
  child.on("exit", () => {
892
- done(parseAvailableGpuScaleFilters(stdout));
600
+ done(require_ffmpeg_args.parseAvailableGpuScaleFilters(stdout));
893
601
  });
894
602
  });
895
603
  }
896
604
  //#endregion
897
605
  //#region src/decoder-ffmpeg/addon/index.ts
898
606
  var FRAME_BUFFER_CAPACITY = 32;
607
+ var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
608
+ ...require_dist.DEFAULT_DECODER_HWACCEL_CONFIG,
609
+ backend: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
610
+ binaryPath: ""
611
+ };
612
+ /** The two selectable decoder backends, in default-first order. */
613
+ var DECODER_BACKEND_OPTIONS = [{
614
+ value: "ffmpeg",
615
+ label: "ffmpeg (subprocess — crash-isolated, default)"
616
+ }, {
617
+ value: "nodeav",
618
+ label: "node-av (in-process FFmpeg bindings)"
619
+ }];
899
620
  var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
900
621
  sessions = /* @__PURE__ */ new Map();
901
622
  frameBuffers = /* @__PURE__ */ new Map();
@@ -926,38 +647,66 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
926
647
  */
927
648
  unsupportedGpuScaleFilters = /* @__PURE__ */ new Set();
928
649
  constructor() {
929
- super(require_dist.DEFAULT_DECODER_HWACCEL_CONFIG);
650
+ super(DEFAULT_DECODER_FFMPEG_ADDON_CONFIG);
930
651
  }
931
652
  globalSettingsSchema() {
932
653
  return this.schema({ sections: [{
933
- id: "hwaccel",
934
- title: "Hardware acceleration",
654
+ id: "backend",
655
+ title: "Decoder backend",
935
656
  tab: "decoder",
936
- 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.",
657
+ description: "Which decoder addon provides the decoder capability on this node per-node selection, stored centrally on the hub. Selection is decided once at addon init (registration-time, race-free), so a change applies at the next restart of the decoder addons on the node.",
937
658
  fields: [this.field({
938
659
  type: "select",
939
- key: "hwaccel",
940
- label: "Preferred backend",
941
- options: [...require_dist.HWACCEL_OPTIONS],
942
- default: "auto",
943
- immediate: true
944
- }), this.field({
945
- type: "text",
946
- key: "probedBestHwaccel",
947
- label: "Probed best",
948
- description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
949
- readonlyField: true,
950
- default: "",
951
- actions: [{
952
- action: "reprobe-hwaccel",
953
- icon: "refresh-cw",
954
- tooltip: "Re-probe hwaccel"
955
- }]
660
+ key: "backend",
661
+ label: "Backend",
662
+ options: [...DECODER_BACKEND_OPTIONS],
663
+ default: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
664
+ immediate: true,
665
+ perNode: true
956
666
  })]
667
+ }, {
668
+ id: "hwaccel",
669
+ title: "Hardware acceleration",
670
+ tab: "decoder",
671
+ 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.",
672
+ fields: [
673
+ this.field({
674
+ type: "select",
675
+ key: "hwaccel",
676
+ label: "Preferred backend",
677
+ options: [...require_dist.HWACCEL_OPTIONS],
678
+ default: "auto",
679
+ immediate: true,
680
+ perNode: true
681
+ }),
682
+ this.field({
683
+ type: "text",
684
+ key: "probedBestHwaccel",
685
+ label: "Probed best",
686
+ description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
687
+ readonlyField: true,
688
+ default: "",
689
+ perNode: true,
690
+ actions: [{
691
+ action: "reprobe-hwaccel",
692
+ icon: "refresh-cw",
693
+ tooltip: "Re-probe hwaccel"
694
+ }]
695
+ }),
696
+ this.field({
697
+ type: "text",
698
+ key: "binaryPath",
699
+ label: "ffmpeg binary path (override)",
700
+ 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.",
701
+ placeholder: "ffmpeg",
702
+ default: "",
703
+ perNode: true
704
+ })
705
+ ]
957
706
  }] });
958
707
  }
959
708
  async onInitialize() {
960
- const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.settings, this.resolveLocalNodeId(), this.ctx.logger);
709
+ const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
961
710
  if (backend !== "ffmpeg") {
962
711
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
963
712
  return [];
@@ -995,10 +744,8 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
995
744
  * decoder cap and a later session spawn re-runs the ensure.
996
745
  */
997
746
  async resolveFfmpegBinaryPath() {
998
- try {
999
- const bp = (await this.ctx.settings?.getSection("ffmpeg") ?? {})["binaryPath"];
1000
- if (typeof bp === "string" && bp.trim().length > 0) return bp;
1001
- } catch {}
747
+ const override = this.config.binaryPath;
748
+ if (typeof override === "string" && override.trim().length > 0) return override;
1002
749
  try {
1003
750
  return await this.ctx.deps.ensureFfmpeg();
1004
751
  } catch (err) {
@@ -1014,20 +761,51 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1014
761
  * - `'none'` → `[]` (pure software).
1015
762
  * - explicit backend → `[<backend>]` (forced; the session still falls to
1016
763
  * software if that one child fails).
1017
- * - `'auto'` → asks `platform-probe.resolveHwAccel` for the kernel-preferred
1018
- * order + the node's supported `-hwaccel` methods
1019
- * (`getHardwareDecodeAccels`), then ranks them via {@link rankDecodeHwAccels}
1020
- * (vaapi ABOVE qsv, since qsv is broken on the Intel stack while vaapi
1021
- * works). On a probe error `[]` (software).
764
+ * - `'auto'` → the LOCAL node's preferred order from `ctx.kernel.hwaccel`
765
+ * the in-process resolver every forked child (hub / worker / agent) is
766
+ * given, which derives the backend from `os.platform()` + a GPU-vendor probe
767
+ * (darwin `videotoolbox`; Intel → `qsv,vaapi`; NVIDIA `cuda,nvdec`).
768
+ * Ranked via {@link rankDecodeHwAccels} (vaapi ABOVE qsv, since qsv is broken
769
+ * on the Intel stack while vaapi works). On a resolver error → `[]`
770
+ * (software).
771
+ *
772
+ * Two things this deliberately does NOT do:
773
+ * - It does NOT call the `platform-probe` singleton cap. Its per-node
774
+ * `nodePin` does not survive a forked agent child's link chain, so a pinned
775
+ * read resolves to the HUB's elected singleton and returns the hub's
776
+ * hardware — a Mac decoder was handed the hub's Intel `vaapi/qsv/...` chain
777
+ * (no `videotoolbox`) and fell all the way to software h265.
778
+ * - It does NOT probe `ffmpeg -hwaccels` (nor node-av) to intersect the list.
779
+ * The accelerator is deterministic from the host, which the kernel resolver
780
+ * already knows in-process — no subprocess, no cross-process round-trip. The
781
+ * rare case where the resolved backend is not usable by the actual ffmpeg
782
+ * build is covered by the session's per-child runtime fallback (advance to
783
+ * the next chain entry, else software). This mirrors how Scrypted resolves
784
+ * hwaccel (static `os.platform()` map + try-then-fall-back), not a native
785
+ * capability enumeration.
786
+ *
787
+ * macOS special-case: the resolved `videotoolbox` is emitted as `-hwaccel
788
+ * auto`, NOT explicit `-hwaccel videotoolbox`. Explicit videotoolbox tears the
789
+ * ffmpeg child down under MULTIPLE concurrent decode streams (documented by
790
+ * Scrypted NVR's ffmpeg path; our Mac decodes many cameras at once — exactly
791
+ * that case). `auto` lets ffmpeg pick VideoToolbox internally and auto-download
792
+ * frames to software (no GPU surface to dispose, no `scale_vt`). The in-process
793
+ * node-av decoder (`decoder-nodeav`) still uses explicit videotoolbox — this
794
+ * substitution is specific to the ffmpeg-subprocess decode.
1022
795
  */
1023
796
  async resolveDecodeHwaccelChain() {
1024
797
  const pref = this.config.hwaccel;
1025
798
  if (pref === "none") return [];
1026
799
  if (pref !== "auto") return [pref];
1027
800
  try {
1028
- const res = await this.ctx.api.platformProbe.resolveHwAccel.query({ prefer: null });
1029
- const supported = await this.ctx.api.platformProbe.getHardwareDecodeAccels.query();
1030
- return rankDecodeHwAccels(res.preferred, supported.methods);
801
+ const resolver = this.ctx.kernel.hwaccel;
802
+ if (!resolver) {
803
+ this.ctx.logger.warn("ffmpeg: no local kernel hwaccel resolver — software decode");
804
+ return [];
805
+ }
806
+ const chain = require_ffmpeg_args.rankDecodeHwAccels((await resolver.resolve(null)).preferred, []);
807
+ if (process.platform === "darwin") return chain.map((method) => method === "videotoolbox" ? "auto" : method);
808
+ return chain;
1031
809
  } catch (err) {
1032
810
  this.ctx.logger.warn("ffmpeg: decode-hwaccel probe failed — software decode", { meta: { error: err instanceof Error ? err.message : String(err) } });
1033
811
  return [];
@@ -1037,13 +815,13 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1037
815
  const resolver = this.ctx.kernel.hwaccel;
1038
816
  if (!resolver) {
1039
817
  this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
1040
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
818
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
1041
819
  return { backend: "none" };
1042
820
  }
1043
821
  try {
1044
822
  const res = await resolver.resolve();
1045
- const backend = res.preferred[0] ?? "none";
1046
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend });
823
+ const backend = require_ffmpeg_args.rankDecodeHwAccels(res.preferred, [])[0] ?? "none";
824
+ await this.updateGlobalSettings({ probedBestHwaccel: backend });
1047
825
  this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", { meta: {
1048
826
  backend,
1049
827
  rationale: res.rationale,
@@ -1052,7 +830,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1052
830
  return { backend };
1053
831
  } catch (err) {
1054
832
  this.ctx.logger.warn("reprobeHwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1055
- await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
833
+ await this.updateGlobalSettings({ probedBestHwaccel: "none" });
1056
834
  return { backend: "none" };
1057
835
  }
1058
836
  }
@@ -1119,7 +897,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1119
897
  };
1120
898
  }
1121
899
  wireCallbackSink(sessionId, session) {
1122
- const ringBuffer = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
900
+ const ringBuffer = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1123
901
  this.frameBuffers.set(sessionId, ringBuffer);
1124
902
  return session.onFrame((frame) => {
1125
903
  const { format } = frame;
@@ -1137,7 +915,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1137
915
  });
1138
916
  }
1139
917
  wireShmSink(sessionId, session) {
1140
- const handleRing = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
918
+ const handleRing = new require_frame_ring_sink.NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
1141
919
  this.handleBuffers.set(sessionId, handleRing);
1142
920
  return session.onFrameHandle((frame) => {
1143
921
  handleRing.push(frame.handle);
@@ -1149,6 +927,8 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1149
927
  if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`);
1150
928
  const unsub = this.unsubscribers.get(sessionId);
1151
929
  if (unsub) unsub();
930
+ this.frameBuffers.get(sessionId)?.cancel();
931
+ this.handleBuffers.get(sessionId)?.cancel();
1152
932
  await session.destroy();
1153
933
  this.sessions.delete(sessionId);
1154
934
  this.frameBuffers.delete(sessionId);
@@ -1186,12 +966,14 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1186
966
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1187
967
  const ringBuffer = this.frameBuffers.get(input.sessionId);
1188
968
  if (!ringBuffer) return [];
969
+ if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
1189
970
  return ringBuffer.drain(input.maxCount);
1190
971
  }
1191
972
  async pullHandles(input) {
1192
973
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1193
974
  const handleRing = this.handleBuffers.get(input.sessionId);
1194
975
  if (!handleRing) return [];
976
+ if (handleRing.size === 0 && input.waitMs) await handleRing.waitForItem(input.waitMs);
1195
977
  return handleRing.drain(input.maxCount);
1196
978
  }
1197
979
  async updateConfig(input) {
@@ -1251,17 +1033,17 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1251
1033
  }
1252
1034
  };
1253
1035
  //#endregion
1254
- exports.DECODE_HWACCEL_RANK = DECODE_HWACCEL_RANK;
1255
- exports.DEFAULT_VAAPI_RENDER_DEVICE = DEFAULT_VAAPI_RENDER_DEVICE;
1036
+ exports.DECODE_HWACCEL_RANK = require_ffmpeg_args.DECODE_HWACCEL_RANK;
1037
+ exports.DEFAULT_VAAPI_RENDER_DEVICE = require_ffmpeg_args.DEFAULT_VAAPI_RENDER_DEVICE;
1256
1038
  exports.DecoderFfmpegAddon = DecoderFfmpegAddon;
1257
1039
  exports.FfmpegDecoderSession = FfmpegDecoderSession;
1258
- exports.buildFfmpegDecodeArgs = buildFfmpegDecodeArgs;
1259
- exports.channelsForPixel = channelsForPixel;
1260
- exports.codecToInputFormat = codecToInputFormat;
1261
- exports.computeOutputGeometry = computeOutputGeometry;
1040
+ exports.buildFfmpegDecodeArgs = require_ffmpeg_args.buildFfmpegDecodeArgs;
1041
+ exports.channelsForPixel = require_ffmpeg_args.channelsForPixel;
1042
+ exports.codecToInputFormat = require_ffmpeg_args.codecToInputFormat;
1043
+ exports.computeOutputGeometry = require_ffmpeg_args.computeOutputGeometry;
1262
1044
  exports.default = DecoderFfmpegAddon;
1263
- exports.maxDecodeWidth = maxDecodeWidth;
1264
- exports.parseFfmpegOutputDims = parseFfmpegOutputDims;
1265
- exports.pickDecodeHwAccel = pickDecodeHwAccel;
1266
- exports.rankDecodeHwAccels = rankDecodeHwAccels;
1267
- exports.rawPixelForFormat = rawPixelForFormat;
1045
+ exports.maxDecodeWidth = require_ffmpeg_args.maxDecodeWidth;
1046
+ exports.parseFfmpegOutputDims = require_ffmpeg_args.parseFfmpegOutputDims;
1047
+ exports.pickDecodeHwAccel = require_ffmpeg_args.pickDecodeHwAccel;
1048
+ exports.rankDecodeHwAccels = require_ffmpeg_args.rankDecodeHwAccels;
1049
+ exports.rawPixelForFormat = require_ffmpeg_args.rawPixelForFormat;