@camstack/addon-decoder-ffmpeg 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -97
- package/dist/index.mjs +143 -97
- package/package.json +1 -1
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
|
-
*
|
|
22728
|
-
*
|
|
22729
|
-
*
|
|
22730
|
-
*
|
|
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
|
|
22740
|
-
|
|
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
|
-
|
|
24055
|
-
|
|
24056
|
-
|
|
24057
|
-
|
|
24058
|
-
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
|
|
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
|
|
24078
|
-
*
|
|
24079
|
-
*
|
|
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: "
|
|
24102
|
-
title: "
|
|
24185
|
+
id: "hwaccel",
|
|
24186
|
+
title: "Hardware acceleration",
|
|
24103
24187
|
tab: "decoder",
|
|
24104
|
-
description: "
|
|
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: "
|
|
24108
|
-
label: "
|
|
24109
|
-
options: [...
|
|
24110
|
-
default:
|
|
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
|
-
|
|
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
|
-
*
|
|
24190
|
-
*
|
|
24191
|
-
*
|
|
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
|
-
*
|
|
22724
|
-
*
|
|
22725
|
-
*
|
|
22726
|
-
*
|
|
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
|
|
22736
|
-
|
|
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
|
-
|
|
24051
|
-
|
|
24052
|
-
|
|
24053
|
-
|
|
24054
|
-
|
|
24055
|
-
|
|
24056
|
-
|
|
24057
|
-
|
|
24058
|
-
|
|
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
|
|
24074
|
-
*
|
|
24075
|
-
*
|
|
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: "
|
|
24098
|
-
title: "
|
|
24181
|
+
id: "hwaccel",
|
|
24182
|
+
title: "Hardware acceleration",
|
|
24099
24183
|
tab: "decoder",
|
|
24100
|
-
description: "
|
|
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: "
|
|
24104
|
-
label: "
|
|
24105
|
-
options: [...
|
|
24106
|
-
default:
|
|
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
|
-
|
|
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
|
-
*
|
|
24186
|
-
*
|
|
24187
|
-
*
|
|
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
|
/**
|