@camstack/types 1.2.273 → 1.2.275
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/capabilities/pipeline-analytics.cap.d.ts +4 -0
- package/dist/{composition-BRxAnxtU.js → composition-DG4jVTfB.js} +221 -0
- package/dist/{composition-BK-GTFYb.mjs → composition-Dq42XWp0.mjs} +186 -1
- package/dist/constants.d.ts +0 -1
- package/dist/ffmpeg/gif-derive.d.ts +71 -0
- package/dist/ffmpeg/pinned-binary.d.ts +27 -0
- package/dist/ffmpeg/pinned-spawn.d.ts +29 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +130 -2
- package/dist/index.mjs +125 -3
- package/dist/node.d.ts +2 -0
- package/dist/node.js +60 -200
- package/dist/node.mjs +44 -187
- package/package.json +1 -1
package/dist/node.js
CHANGED
|
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
const require_composition = require("./composition-
|
|
24
|
+
const require_composition = require("./composition-DG4jVTfB.js");
|
|
25
25
|
const require_err_msg = require("./err-msg-Dx2Kor0g.js");
|
|
26
26
|
let node_crypto = require("node:crypto");
|
|
27
27
|
let node_fs = require("node:fs");
|
|
@@ -315,191 +315,6 @@ async function probeFfmpegBinary(path) {
|
|
|
315
315
|
return parseFfmpegCapabilities(path, versionOut, hwaccelOut, encoderOut);
|
|
316
316
|
}
|
|
317
317
|
//#endregion
|
|
318
|
-
//#region src/deps/ffmpeg-artifacts.ts
|
|
319
|
-
/**
|
|
320
|
-
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
321
|
-
*
|
|
322
|
-
* ## Why a download, on every node, including the container
|
|
323
|
-
*
|
|
324
|
-
* The version of ffmpeg used to be CODE: baked into the image, changeable only
|
|
325
|
-
* by rebuilding and rolling it. Now it is DATA — this file states the release,
|
|
326
|
-
* and a node that boots with a different one downloads it. Changing ffmpeg is a
|
|
327
|
-
* framework bump, not an image rebuild.
|
|
328
|
-
*
|
|
329
|
-
* It also settles the licence question by removing it. FFmpeg's licence is
|
|
330
|
-
* chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
|
|
331
|
-
* (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
|
|
332
|
-
* makes the result undistributable. These are GPL v3 builds, and we do not
|
|
333
|
-
* distribute them: the user's own node fetches the archive from the project
|
|
334
|
-
* that publishes it, so we convey no GPL work and inherit none of the
|
|
335
|
-
* obligations that come with conveying one. That is the whole reason the image
|
|
336
|
-
* no longer installs ffmpeg at all — not a technical preference.
|
|
337
|
-
*
|
|
338
|
-
* ## Why jellyfin-ffmpeg
|
|
339
|
-
*
|
|
340
|
-
* One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
|
|
341
|
-
* from one release, with the widest hardware surface of anything maintained:
|
|
342
|
-
* VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
|
|
343
|
-
* MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
|
|
344
|
-
* so the binary is not linked against a driver stack it may not find.
|
|
345
|
-
*
|
|
346
|
-
* **What it still needs from the host, measured 2026-09-19.** On the Unraid
|
|
347
|
-
* HOST, which has no libva, `-vaapi_device` aborts the process outright —
|
|
348
|
-
* `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
|
|
349
|
-
* "Assertion in generated code"' failed`. Inside our container, which installs
|
|
350
|
-
* libva plus the iHD driver, the same command encodes: verified end to end with
|
|
351
|
-
* `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
|
|
352
|
-
* KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
|
|
353
|
-
*
|
|
354
|
-
* ## Why each artifact carries a checksum and a claim
|
|
355
|
-
*
|
|
356
|
-
* The download is now the only source, so `sha256` is verified before anything
|
|
357
|
-
* is extracted — jellyfin publishes no checksums on its release assets, so
|
|
358
|
-
* these are ours, taken at the version bump. And `expectedHwaccels` is what the
|
|
359
|
-
* build is supposed to be able to do: a binary that probes short of its own
|
|
360
|
-
* claim is a source that changed under us, which is precisely what went
|
|
361
|
-
* unnoticed for months when a static build turned out to carry no VAAPI at all
|
|
362
|
-
* (D536).
|
|
363
|
-
*/
|
|
364
|
-
/** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
|
|
365
|
-
var FFMPEG_RELEASE = "8.1.2-5";
|
|
366
|
-
var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
|
|
367
|
-
function portable(target) {
|
|
368
|
-
return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Every artifact below was fetched and hashed on 2026-09-19, and each one's
|
|
372
|
-
* capability claim was verified by RUNNING it: the linux64 build on the hub
|
|
373
|
-
* itself, the two macOS builds on an M-series Mac (the x86_64 one under
|
|
374
|
-
* Rosetta, which translates x86 to arm — never the reverse, which is why the
|
|
375
|
-
* darwin split matters at all).
|
|
376
|
-
*
|
|
377
|
-
* linux-arm64 is hashed but NOT executed — there is no arm64 Linux node in
|
|
378
|
-
* this cluster yet. Its claim is taken from the project's build script, so the
|
|
379
|
-
* first ARM node to boot will either confirm it or trip the assertion, which is
|
|
380
|
-
* the outcome the assertion exists for.
|
|
381
|
-
*/
|
|
382
|
-
var FFMPEG_ARTIFACTS = {
|
|
383
|
-
"linux-x64": {
|
|
384
|
-
url: portable("linux64"),
|
|
385
|
-
archiveFormat: "tar.xz",
|
|
386
|
-
sha256: "1fd859927053c44a4f2dbf67ae8b9ba8d29fb3b8930df0dd57d91aa60589363d",
|
|
387
|
-
sizeBytes: 60255400,
|
|
388
|
-
expectedHwaccels: [
|
|
389
|
-
"vaapi",
|
|
390
|
-
"qsv",
|
|
391
|
-
"drm",
|
|
392
|
-
"vulkan",
|
|
393
|
-
"opencl"
|
|
394
|
-
]
|
|
395
|
-
},
|
|
396
|
-
"linux-arm64": {
|
|
397
|
-
url: portable("linuxarm64"),
|
|
398
|
-
archiveFormat: "tar.xz",
|
|
399
|
-
sha256: "1bd4fafaf4c309cad896d3479152c31d6b7604d4be8b8526882ea16cc6d5f589",
|
|
400
|
-
sizeBytes: 53958064,
|
|
401
|
-
expectedHwaccels: ["vaapi", "drm"]
|
|
402
|
-
},
|
|
403
|
-
"darwin-arm64": {
|
|
404
|
-
url: portable("macarm64"),
|
|
405
|
-
archiveFormat: "tar.xz",
|
|
406
|
-
sha256: "b2ac80bb184e9a2f3f7c236876b2f56a5639596a95b42f41ced34fab66ad720d",
|
|
407
|
-
sizeBytes: 32896684,
|
|
408
|
-
expectedHwaccels: ["videotoolbox"]
|
|
409
|
-
},
|
|
410
|
-
"darwin-x64": {
|
|
411
|
-
url: portable("mac64"),
|
|
412
|
-
archiveFormat: "tar.xz",
|
|
413
|
-
sha256: "021cd321ea169722cd2e4aca35884305449666a0d231d3378af7f87d6a5626e5",
|
|
414
|
-
sizeBytes: 37852180,
|
|
415
|
-
expectedHwaccels: ["videotoolbox"]
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
/** The artifact for this node, or a refusal naming what was asked for. */
|
|
419
|
-
function getFfmpegArtifact(platform, arch) {
|
|
420
|
-
const artifact = FFMPEG_ARTIFACTS[`${platform}-${arch}`];
|
|
421
|
-
if (!artifact) throw new Error(`Unsupported platform/architecture for ffmpeg: ${platform}-${arch}. Point CAMSTACK_FFMPEG_PATH at a binary this node can run.`);
|
|
422
|
-
return artifact;
|
|
423
|
-
}
|
|
424
|
-
/** The URL alone, for a caller that wants nothing else. */
|
|
425
|
-
function getFfmpegDownloadUrl(platform, arch) {
|
|
426
|
-
return getFfmpegArtifact(platform, arch).url;
|
|
427
|
-
}
|
|
428
|
-
//#endregion
|
|
429
|
-
//#region src/deps/ffmpeg-binary-source.ts
|
|
430
|
-
/**
|
|
431
|
-
* WHICH ffmpeg a node runs — one rule, on every node.
|
|
432
|
-
*
|
|
433
|
-
* ## The rule
|
|
434
|
-
*
|
|
435
|
-
* 1. `CAMSTACK_FFMPEG_PATH`, when the operator set it and it exists. Pointing
|
|
436
|
-
* at your own build is a deliberate act and it wins over everything.
|
|
437
|
-
* 2. The pinned build this node has already downloaded, named by release
|
|
438
|
-
* (`ffmpeg-<release>`), in `<dataDir>/deps`.
|
|
439
|
-
* 3. Nothing — the caller downloads it. See `ffmpeg-artifacts.ts`.
|
|
440
|
-
*
|
|
441
|
-
* The system PATH is not on that list and must never be. A host binary is an
|
|
442
|
-
* unknown version with unknown vendor libraries, and it changes under us on any
|
|
443
|
-
* host update; a self-hosted NVR cannot assume the host has ffmpeg at all
|
|
444
|
-
* (D536).
|
|
445
|
-
*
|
|
446
|
-
* ## Why the container image is not special any more
|
|
447
|
-
*
|
|
448
|
-
* It was, for one day: the image installed ffmpeg, CamStack preferred it, and
|
|
449
|
-
* that made the answer depend on where a node ran — an image node got VAAPI, a
|
|
450
|
-
* native one got a portable build with none. Now every node downloads the same
|
|
451
|
-
* pinned build, so:
|
|
452
|
-
*
|
|
453
|
-
* - the ffmpeg version is DATA, not code: changing it is a framework bump, not
|
|
454
|
-
* an image rebuild and a container swap on every machine;
|
|
455
|
-
* - we ship no ffmpeg binary, so we convey no GPL work — the node fetches it
|
|
456
|
-
* from the project that publishes it;
|
|
457
|
-
* - one binary, one set of capabilities, everywhere. A bug that depends on
|
|
458
|
-
* which ffmpeg answered stops being possible.
|
|
459
|
-
*
|
|
460
|
-
* What the image still owes is the DRIVERS, which no binary can bring: libva
|
|
461
|
-
* plus the iHD media driver for Intel. Measured 2026-09-19 — the portable build
|
|
462
|
-
* encodes VAAPI inside our container and ABORTS on the bare Unraid host, where
|
|
463
|
-
* `libva-drm.so.2` does not exist.
|
|
464
|
-
*
|
|
465
|
-
* ## Absent is absent
|
|
466
|
-
*
|
|
467
|
-
* The naming carries the release, so two versions coexist during a change and a
|
|
468
|
-
* rollback is a file that is already on disk rather than a download.
|
|
469
|
-
*/
|
|
470
|
-
/** Operator override. Unset on every node unless a human deliberately set it. */
|
|
471
|
-
var FFMPEG_PATH_ENV = "CAMSTACK_FFMPEG_PATH";
|
|
472
|
-
/**
|
|
473
|
-
* The file name the pinned build is stored under.
|
|
474
|
-
*
|
|
475
|
-
* Versioned on purpose: a download is then idempotent, two releases can sit
|
|
476
|
-
* side by side while a change rolls through a cluster, and going back is a path
|
|
477
|
-
* that already exists instead of a fetch that has to succeed.
|
|
478
|
-
*/
|
|
479
|
-
function ffmpegBinaryName(platform) {
|
|
480
|
-
return `ffmpeg-${FFMPEG_RELEASE}${platform === "win32" ? ".exe" : ""}`;
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* The binary to use, or `null` when it has to be downloaded first.
|
|
484
|
-
*
|
|
485
|
-
* An override that does not exist is NOT silently skipped — it is an operator
|
|
486
|
-
* mistake, and falling through to a different binary than the one they named
|
|
487
|
-
* would hide it. The caller reports it and carries on with the pinned build,
|
|
488
|
-
* which is the only safe direction: a typo must not stop a node serving video.
|
|
489
|
-
*/
|
|
490
|
-
function chooseFfmpegBinary(input) {
|
|
491
|
-
const override = input.override?.trim() ?? "";
|
|
492
|
-
if (override.length > 0 && input.exists(override)) return {
|
|
493
|
-
origin: "override",
|
|
494
|
-
path: override
|
|
495
|
-
};
|
|
496
|
-
if (input.exists(input.pinnedPath)) return {
|
|
497
|
-
origin: "downloaded",
|
|
498
|
-
path: input.pinnedPath
|
|
499
|
-
};
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
//#endregion
|
|
503
318
|
//#region src/deps/ffmpeg-downloader.ts
|
|
504
319
|
/**
|
|
505
320
|
* Provisioning the ffmpeg binary for this node.
|
|
@@ -529,26 +344,26 @@ async function ensureFfmpeg(dataDir, logger) {
|
|
|
529
344
|
const depsDir = (0, node_path.join)(dataDir, "deps");
|
|
530
345
|
const platform = process.platform;
|
|
531
346
|
const arch = process.arch;
|
|
532
|
-
const pinnedPath = (0, node_path.join)(depsDir, ffmpegBinaryName(platform));
|
|
533
|
-
const override = process.env[FFMPEG_PATH_ENV];
|
|
534
|
-
const chosen = chooseFfmpegBinary({
|
|
347
|
+
const pinnedPath = (0, node_path.join)(depsDir, require_composition.ffmpegBinaryName(platform));
|
|
348
|
+
const override = process.env[require_composition.FFMPEG_PATH_ENV];
|
|
349
|
+
const chosen = require_composition.chooseFfmpegBinary({
|
|
535
350
|
platform,
|
|
536
351
|
pinnedPath,
|
|
537
352
|
override,
|
|
538
353
|
exists: node_fs.existsSync
|
|
539
354
|
});
|
|
540
|
-
if (override !== void 0 && override.trim().length > 0 && chosen?.origin !== "override") logger.error(`${FFMPEG_PATH_ENV} names a binary that does not exist — ignoring it`, { meta: { [FFMPEG_PATH_ENV]: override } });
|
|
541
|
-
const artifact = chosen === null ? getFfmpegArtifact(platform, arch) : null;
|
|
355
|
+
if (override !== void 0 && override.trim().length > 0 && chosen?.origin !== "override") logger.error(`${require_composition.FFMPEG_PATH_ENV} names a binary that does not exist — ignoring it`, { meta: { [require_composition.FFMPEG_PATH_ENV]: override } });
|
|
356
|
+
const artifact = chosen === null ? require_composition.getFfmpegArtifact(platform, arch) : null;
|
|
542
357
|
if (artifact !== null) logger.info("ffmpeg is not on this node yet — downloading the pinned build", { meta: {
|
|
543
|
-
release: FFMPEG_RELEASE,
|
|
358
|
+
release: require_composition.FFMPEG_RELEASE,
|
|
544
359
|
url: artifact.url,
|
|
545
360
|
sizeMb: Math.round(artifact.sizeBytes / 1e6)
|
|
546
361
|
} });
|
|
547
362
|
const path = chosen?.path ?? await downloadBinary({
|
|
548
363
|
name: "ffmpeg",
|
|
549
|
-
url: artifact?.url ?? getFfmpegDownloadUrl(platform, arch),
|
|
364
|
+
url: artifact?.url ?? require_composition.getFfmpegDownloadUrl(platform, arch),
|
|
550
365
|
targetDir: depsDir,
|
|
551
|
-
targetName: ffmpegBinaryName(platform),
|
|
366
|
+
targetName: require_composition.ffmpegBinaryName(platform),
|
|
552
367
|
logger,
|
|
553
368
|
isArchive: true,
|
|
554
369
|
archiveFormat: artifact?.archiveFormat ?? "tar.xz",
|
|
@@ -559,16 +374,16 @@ async function ensureFfmpeg(dataDir, logger) {
|
|
|
559
374
|
logger.info("ffmpeg binary resolved", { meta: {
|
|
560
375
|
path,
|
|
561
376
|
origin: chosen?.origin ?? "downloaded",
|
|
562
|
-
release: FFMPEG_RELEASE,
|
|
377
|
+
release: require_composition.FFMPEG_RELEASE,
|
|
563
378
|
version: capabilities.version,
|
|
564
379
|
hwaccels: [...capabilities.hwaccels].join(",") || "none"
|
|
565
380
|
} });
|
|
566
381
|
if (chosen?.origin !== "override") {
|
|
567
|
-
const expected = getFfmpegArtifact(platform, arch).expectedHwaccels;
|
|
382
|
+
const expected = require_composition.getFfmpegArtifact(platform, arch).expectedHwaccels;
|
|
568
383
|
const missing = expected.filter((name) => !capabilities.hwaccels.has(name));
|
|
569
384
|
if (missing.length > 0) logger.error("this ffmpeg cannot do what its artifact is supposed to — the source changed under us", { meta: {
|
|
570
385
|
path,
|
|
571
|
-
release: FFMPEG_RELEASE,
|
|
386
|
+
release: require_composition.FFMPEG_RELEASE,
|
|
572
387
|
expected: expected.join(","),
|
|
573
388
|
missing: missing.join(","),
|
|
574
389
|
got: [...capabilities.hwaccels].join(",") || "none"
|
|
@@ -1560,6 +1375,48 @@ var FfmpegProcess = class {
|
|
|
1560
1375
|
}
|
|
1561
1376
|
};
|
|
1562
1377
|
//#endregion
|
|
1378
|
+
//#region src/ffmpeg/pinned-spawn.ts
|
|
1379
|
+
/**
|
|
1380
|
+
* The spawn a one-shot ffmpeg site uses: the spec's seam when one is injected,
|
|
1381
|
+
* else the node's PINNED binary (D540), else `null` — and the site refuses by
|
|
1382
|
+
* name. There is no fourth answer: the `?? 'ffmpeg'` this replaces made "no
|
|
1383
|
+
* pinned build" mean "run the host's", silently, at every clip site that had it.
|
|
1384
|
+
*
|
|
1385
|
+
* Node-only (`node:child_process`), so it lives under `@camstack/types/node`.
|
|
1386
|
+
*/
|
|
1387
|
+
/** The detail a site refusing for want of a binary puts in its refusal. */
|
|
1388
|
+
var NO_PINNED_FFMPEG_DETAIL = "no pinned ffmpeg on this node (D540)";
|
|
1389
|
+
/**
|
|
1390
|
+
* {@link pinnedFfmpegSpawner}, asked at the point of use through the node's
|
|
1391
|
+
* resolver (`memoizePinnedFfmpeg`). A site that keeps the RESOLVER rather than
|
|
1392
|
+
* a path captured at init recovers when a later resolution succeeds; a path
|
|
1393
|
+
* captured as `null` at boot stayed `null` until the runner restarted.
|
|
1394
|
+
*/
|
|
1395
|
+
async function resolvePinnedFfmpegSpawner(seam, resolveFfmpeg, options) {
|
|
1396
|
+
if (seam !== void 0) return {
|
|
1397
|
+
kind: "ready",
|
|
1398
|
+
spawn: seam
|
|
1399
|
+
};
|
|
1400
|
+
let ffmpegBinaryPath;
|
|
1401
|
+
try {
|
|
1402
|
+
ffmpegBinaryPath = await resolveFfmpeg();
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
return {
|
|
1405
|
+
kind: "refused",
|
|
1406
|
+
detail: `${NO_PINNED_FFMPEG_DETAIL}: ${err instanceof Error ? err.message : String(err)}`
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
kind: "ready",
|
|
1411
|
+
spawn: (args) => (0, node_child_process.spawn)(ffmpegBinaryPath, [...args], options)
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function pinnedFfmpegSpawner(seam, ffmpegBinaryPath, options) {
|
|
1415
|
+
if (seam !== void 0) return seam;
|
|
1416
|
+
if (ffmpegBinaryPath === null) return null;
|
|
1417
|
+
return (args) => (0, node_child_process.spawn)(ffmpegBinaryPath, [...args], options);
|
|
1418
|
+
}
|
|
1419
|
+
//#endregion
|
|
1563
1420
|
//#region src/process/child-cost-registry.ts
|
|
1564
1421
|
/**
|
|
1565
1422
|
* The registry an addon claims its OWN child processes in — the spawn-site
|
|
@@ -2142,12 +1999,13 @@ function resolveExportFingerprint(input) {
|
|
|
2142
1999
|
}
|
|
2143
2000
|
//#endregion
|
|
2144
2001
|
exports.ChildCostRegistry = ChildCostRegistry;
|
|
2145
|
-
exports.FFMPEG_RELEASE = FFMPEG_RELEASE;
|
|
2002
|
+
exports.FFMPEG_RELEASE = require_composition.FFMPEG_RELEASE;
|
|
2146
2003
|
exports.FfmpegProcess = FfmpegProcess;
|
|
2147
2004
|
exports.FilesystemStorageProvider = FilesystemStorageProvider;
|
|
2148
2005
|
exports.Fmp4FragmentChild = Fmp4FragmentChild;
|
|
2149
2006
|
exports.Fmp4FragmentPlane = Fmp4FragmentPlane;
|
|
2150
2007
|
exports.NO_COST_CLAIM = NO_COST_CLAIM;
|
|
2008
|
+
exports.NO_PINNED_FFMPEG_DETAIL = NO_PINNED_FFMPEG_DETAIL;
|
|
2151
2009
|
exports.PYTHON_VERSION = PYTHON_VERSION;
|
|
2152
2010
|
exports.buildBinaryPath = buildBinaryPath;
|
|
2153
2011
|
exports.canonicalDeviceFingerprint = canonicalDeviceFingerprint;
|
|
@@ -2160,8 +2018,8 @@ exports.ensureBinary = ensureBinary;
|
|
|
2160
2018
|
exports.ensureFfmpeg = ensureFfmpeg;
|
|
2161
2019
|
exports.ensurePython = ensurePython;
|
|
2162
2020
|
exports.findInPath = findInPath;
|
|
2163
|
-
exports.getFfmpegArtifact = getFfmpegArtifact;
|
|
2164
|
-
exports.getFfmpegDownloadUrl = getFfmpegDownloadUrl;
|
|
2021
|
+
exports.getFfmpegArtifact = require_composition.getFfmpegArtifact;
|
|
2022
|
+
exports.getFfmpegDownloadUrl = require_composition.getFfmpegDownloadUrl;
|
|
2165
2023
|
exports.getPlatformInfo = getPlatformInfo;
|
|
2166
2024
|
exports.getPythonDownloadUrl = getPythonDownloadUrl;
|
|
2167
2025
|
exports.installPythonPackages = installPythonPackages;
|
|
@@ -2171,9 +2029,11 @@ exports.parseFfmpegCapabilities = parseFfmpegCapabilities;
|
|
|
2171
2029
|
exports.parseProcCpuSeconds = parseProcCpuSeconds;
|
|
2172
2030
|
exports.parseProcRssBytes = parseProcRssBytes;
|
|
2173
2031
|
exports.physicalRootOf = physicalRootOf;
|
|
2032
|
+
exports.pinnedFfmpegSpawner = pinnedFfmpegSpawner;
|
|
2174
2033
|
exports.probeFfmpegBinary = probeFfmpegBinary;
|
|
2175
2034
|
exports.readProcessCost = readProcessCost;
|
|
2176
2035
|
exports.resolveExportFingerprint = resolveExportFingerprint;
|
|
2036
|
+
exports.resolvePinnedFfmpegSpawner = resolvePinnedFfmpegSpawner;
|
|
2177
2037
|
exports.signExpiringUrl = signExpiringUrl;
|
|
2178
2038
|
exports.verifyExpiringUrl = verifyExpiringUrl;
|
|
2179
2039
|
exports.writeFileAtomic = writeFileAtomic;
|
package/dist/node.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as
|
|
1
|
+
import { Ct as chooseFfmpegBinary, Dt as getFfmpegDownloadUrl, Et as getFfmpegArtifact, Lt as isSoftwareDecode, Nt as buildFfmpegArgs, Ot as canonicalHash, P as compositionSourceKey, St as FFMPEG_PATH_ENV, Tt as FFMPEG_RELEASE, a as CUSTOMIZATION_BLOCK_NAME_PREFIX, kt as Fmp4BoxSplitter, wt as ffmpegBinaryName } from "./composition-Dq42XWp0.mjs";
|
|
2
2
|
import { t as errMsg } from "./err-msg-DX6i_MY4.mjs";
|
|
3
3
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -292,191 +292,6 @@ async function probeFfmpegBinary(path) {
|
|
|
292
292
|
return parseFfmpegCapabilities(path, versionOut, hwaccelOut, encoderOut);
|
|
293
293
|
}
|
|
294
294
|
//#endregion
|
|
295
|
-
//#region src/deps/ffmpeg-artifacts.ts
|
|
296
|
-
/**
|
|
297
|
-
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
298
|
-
*
|
|
299
|
-
* ## Why a download, on every node, including the container
|
|
300
|
-
*
|
|
301
|
-
* The version of ffmpeg used to be CODE: baked into the image, changeable only
|
|
302
|
-
* by rebuilding and rolling it. Now it is DATA — this file states the release,
|
|
303
|
-
* and a node that boots with a different one downloads it. Changing ffmpeg is a
|
|
304
|
-
* framework bump, not an image rebuild.
|
|
305
|
-
*
|
|
306
|
-
* It also settles the licence question by removing it. FFmpeg's licence is
|
|
307
|
-
* chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
|
|
308
|
-
* (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
|
|
309
|
-
* makes the result undistributable. These are GPL v3 builds, and we do not
|
|
310
|
-
* distribute them: the user's own node fetches the archive from the project
|
|
311
|
-
* that publishes it, so we convey no GPL work and inherit none of the
|
|
312
|
-
* obligations that come with conveying one. That is the whole reason the image
|
|
313
|
-
* no longer installs ffmpeg at all — not a technical preference.
|
|
314
|
-
*
|
|
315
|
-
* ## Why jellyfin-ffmpeg
|
|
316
|
-
*
|
|
317
|
-
* One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
|
|
318
|
-
* from one release, with the widest hardware surface of anything maintained:
|
|
319
|
-
* VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
|
|
320
|
-
* MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
|
|
321
|
-
* so the binary is not linked against a driver stack it may not find.
|
|
322
|
-
*
|
|
323
|
-
* **What it still needs from the host, measured 2026-09-19.** On the Unraid
|
|
324
|
-
* HOST, which has no libva, `-vaapi_device` aborts the process outright —
|
|
325
|
-
* `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
|
|
326
|
-
* "Assertion in generated code"' failed`. Inside our container, which installs
|
|
327
|
-
* libva plus the iHD driver, the same command encodes: verified end to end with
|
|
328
|
-
* `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
|
|
329
|
-
* KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
|
|
330
|
-
*
|
|
331
|
-
* ## Why each artifact carries a checksum and a claim
|
|
332
|
-
*
|
|
333
|
-
* The download is now the only source, so `sha256` is verified before anything
|
|
334
|
-
* is extracted — jellyfin publishes no checksums on its release assets, so
|
|
335
|
-
* these are ours, taken at the version bump. And `expectedHwaccels` is what the
|
|
336
|
-
* build is supposed to be able to do: a binary that probes short of its own
|
|
337
|
-
* claim is a source that changed under us, which is precisely what went
|
|
338
|
-
* unnoticed for months when a static build turned out to carry no VAAPI at all
|
|
339
|
-
* (D536).
|
|
340
|
-
*/
|
|
341
|
-
/** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
|
|
342
|
-
var FFMPEG_RELEASE = "8.1.2-5";
|
|
343
|
-
var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
|
|
344
|
-
function portable(target) {
|
|
345
|
-
return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Every artifact below was fetched and hashed on 2026-09-19, and each one's
|
|
349
|
-
* capability claim was verified by RUNNING it: the linux64 build on the hub
|
|
350
|
-
* itself, the two macOS builds on an M-series Mac (the x86_64 one under
|
|
351
|
-
* Rosetta, which translates x86 to arm — never the reverse, which is why the
|
|
352
|
-
* darwin split matters at all).
|
|
353
|
-
*
|
|
354
|
-
* linux-arm64 is hashed but NOT executed — there is no arm64 Linux node in
|
|
355
|
-
* this cluster yet. Its claim is taken from the project's build script, so the
|
|
356
|
-
* first ARM node to boot will either confirm it or trip the assertion, which is
|
|
357
|
-
* the outcome the assertion exists for.
|
|
358
|
-
*/
|
|
359
|
-
var FFMPEG_ARTIFACTS = {
|
|
360
|
-
"linux-x64": {
|
|
361
|
-
url: portable("linux64"),
|
|
362
|
-
archiveFormat: "tar.xz",
|
|
363
|
-
sha256: "1fd859927053c44a4f2dbf67ae8b9ba8d29fb3b8930df0dd57d91aa60589363d",
|
|
364
|
-
sizeBytes: 60255400,
|
|
365
|
-
expectedHwaccels: [
|
|
366
|
-
"vaapi",
|
|
367
|
-
"qsv",
|
|
368
|
-
"drm",
|
|
369
|
-
"vulkan",
|
|
370
|
-
"opencl"
|
|
371
|
-
]
|
|
372
|
-
},
|
|
373
|
-
"linux-arm64": {
|
|
374
|
-
url: portable("linuxarm64"),
|
|
375
|
-
archiveFormat: "tar.xz",
|
|
376
|
-
sha256: "1bd4fafaf4c309cad896d3479152c31d6b7604d4be8b8526882ea16cc6d5f589",
|
|
377
|
-
sizeBytes: 53958064,
|
|
378
|
-
expectedHwaccels: ["vaapi", "drm"]
|
|
379
|
-
},
|
|
380
|
-
"darwin-arm64": {
|
|
381
|
-
url: portable("macarm64"),
|
|
382
|
-
archiveFormat: "tar.xz",
|
|
383
|
-
sha256: "b2ac80bb184e9a2f3f7c236876b2f56a5639596a95b42f41ced34fab66ad720d",
|
|
384
|
-
sizeBytes: 32896684,
|
|
385
|
-
expectedHwaccels: ["videotoolbox"]
|
|
386
|
-
},
|
|
387
|
-
"darwin-x64": {
|
|
388
|
-
url: portable("mac64"),
|
|
389
|
-
archiveFormat: "tar.xz",
|
|
390
|
-
sha256: "021cd321ea169722cd2e4aca35884305449666a0d231d3378af7f87d6a5626e5",
|
|
391
|
-
sizeBytes: 37852180,
|
|
392
|
-
expectedHwaccels: ["videotoolbox"]
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
/** The artifact for this node, or a refusal naming what was asked for. */
|
|
396
|
-
function getFfmpegArtifact(platform, arch) {
|
|
397
|
-
const artifact = FFMPEG_ARTIFACTS[`${platform}-${arch}`];
|
|
398
|
-
if (!artifact) throw new Error(`Unsupported platform/architecture for ffmpeg: ${platform}-${arch}. Point CAMSTACK_FFMPEG_PATH at a binary this node can run.`);
|
|
399
|
-
return artifact;
|
|
400
|
-
}
|
|
401
|
-
/** The URL alone, for a caller that wants nothing else. */
|
|
402
|
-
function getFfmpegDownloadUrl(platform, arch) {
|
|
403
|
-
return getFfmpegArtifact(platform, arch).url;
|
|
404
|
-
}
|
|
405
|
-
//#endregion
|
|
406
|
-
//#region src/deps/ffmpeg-binary-source.ts
|
|
407
|
-
/**
|
|
408
|
-
* WHICH ffmpeg a node runs — one rule, on every node.
|
|
409
|
-
*
|
|
410
|
-
* ## The rule
|
|
411
|
-
*
|
|
412
|
-
* 1. `CAMSTACK_FFMPEG_PATH`, when the operator set it and it exists. Pointing
|
|
413
|
-
* at your own build is a deliberate act and it wins over everything.
|
|
414
|
-
* 2. The pinned build this node has already downloaded, named by release
|
|
415
|
-
* (`ffmpeg-<release>`), in `<dataDir>/deps`.
|
|
416
|
-
* 3. Nothing — the caller downloads it. See `ffmpeg-artifacts.ts`.
|
|
417
|
-
*
|
|
418
|
-
* The system PATH is not on that list and must never be. A host binary is an
|
|
419
|
-
* unknown version with unknown vendor libraries, and it changes under us on any
|
|
420
|
-
* host update; a self-hosted NVR cannot assume the host has ffmpeg at all
|
|
421
|
-
* (D536).
|
|
422
|
-
*
|
|
423
|
-
* ## Why the container image is not special any more
|
|
424
|
-
*
|
|
425
|
-
* It was, for one day: the image installed ffmpeg, CamStack preferred it, and
|
|
426
|
-
* that made the answer depend on where a node ran — an image node got VAAPI, a
|
|
427
|
-
* native one got a portable build with none. Now every node downloads the same
|
|
428
|
-
* pinned build, so:
|
|
429
|
-
*
|
|
430
|
-
* - the ffmpeg version is DATA, not code: changing it is a framework bump, not
|
|
431
|
-
* an image rebuild and a container swap on every machine;
|
|
432
|
-
* - we ship no ffmpeg binary, so we convey no GPL work — the node fetches it
|
|
433
|
-
* from the project that publishes it;
|
|
434
|
-
* - one binary, one set of capabilities, everywhere. A bug that depends on
|
|
435
|
-
* which ffmpeg answered stops being possible.
|
|
436
|
-
*
|
|
437
|
-
* What the image still owes is the DRIVERS, which no binary can bring: libva
|
|
438
|
-
* plus the iHD media driver for Intel. Measured 2026-09-19 — the portable build
|
|
439
|
-
* encodes VAAPI inside our container and ABORTS on the bare Unraid host, where
|
|
440
|
-
* `libva-drm.so.2` does not exist.
|
|
441
|
-
*
|
|
442
|
-
* ## Absent is absent
|
|
443
|
-
*
|
|
444
|
-
* The naming carries the release, so two versions coexist during a change and a
|
|
445
|
-
* rollback is a file that is already on disk rather than a download.
|
|
446
|
-
*/
|
|
447
|
-
/** Operator override. Unset on every node unless a human deliberately set it. */
|
|
448
|
-
var FFMPEG_PATH_ENV = "CAMSTACK_FFMPEG_PATH";
|
|
449
|
-
/**
|
|
450
|
-
* The file name the pinned build is stored under.
|
|
451
|
-
*
|
|
452
|
-
* Versioned on purpose: a download is then idempotent, two releases can sit
|
|
453
|
-
* side by side while a change rolls through a cluster, and going back is a path
|
|
454
|
-
* that already exists instead of a fetch that has to succeed.
|
|
455
|
-
*/
|
|
456
|
-
function ffmpegBinaryName(platform) {
|
|
457
|
-
return `ffmpeg-${FFMPEG_RELEASE}${platform === "win32" ? ".exe" : ""}`;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* The binary to use, or `null` when it has to be downloaded first.
|
|
461
|
-
*
|
|
462
|
-
* An override that does not exist is NOT silently skipped — it is an operator
|
|
463
|
-
* mistake, and falling through to a different binary than the one they named
|
|
464
|
-
* would hide it. The caller reports it and carries on with the pinned build,
|
|
465
|
-
* which is the only safe direction: a typo must not stop a node serving video.
|
|
466
|
-
*/
|
|
467
|
-
function chooseFfmpegBinary(input) {
|
|
468
|
-
const override = input.override?.trim() ?? "";
|
|
469
|
-
if (override.length > 0 && input.exists(override)) return {
|
|
470
|
-
origin: "override",
|
|
471
|
-
path: override
|
|
472
|
-
};
|
|
473
|
-
if (input.exists(input.pinnedPath)) return {
|
|
474
|
-
origin: "downloaded",
|
|
475
|
-
path: input.pinnedPath
|
|
476
|
-
};
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
//#endregion
|
|
480
295
|
//#region src/deps/ffmpeg-downloader.ts
|
|
481
296
|
/**
|
|
482
297
|
* Provisioning the ffmpeg binary for this node.
|
|
@@ -1537,6 +1352,48 @@ var FfmpegProcess = class {
|
|
|
1537
1352
|
}
|
|
1538
1353
|
};
|
|
1539
1354
|
//#endregion
|
|
1355
|
+
//#region src/ffmpeg/pinned-spawn.ts
|
|
1356
|
+
/**
|
|
1357
|
+
* The spawn a one-shot ffmpeg site uses: the spec's seam when one is injected,
|
|
1358
|
+
* else the node's PINNED binary (D540), else `null` — and the site refuses by
|
|
1359
|
+
* name. There is no fourth answer: the `?? 'ffmpeg'` this replaces made "no
|
|
1360
|
+
* pinned build" mean "run the host's", silently, at every clip site that had it.
|
|
1361
|
+
*
|
|
1362
|
+
* Node-only (`node:child_process`), so it lives under `@camstack/types/node`.
|
|
1363
|
+
*/
|
|
1364
|
+
/** The detail a site refusing for want of a binary puts in its refusal. */
|
|
1365
|
+
var NO_PINNED_FFMPEG_DETAIL = "no pinned ffmpeg on this node (D540)";
|
|
1366
|
+
/**
|
|
1367
|
+
* {@link pinnedFfmpegSpawner}, asked at the point of use through the node's
|
|
1368
|
+
* resolver (`memoizePinnedFfmpeg`). A site that keeps the RESOLVER rather than
|
|
1369
|
+
* a path captured at init recovers when a later resolution succeeds; a path
|
|
1370
|
+
* captured as `null` at boot stayed `null` until the runner restarted.
|
|
1371
|
+
*/
|
|
1372
|
+
async function resolvePinnedFfmpegSpawner(seam, resolveFfmpeg, options) {
|
|
1373
|
+
if (seam !== void 0) return {
|
|
1374
|
+
kind: "ready",
|
|
1375
|
+
spawn: seam
|
|
1376
|
+
};
|
|
1377
|
+
let ffmpegBinaryPath;
|
|
1378
|
+
try {
|
|
1379
|
+
ffmpegBinaryPath = await resolveFfmpeg();
|
|
1380
|
+
} catch (err) {
|
|
1381
|
+
return {
|
|
1382
|
+
kind: "refused",
|
|
1383
|
+
detail: `${NO_PINNED_FFMPEG_DETAIL}: ${err instanceof Error ? err.message : String(err)}`
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
kind: "ready",
|
|
1388
|
+
spawn: (args) => spawn(ffmpegBinaryPath, [...args], options)
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function pinnedFfmpegSpawner(seam, ffmpegBinaryPath, options) {
|
|
1392
|
+
if (seam !== void 0) return seam;
|
|
1393
|
+
if (ffmpegBinaryPath === null) return null;
|
|
1394
|
+
return (args) => spawn(ffmpegBinaryPath, [...args], options);
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1540
1397
|
//#region src/process/child-cost-registry.ts
|
|
1541
1398
|
/**
|
|
1542
1399
|
* The registry an addon claims its OWN child processes in — the spawn-site
|
|
@@ -2118,4 +1975,4 @@ function resolveExportFingerprint(input) {
|
|
|
2118
1975
|
return input.persisted ?? input.fresh;
|
|
2119
1976
|
}
|
|
2120
1977
|
//#endregion
|
|
2121
|
-
export { ChildCostRegistry, FFMPEG_RELEASE, FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, NO_COST_CLAIM, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, containsOrEquals, customizationBlockName, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegArtifact, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, nodeProcStatReader, parseFfmpegCapabilities, parseProcCpuSeconds, parseProcRssBytes, physicalRootOf, probeFfmpegBinary, readProcessCost, resolveExportFingerprint, signExpiringUrl, verifyExpiringUrl, writeFileAtomic };
|
|
1978
|
+
export { ChildCostRegistry, FFMPEG_RELEASE, FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, NO_COST_CLAIM, NO_PINNED_FFMPEG_DETAIL, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, containsOrEquals, customizationBlockName, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegArtifact, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, nodeProcStatReader, parseFfmpegCapabilities, parseProcCpuSeconds, parseProcRssBytes, physicalRootOf, pinnedFfmpegSpawner, probeFfmpegBinary, readProcessCost, resolveExportFingerprint, resolvePinnedFfmpegSpawner, signExpiringUrl, verifyExpiringUrl, writeFileAtomic };
|