@camstack/addon-decoder-ffmpeg 1.1.2 → 1.1.4

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 (3) hide show
  1. package/dist/index.js +143 -97
  2. package/dist/index.mjs +143 -97
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -13052,7 +13052,9 @@ var decoderCapability = {
13052
13052
  id: string(),
13053
13053
  name: string(),
13054
13054
  isPullMode: boolean().optional(),
13055
- priority: number().optional()
13055
+ priority: number().optional(),
13056
+ hwaccel: string().optional(),
13057
+ probedBestHwaccel: string().optional()
13056
13058
  })),
13057
13059
  createSession: method(DecoderSessionConfigSchema, object({
13058
13060
  sessionId: string(),
@@ -22721,23 +22723,110 @@ var DEFAULT_DECODER_BACKEND = "nodeav";
22721
22723
  function parseDecoderBackend(value) {
22722
22724
  return value === "ffmpeg" || value === "nodeav" ? value : null;
22723
22725
  }
22726
+ /**
22727
+ * Normalise a raw kernel node id to the bare node id used for scoping.
22728
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the decoder selection is
22729
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
22730
+ */
22731
+ function normalizeDecoderNodeId(rawNodeId) {
22732
+ const raw = rawNodeId ?? "hub";
22733
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
22734
+ }
22724
22735
  //#endregion
22725
22736
  //#region src/shared/decoder-backend.ts
22737
+ /** The NEUTRAL addon (pipeline-orchestrator, hub-resident) whose global
22738
+ * settings OWN the per-node `backend` selector. Neither decoder addon owns
22739
+ * it, so decoder-nodeav / decoder-ffmpeg stay fully independent. */
22740
+ var DECODER_OWNER_ADDON_ID = "pipeline-orchestrator";
22741
+ function isHydratedField(entry) {
22742
+ return typeof entry === "object" && entry !== null && "key" in entry;
22743
+ }
22726
22744
  /**
22727
- * Resolve the OWNER addon's (`decoder-ffmpeg`) own per-node backend from its
22728
- * ALREADY-PROJECTED global store the store `BaseAddon.resolveGlobalStore`
22729
- * returns, where the `perNode` `backend` field carries THIS node's value on
22730
- * its bare key. The owner MUST use this instead of {@link resolveDecoderBackend}:
22731
- * routing `addonSettings.getGlobalSettings({addonId:'decoder-ffmpeg'})` back to
22732
- * itself during its OWN `onInitialize` DEADLOCKS (the addon is not "loaded" on
22733
- * the node until init completes, but init is blocked on the read) → the read
22734
- * throws `transport-failed (addon not loaded)` → it silently defaults to the
22735
- * built-in default and mis-decides its own registration (e.g. an explicitly
22736
- * selected `ffmpeg` fallback would never come up). Reading its own store is
22737
- * local, race-free, and has no deadlock.
22745
+ * Pure selection from an already-read hydrated settings payload: extract the
22746
+ * owner's `backend` field value (which the owner projected per-node from its
22747
+ * scoped store key) and narrow it. A missing/invalid field or a null payload
22748
+ * resolves to {@link DEFAULT_DECODER_BACKEND} never a bare store key.
22738
22749
  */
22739
- function resolveOwnDecoderBackend(projectedGlobalStore) {
22740
- return parseDecoderBackend(projectedGlobalStore["backend"]) ?? "nodeav";
22750
+ function pickDecoderBackendFromSettings(view) {
22751
+ if (view === null) return DEFAULT_DECODER_BACKEND;
22752
+ for (const section of view.sections) for (const entry of section.fields) {
22753
+ if (!isHydratedField(entry) || entry.key !== "backend") continue;
22754
+ return parseDecoderBackend(entry.value) ?? "nodeav";
22755
+ }
22756
+ return DEFAULT_DECODER_BACKEND;
22757
+ }
22758
+ /** Missing optional owner fingerprints: ffmpeg may be uninstalled. */
22759
+ function isMissingOwnerSettingsError(message) {
22760
+ return /not routable/i.test(message) || /provider not available/i.test(message);
22761
+ }
22762
+ /** Transient transport/settings-store fingerprints worth retrying on. */
22763
+ function isTransientSettingsError(message) {
22764
+ return /not loaded/i.test(message) || /transport-failed/i.test(message) || /not connected/i.test(message) || /SqliteSettingsBackend not initialized/i.test(message);
22765
+ }
22766
+ /**
22767
+ * Resolve the decoder backend a NON-OWNER addon (`decoder-nodeav`) should run,
22768
+ * by reading the OWNER's (`decoder-ffmpeg`) hub-central per-node `backend`.
22769
+ *
22770
+ * The read routes to the owner addon's child runner; during a simultaneous
22771
+ * (re)start the owner may not be up yet → a transient `transport-failed (addon
22772
+ * not loaded)`. Immediately defaulting here would ignore an explicit `ffmpeg`
22773
+ * selection (the node would silently run the node-av default instead). So retry
22774
+ * on the transient fingerprints with a bounded budget (mirrors
22775
+ * `BaseAddon.readAddonStoreWithRetry`) until the owner answers; only a
22776
+ * persistent failure falls back to {@link DEFAULT_DECODER_BACKEND}.
22777
+ *
22778
+ * The OWNER addon must NOT call this (it would self-route + deadlock) — it uses
22779
+ * {@link resolveOwnDecoderBackend} against its own store instead.
22780
+ */
22781
+ async function resolveDecoderBackend(api, nodeId, logger) {
22782
+ if (!api) {
22783
+ logger.warn("decoder-backend: no api surface — using default backend", { meta: { default: DEFAULT_DECODER_BACKEND } });
22784
+ return DEFAULT_DECODER_BACKEND;
22785
+ }
22786
+ const normalized = normalizeDecoderNodeId(nodeId);
22787
+ const delaysMs = [
22788
+ 150,
22789
+ 350,
22790
+ 600,
22791
+ 900,
22792
+ 1200
22793
+ ];
22794
+ let lastErr;
22795
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) {
22796
+ try {
22797
+ const view = await api.addonSettings.getGlobalSettings.query({
22798
+ addonId: DECODER_OWNER_ADDON_ID,
22799
+ nodeId: normalized
22800
+ });
22801
+ if (view !== null) return pickDecoderBackendFromSettings(view);
22802
+ lastErr = /* @__PURE__ */ new Error("owner settings unavailable (null)");
22803
+ } catch (err) {
22804
+ lastErr = err;
22805
+ const msg = err instanceof Error ? err.message : String(err);
22806
+ if (isTransientSettingsError(msg)) {} else if (isMissingOwnerSettingsError(msg)) {
22807
+ logger.warn("decoder-backend: optional owner unavailable — using default backend", { meta: {
22808
+ default: DEFAULT_DECODER_BACKEND,
22809
+ owner: DECODER_OWNER_ADDON_ID,
22810
+ error: msg
22811
+ } });
22812
+ return DEFAULT_DECODER_BACKEND;
22813
+ } else {
22814
+ logger.warn("decoder-backend: settings read failed — using default backend", { meta: {
22815
+ default: DEFAULT_DECODER_BACKEND,
22816
+ error: msg
22817
+ } });
22818
+ return DEFAULT_DECODER_BACKEND;
22819
+ }
22820
+ }
22821
+ if (attempt === delaysMs.length) break;
22822
+ await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt]));
22823
+ }
22824
+ logger.warn("decoder-backend: owner settings unavailable after retries — using default backend", { meta: {
22825
+ default: DEFAULT_DECODER_BACKEND,
22826
+ owner: DECODER_OWNER_ADDON_ID,
22827
+ error: lastErr instanceof Error ? lastErr.message : String(lastErr)
22828
+ } });
22829
+ return DEFAULT_DECODER_BACKEND;
22741
22830
  }
22742
22831
  //#endregion
22743
22832
  //#region src/shared/notifying-ring-buffer.ts
@@ -24051,19 +24140,15 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
24051
24140
  //#endregion
24052
24141
  //#region src/addon/index.ts
24053
24142
  var FRAME_BUFFER_CAPACITY = 32;
24054
- var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
24055
- ...DEFAULT_DECODER_HWACCEL_CONFIG,
24056
- backend: DEFAULT_DECODER_BACKEND,
24057
- binaryPath: ""
24058
- };
24059
- /** The two selectable decoder backends, in default-first order. */
24060
- var DECODER_BACKEND_OPTIONS = [{
24061
- value: "nodeav",
24062
- label: "node-av (in-process FFmpeg bindings — default)"
24063
- }, {
24064
- value: "ffmpeg",
24065
- label: "ffmpeg (subprocess — crash-isolated fallback)"
24066
- }];
24143
+ /**
24144
+ * decoder-ffmpeg owns only its own `hwaccel` override + `probedBestHwaccel`
24145
+ * probe (the shared {@link DecoderHwAccelConfig}), both `perNode: true` — the
24146
+ * BASE `BaseAddon` settings surface persists them node-scoped as
24147
+ * `<key>@<nodeId>`. The per-node `backend` selector lives on the neutral
24148
+ * pipeline-orchestrator; the ffmpeg binary is resolved node-level via
24149
+ * `ctx.deps.ensureFfmpeg()` (no per-addon override).
24150
+ */
24151
+ var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = { ...DEFAULT_DECODER_HWACCEL_CONFIG };
24067
24152
  var DecoderFfmpegAddon = class extends BaseAddon {
24068
24153
  sessions = /* @__PURE__ */ new Map();
24069
24154
  frameBuffers = /* @__PURE__ */ new Map();
@@ -24074,10 +24159,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24074
24159
  getFrameHits = 0;
24075
24160
  getFrameMisses = 0;
24076
24161
  /**
24077
- * The ffmpeg binary every session spawns — resolved once at init from the
24078
- * cluster `ffmpeg` config section (`binaryPath`), defaulting to PATH
24079
- * `ffmpeg`. MUST be the same binary the broker / recorder / probe use so the
24080
- * decode-accel probe reports for the right build.
24162
+ * The ffmpeg binary every session spawns — resolved once at init via the
24163
+ * node-level `ctx.deps.ensureFfmpeg()`. The SAME binary the broker / recorder
24164
+ * / audio-codec use, so the decode-accel probe reports for the right build.
24081
24165
  */
24082
24166
  ffmpegPath = "ffmpeg";
24083
24167
  /**
@@ -24098,74 +24182,39 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24098
24182
  }
24099
24183
  globalSettingsSchema() {
24100
24184
  return this.schema({ sections: [{
24101
- id: "backend",
24102
- title: "Decoder backend",
24185
+ id: "hwaccel",
24186
+ title: "Hardware acceleration",
24103
24187
  tab: "decoder",
24104
- 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.",
24188
+ 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.",
24105
24189
  fields: [this.field({
24106
24190
  type: "select",
24107
- key: "backend",
24108
- label: "Backend",
24109
- options: [...DECODER_BACKEND_OPTIONS],
24110
- default: DEFAULT_DECODER_BACKEND,
24191
+ key: "hwaccel",
24192
+ label: "Preferred backend",
24193
+ options: [...HWACCEL_OPTIONS],
24194
+ default: "auto",
24111
24195
  immediate: true,
24112
24196
  perNode: true
24197
+ }), this.field({
24198
+ type: "text",
24199
+ key: "probedBestHwaccel",
24200
+ label: "Probed best",
24201
+ description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
24202
+ readonlyField: true,
24203
+ default: "",
24204
+ perNode: true,
24205
+ actions: [{
24206
+ action: "reprobe-hwaccel",
24207
+ icon: "refresh-cw",
24208
+ tooltip: "Re-probe hwaccel"
24209
+ }]
24113
24210
  })]
24114
- }, {
24115
- id: "hwaccel",
24116
- title: "Hardware acceleration",
24117
- tab: "decoder",
24118
- 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.",
24119
- fields: [
24120
- this.field({
24121
- type: "select",
24122
- key: "hwaccel",
24123
- label: "Preferred backend",
24124
- options: [...HWACCEL_OPTIONS],
24125
- default: "auto",
24126
- immediate: true,
24127
- perNode: true
24128
- }),
24129
- this.field({
24130
- type: "text",
24131
- key: "probedBestHwaccel",
24132
- label: "Probed best",
24133
- description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
24134
- readonlyField: true,
24135
- default: "",
24136
- perNode: true,
24137
- actions: [{
24138
- action: "reprobe-hwaccel",
24139
- icon: "refresh-cw",
24140
- tooltip: "Re-probe hwaccel"
24141
- }]
24142
- }),
24143
- this.field({
24144
- type: "text",
24145
- key: "binaryPath",
24146
- label: "ffmpeg binary path (override)",
24147
- 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.",
24148
- placeholder: "ffmpeg",
24149
- default: "",
24150
- perNode: true
24151
- })
24152
- ]
24153
24211
  }] });
24154
24212
  }
24155
24213
  async onInitialize() {
24156
24214
  if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
24157
24215
  this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
24158
24216
  });
24159
- let backend;
24160
- try {
24161
- backend = resolveOwnDecoderBackend(await this.resolveGlobalStore(this.resolveLocalNodeId()));
24162
- } catch (err) {
24163
- this.ctx.logger.warn("decoder-ffmpeg: own-store backend read failed — using default", { meta: {
24164
- default: DEFAULT_DECODER_BACKEND,
24165
- error: err instanceof Error ? err.message : String(err)
24166
- } });
24167
- backend = DEFAULT_DECODER_BACKEND;
24168
- }
24217
+ const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
24169
24218
  if (backend !== "ffmpeg") {
24170
24219
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
24171
24220
  return [];
@@ -24184,14 +24233,11 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24184
24233
  }];
24185
24234
  }
24186
24235
  /**
24187
- * Resolve the ffmpeg binary the sessions spawn.
24236
+ * Resolve the ffmpeg binary the sessions spawn — node-level only.
24188
24237
  *
24189
- * Precedence:
24190
- * 1. An explicit operator override in the cluster `ffmpeg` config section
24191
- * (`binaryPath`) wins when set, so a hand-picked build can be forced.
24192
- * 2. Otherwise `ctx.deps.ensureFfmpeg()` — provisions a portable static
24193
- * ffmpeg into the node's deps dir when the system has none and returns its
24194
- * absolute path. This GUARANTEES ffmpeg on every node (hub, agent, Mac),
24238
+ * `ctx.deps.ensureFfmpeg()` provisions a portable static ffmpeg into the
24239
+ * node's deps dir when the system has none and returns its absolute path.
24240
+ * This GUARANTEES ffmpeg on every node (hub, agent, Mac),
24195
24241
  * so there is no missing-ffmpeg case and no node-av fallback.
24196
24242
  *
24197
24243
  * `ensureFfmpeg()` is single-flighted, so we resolve once at init and reuse
@@ -24200,8 +24246,6 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24200
24246
  * decoder cap and a later session spawn re-runs the ensure.
24201
24247
  */
24202
24248
  async resolveFfmpegBinaryPath() {
24203
- const override = this.config.binaryPath;
24204
- if (typeof override === "string" && override.trim().length > 0) return override;
24205
24249
  try {
24206
24250
  return await this.ctx.deps.ensureFfmpeg();
24207
24251
  } catch (err) {
@@ -24302,7 +24346,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24302
24346
  id: "decoder-ffmpeg",
24303
24347
  name: "Decoder (ffmpeg)",
24304
24348
  isPullMode: true,
24305
- priority: 5
24349
+ priority: 5,
24350
+ hwaccel: this.config.hwaccel,
24351
+ probedBestHwaccel: this.config.probedBestHwaccel || void 0
24306
24352
  };
24307
24353
  }
24308
24354
  /**
package/dist/index.mjs CHANGED
@@ -13048,7 +13048,9 @@ var decoderCapability = {
13048
13048
  id: string(),
13049
13049
  name: string(),
13050
13050
  isPullMode: boolean().optional(),
13051
- priority: number().optional()
13051
+ priority: number().optional(),
13052
+ hwaccel: string().optional(),
13053
+ probedBestHwaccel: string().optional()
13052
13054
  })),
13053
13055
  createSession: method(DecoderSessionConfigSchema, object({
13054
13056
  sessionId: string(),
@@ -22717,23 +22719,110 @@ var DEFAULT_DECODER_BACKEND = "nodeav";
22717
22719
  function parseDecoderBackend(value) {
22718
22720
  return value === "ffmpeg" || value === "nodeav" ? value : null;
22719
22721
  }
22722
+ /**
22723
+ * Normalise a raw kernel node id to the bare node id used for scoping.
22724
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the decoder selection is
22725
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
22726
+ */
22727
+ function normalizeDecoderNodeId(rawNodeId) {
22728
+ const raw = rawNodeId ?? "hub";
22729
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
22730
+ }
22720
22731
  //#endregion
22721
22732
  //#region src/shared/decoder-backend.ts
22733
+ /** The NEUTRAL addon (pipeline-orchestrator, hub-resident) whose global
22734
+ * settings OWN the per-node `backend` selector. Neither decoder addon owns
22735
+ * it, so decoder-nodeav / decoder-ffmpeg stay fully independent. */
22736
+ var DECODER_OWNER_ADDON_ID = "pipeline-orchestrator";
22737
+ function isHydratedField(entry) {
22738
+ return typeof entry === "object" && entry !== null && "key" in entry;
22739
+ }
22722
22740
  /**
22723
- * Resolve the OWNER addon's (`decoder-ffmpeg`) own per-node backend from its
22724
- * ALREADY-PROJECTED global store the store `BaseAddon.resolveGlobalStore`
22725
- * returns, where the `perNode` `backend` field carries THIS node's value on
22726
- * its bare key. The owner MUST use this instead of {@link resolveDecoderBackend}:
22727
- * routing `addonSettings.getGlobalSettings({addonId:'decoder-ffmpeg'})` back to
22728
- * itself during its OWN `onInitialize` DEADLOCKS (the addon is not "loaded" on
22729
- * the node until init completes, but init is blocked on the read) → the read
22730
- * throws `transport-failed (addon not loaded)` → it silently defaults to the
22731
- * built-in default and mis-decides its own registration (e.g. an explicitly
22732
- * selected `ffmpeg` fallback would never come up). Reading its own store is
22733
- * local, race-free, and has no deadlock.
22741
+ * Pure selection from an already-read hydrated settings payload: extract the
22742
+ * owner's `backend` field value (which the owner projected per-node from its
22743
+ * scoped store key) and narrow it. A missing/invalid field or a null payload
22744
+ * resolves to {@link DEFAULT_DECODER_BACKEND} never a bare store key.
22734
22745
  */
22735
- function resolveOwnDecoderBackend(projectedGlobalStore) {
22736
- return parseDecoderBackend(projectedGlobalStore["backend"]) ?? "nodeav";
22746
+ function pickDecoderBackendFromSettings(view) {
22747
+ if (view === null) return DEFAULT_DECODER_BACKEND;
22748
+ for (const section of view.sections) for (const entry of section.fields) {
22749
+ if (!isHydratedField(entry) || entry.key !== "backend") continue;
22750
+ return parseDecoderBackend(entry.value) ?? "nodeav";
22751
+ }
22752
+ return DEFAULT_DECODER_BACKEND;
22753
+ }
22754
+ /** Missing optional owner fingerprints: ffmpeg may be uninstalled. */
22755
+ function isMissingOwnerSettingsError(message) {
22756
+ return /not routable/i.test(message) || /provider not available/i.test(message);
22757
+ }
22758
+ /** Transient transport/settings-store fingerprints worth retrying on. */
22759
+ function isTransientSettingsError(message) {
22760
+ return /not loaded/i.test(message) || /transport-failed/i.test(message) || /not connected/i.test(message) || /SqliteSettingsBackend not initialized/i.test(message);
22761
+ }
22762
+ /**
22763
+ * Resolve the decoder backend a NON-OWNER addon (`decoder-nodeav`) should run,
22764
+ * by reading the OWNER's (`decoder-ffmpeg`) hub-central per-node `backend`.
22765
+ *
22766
+ * The read routes to the owner addon's child runner; during a simultaneous
22767
+ * (re)start the owner may not be up yet → a transient `transport-failed (addon
22768
+ * not loaded)`. Immediately defaulting here would ignore an explicit `ffmpeg`
22769
+ * selection (the node would silently run the node-av default instead). So retry
22770
+ * on the transient fingerprints with a bounded budget (mirrors
22771
+ * `BaseAddon.readAddonStoreWithRetry`) until the owner answers; only a
22772
+ * persistent failure falls back to {@link DEFAULT_DECODER_BACKEND}.
22773
+ *
22774
+ * The OWNER addon must NOT call this (it would self-route + deadlock) — it uses
22775
+ * {@link resolveOwnDecoderBackend} against its own store instead.
22776
+ */
22777
+ async function resolveDecoderBackend(api, nodeId, logger) {
22778
+ if (!api) {
22779
+ logger.warn("decoder-backend: no api surface — using default backend", { meta: { default: DEFAULT_DECODER_BACKEND } });
22780
+ return DEFAULT_DECODER_BACKEND;
22781
+ }
22782
+ const normalized = normalizeDecoderNodeId(nodeId);
22783
+ const delaysMs = [
22784
+ 150,
22785
+ 350,
22786
+ 600,
22787
+ 900,
22788
+ 1200
22789
+ ];
22790
+ let lastErr;
22791
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) {
22792
+ try {
22793
+ const view = await api.addonSettings.getGlobalSettings.query({
22794
+ addonId: DECODER_OWNER_ADDON_ID,
22795
+ nodeId: normalized
22796
+ });
22797
+ if (view !== null) return pickDecoderBackendFromSettings(view);
22798
+ lastErr = /* @__PURE__ */ new Error("owner settings unavailable (null)");
22799
+ } catch (err) {
22800
+ lastErr = err;
22801
+ const msg = err instanceof Error ? err.message : String(err);
22802
+ if (isTransientSettingsError(msg)) {} else if (isMissingOwnerSettingsError(msg)) {
22803
+ logger.warn("decoder-backend: optional owner unavailable — using default backend", { meta: {
22804
+ default: DEFAULT_DECODER_BACKEND,
22805
+ owner: DECODER_OWNER_ADDON_ID,
22806
+ error: msg
22807
+ } });
22808
+ return DEFAULT_DECODER_BACKEND;
22809
+ } else {
22810
+ logger.warn("decoder-backend: settings read failed — using default backend", { meta: {
22811
+ default: DEFAULT_DECODER_BACKEND,
22812
+ error: msg
22813
+ } });
22814
+ return DEFAULT_DECODER_BACKEND;
22815
+ }
22816
+ }
22817
+ if (attempt === delaysMs.length) break;
22818
+ await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt]));
22819
+ }
22820
+ logger.warn("decoder-backend: owner settings unavailable after retries — using default backend", { meta: {
22821
+ default: DEFAULT_DECODER_BACKEND,
22822
+ owner: DECODER_OWNER_ADDON_ID,
22823
+ error: lastErr instanceof Error ? lastErr.message : String(lastErr)
22824
+ } });
22825
+ return DEFAULT_DECODER_BACKEND;
22737
22826
  }
22738
22827
  //#endregion
22739
22828
  //#region src/shared/notifying-ring-buffer.ts
@@ -24047,19 +24136,15 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
24047
24136
  //#endregion
24048
24137
  //#region src/addon/index.ts
24049
24138
  var FRAME_BUFFER_CAPACITY = 32;
24050
- var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = {
24051
- ...DEFAULT_DECODER_HWACCEL_CONFIG,
24052
- backend: DEFAULT_DECODER_BACKEND,
24053
- binaryPath: ""
24054
- };
24055
- /** The two selectable decoder backends, in default-first order. */
24056
- var DECODER_BACKEND_OPTIONS = [{
24057
- value: "nodeav",
24058
- label: "node-av (in-process FFmpeg bindings — default)"
24059
- }, {
24060
- value: "ffmpeg",
24061
- label: "ffmpeg (subprocess — crash-isolated fallback)"
24062
- }];
24139
+ /**
24140
+ * decoder-ffmpeg owns only its own `hwaccel` override + `probedBestHwaccel`
24141
+ * probe (the shared {@link DecoderHwAccelConfig}), both `perNode: true` — the
24142
+ * BASE `BaseAddon` settings surface persists them node-scoped as
24143
+ * `<key>@<nodeId>`. The per-node `backend` selector lives on the neutral
24144
+ * pipeline-orchestrator; the ffmpeg binary is resolved node-level via
24145
+ * `ctx.deps.ensureFfmpeg()` (no per-addon override).
24146
+ */
24147
+ var DEFAULT_DECODER_FFMPEG_ADDON_CONFIG = { ...DEFAULT_DECODER_HWACCEL_CONFIG };
24063
24148
  var DecoderFfmpegAddon = class extends BaseAddon {
24064
24149
  sessions = /* @__PURE__ */ new Map();
24065
24150
  frameBuffers = /* @__PURE__ */ new Map();
@@ -24070,10 +24155,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24070
24155
  getFrameHits = 0;
24071
24156
  getFrameMisses = 0;
24072
24157
  /**
24073
- * The ffmpeg binary every session spawns — resolved once at init from the
24074
- * cluster `ffmpeg` config section (`binaryPath`), defaulting to PATH
24075
- * `ffmpeg`. MUST be the same binary the broker / recorder / probe use so the
24076
- * decode-accel probe reports for the right build.
24158
+ * The ffmpeg binary every session spawns — resolved once at init via the
24159
+ * node-level `ctx.deps.ensureFfmpeg()`. The SAME binary the broker / recorder
24160
+ * / audio-codec use, so the decode-accel probe reports for the right build.
24077
24161
  */
24078
24162
  ffmpegPath = "ffmpeg";
24079
24163
  /**
@@ -24094,74 +24178,39 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24094
24178
  }
24095
24179
  globalSettingsSchema() {
24096
24180
  return this.schema({ sections: [{
24097
- id: "backend",
24098
- title: "Decoder backend",
24181
+ id: "hwaccel",
24182
+ title: "Hardware acceleration",
24099
24183
  tab: "decoder",
24100
- 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.",
24184
+ 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.",
24101
24185
  fields: [this.field({
24102
24186
  type: "select",
24103
- key: "backend",
24104
- label: "Backend",
24105
- options: [...DECODER_BACKEND_OPTIONS],
24106
- default: DEFAULT_DECODER_BACKEND,
24187
+ key: "hwaccel",
24188
+ label: "Preferred backend",
24189
+ options: [...HWACCEL_OPTIONS],
24190
+ default: "auto",
24107
24191
  immediate: true,
24108
24192
  perNode: true
24193
+ }), this.field({
24194
+ type: "text",
24195
+ key: "probedBestHwaccel",
24196
+ label: "Probed best",
24197
+ description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
24198
+ readonlyField: true,
24199
+ default: "",
24200
+ perNode: true,
24201
+ actions: [{
24202
+ action: "reprobe-hwaccel",
24203
+ icon: "refresh-cw",
24204
+ tooltip: "Re-probe hwaccel"
24205
+ }]
24109
24206
  })]
24110
- }, {
24111
- id: "hwaccel",
24112
- title: "Hardware acceleration",
24113
- tab: "decoder",
24114
- 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.",
24115
- fields: [
24116
- this.field({
24117
- type: "select",
24118
- key: "hwaccel",
24119
- label: "Preferred backend",
24120
- options: [...HWACCEL_OPTIONS],
24121
- default: "auto",
24122
- immediate: true,
24123
- perNode: true
24124
- }),
24125
- this.field({
24126
- type: "text",
24127
- key: "probedBestHwaccel",
24128
- label: "Probed best",
24129
- description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
24130
- readonlyField: true,
24131
- default: "",
24132
- perNode: true,
24133
- actions: [{
24134
- action: "reprobe-hwaccel",
24135
- icon: "refresh-cw",
24136
- tooltip: "Re-probe hwaccel"
24137
- }]
24138
- }),
24139
- this.field({
24140
- type: "text",
24141
- key: "binaryPath",
24142
- label: "ffmpeg binary path (override)",
24143
- 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.",
24144
- placeholder: "ffmpeg",
24145
- default: "",
24146
- perNode: true
24147
- })
24148
- ]
24149
24207
  }] });
24150
24208
  }
24151
24209
  async onInitialize() {
24152
24210
  if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
24153
24211
  this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
24154
24212
  });
24155
- let backend;
24156
- try {
24157
- backend = resolveOwnDecoderBackend(await this.resolveGlobalStore(this.resolveLocalNodeId()));
24158
- } catch (err) {
24159
- this.ctx.logger.warn("decoder-ffmpeg: own-store backend read failed — using default", { meta: {
24160
- default: DEFAULT_DECODER_BACKEND,
24161
- error: err instanceof Error ? err.message : String(err)
24162
- } });
24163
- backend = DEFAULT_DECODER_BACKEND;
24164
- }
24213
+ const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
24165
24214
  if (backend !== "ffmpeg") {
24166
24215
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
24167
24216
  return [];
@@ -24180,14 +24229,11 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24180
24229
  }];
24181
24230
  }
24182
24231
  /**
24183
- * Resolve the ffmpeg binary the sessions spawn.
24232
+ * Resolve the ffmpeg binary the sessions spawn — node-level only.
24184
24233
  *
24185
- * Precedence:
24186
- * 1. An explicit operator override in the cluster `ffmpeg` config section
24187
- * (`binaryPath`) wins when set, so a hand-picked build can be forced.
24188
- * 2. Otherwise `ctx.deps.ensureFfmpeg()` — provisions a portable static
24189
- * ffmpeg into the node's deps dir when the system has none and returns its
24190
- * absolute path. This GUARANTEES ffmpeg on every node (hub, agent, Mac),
24234
+ * `ctx.deps.ensureFfmpeg()` provisions a portable static ffmpeg into the
24235
+ * node's deps dir when the system has none and returns its absolute path.
24236
+ * This GUARANTEES ffmpeg on every node (hub, agent, Mac),
24191
24237
  * so there is no missing-ffmpeg case and no node-av fallback.
24192
24238
  *
24193
24239
  * `ensureFfmpeg()` is single-flighted, so we resolve once at init and reuse
@@ -24196,8 +24242,6 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24196
24242
  * decoder cap and a later session spawn re-runs the ensure.
24197
24243
  */
24198
24244
  async resolveFfmpegBinaryPath() {
24199
- const override = this.config.binaryPath;
24200
- if (typeof override === "string" && override.trim().length > 0) return override;
24201
24245
  try {
24202
24246
  return await this.ctx.deps.ensureFfmpeg();
24203
24247
  } catch (err) {
@@ -24298,7 +24342,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
24298
24342
  id: "decoder-ffmpeg",
24299
24343
  name: "Decoder (ffmpeg)",
24300
24344
  isPullMode: true,
24301
- priority: 5
24345
+ priority: 5,
24346
+ hwaccel: this.config.hwaccel,
24347
+ probedBestHwaccel: this.config.probedBestHwaccel || void 0
24302
24348
  };
24303
24349
  }
24304
24350
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-ffmpeg",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Standalone ffmpeg-subprocess decoder fallback addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -48,7 +48,7 @@
48
48
  }
49
49
  ],
50
50
  "passive": true,
51
- "protected": true,
51
+ "protected": false,
52
52
  "icon": "assets/icon.svg",
53
53
  "color": "#0ea5e9"
54
54
  }