@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
|
@@ -663,6 +663,7 @@ declare const ObjectEventSchema: z.ZodObject<{
|
|
|
663
663
|
mediaId: z.ZodOptional<z.ZodNumber>;
|
|
664
664
|
keyFrameMediaId: z.ZodOptional<z.ZodNumber>;
|
|
665
665
|
mediaUrl: z.ZodOptional<z.ZodString>;
|
|
666
|
+
observedAt: z.ZodOptional<z.ZodNumber>;
|
|
666
667
|
importance: z.ZodOptional<z.ZodNumber>;
|
|
667
668
|
label: z.ZodOptional<z.ZodString>;
|
|
668
669
|
labelScore: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1579,6 +1580,7 @@ export declare const ScoredObjectEventSchema: z.ZodObject<{
|
|
|
1579
1580
|
mediaId: z.ZodOptional<z.ZodNumber>;
|
|
1580
1581
|
keyFrameMediaId: z.ZodOptional<z.ZodNumber>;
|
|
1581
1582
|
mediaUrl: z.ZodOptional<z.ZodString>;
|
|
1583
|
+
observedAt: z.ZodOptional<z.ZodNumber>;
|
|
1582
1584
|
importance: z.ZodOptional<z.ZodNumber>;
|
|
1583
1585
|
label: z.ZodOptional<z.ZodString>;
|
|
1584
1586
|
labelScore: z.ZodOptional<z.ZodNumber>;
|
|
@@ -2421,6 +2423,7 @@ export declare const pipelineAnalyticsCapability: {
|
|
|
2421
2423
|
mediaId: z.ZodOptional<z.ZodNumber>;
|
|
2422
2424
|
keyFrameMediaId: z.ZodOptional<z.ZodNumber>;
|
|
2423
2425
|
mediaUrl: z.ZodOptional<z.ZodString>;
|
|
2426
|
+
observedAt: z.ZodOptional<z.ZodNumber>;
|
|
2424
2427
|
importance: z.ZodOptional<z.ZodNumber>;
|
|
2425
2428
|
label: z.ZodOptional<z.ZodString>;
|
|
2426
2429
|
labelScore: z.ZodOptional<z.ZodNumber>;
|
|
@@ -4178,6 +4181,7 @@ export declare const pipelineAnalyticsCapability: {
|
|
|
4178
4181
|
mediaId: z.ZodOptional<z.ZodNumber>;
|
|
4179
4182
|
keyFrameMediaId: z.ZodOptional<z.ZodNumber>;
|
|
4180
4183
|
mediaUrl: z.ZodOptional<z.ZodString>;
|
|
4184
|
+
observedAt: z.ZodOptional<z.ZodNumber>;
|
|
4181
4185
|
importance: z.ZodOptional<z.ZodNumber>;
|
|
4182
4186
|
label: z.ZodOptional<z.ZodString>;
|
|
4183
4187
|
labelScore: z.ZodOptional<z.ZodNumber>;
|
|
@@ -714,6 +714,191 @@ function replaceWithSortedKeys(_key, value) {
|
|
|
714
714
|
return value;
|
|
715
715
|
}
|
|
716
716
|
//#endregion
|
|
717
|
+
//#region src/deps/ffmpeg-artifacts.ts
|
|
718
|
+
/**
|
|
719
|
+
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
720
|
+
*
|
|
721
|
+
* ## Why a download, on every node, including the container
|
|
722
|
+
*
|
|
723
|
+
* The version of ffmpeg used to be CODE: baked into the image, changeable only
|
|
724
|
+
* by rebuilding and rolling it. Now it is DATA — this file states the release,
|
|
725
|
+
* and a node that boots with a different one downloads it. Changing ffmpeg is a
|
|
726
|
+
* framework bump, not an image rebuild.
|
|
727
|
+
*
|
|
728
|
+
* It also settles the licence question by removing it. FFmpeg's licence is
|
|
729
|
+
* chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
|
|
730
|
+
* (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
|
|
731
|
+
* makes the result undistributable. These are GPL v3 builds, and we do not
|
|
732
|
+
* distribute them: the user's own node fetches the archive from the project
|
|
733
|
+
* that publishes it, so we convey no GPL work and inherit none of the
|
|
734
|
+
* obligations that come with conveying one. That is the whole reason the image
|
|
735
|
+
* no longer installs ffmpeg at all — not a technical preference.
|
|
736
|
+
*
|
|
737
|
+
* ## Why jellyfin-ffmpeg
|
|
738
|
+
*
|
|
739
|
+
* One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
|
|
740
|
+
* from one release, with the widest hardware surface of anything maintained:
|
|
741
|
+
* VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
|
|
742
|
+
* MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
|
|
743
|
+
* so the binary is not linked against a driver stack it may not find.
|
|
744
|
+
*
|
|
745
|
+
* **What it still needs from the host, measured 2026-09-19.** On the Unraid
|
|
746
|
+
* HOST, which has no libva, `-vaapi_device` aborts the process outright —
|
|
747
|
+
* `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
|
|
748
|
+
* "Assertion in generated code"' failed`. Inside our container, which installs
|
|
749
|
+
* libva plus the iHD driver, the same command encodes: verified end to end with
|
|
750
|
+
* `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
|
|
751
|
+
* KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
|
|
752
|
+
*
|
|
753
|
+
* ## Why each artifact carries a checksum and a claim
|
|
754
|
+
*
|
|
755
|
+
* The download is now the only source, so `sha256` is verified before anything
|
|
756
|
+
* is extracted — jellyfin publishes no checksums on its release assets, so
|
|
757
|
+
* these are ours, taken at the version bump. And `expectedHwaccels` is what the
|
|
758
|
+
* build is supposed to be able to do: a binary that probes short of its own
|
|
759
|
+
* claim is a source that changed under us, which is precisely what went
|
|
760
|
+
* unnoticed for months when a static build turned out to carry no VAAPI at all
|
|
761
|
+
* (D536).
|
|
762
|
+
*/
|
|
763
|
+
/** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
|
|
764
|
+
var FFMPEG_RELEASE = "8.1.2-5";
|
|
765
|
+
var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
|
|
766
|
+
function portable(target) {
|
|
767
|
+
return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Every artifact below was fetched and hashed on 2026-09-19, and each one's
|
|
771
|
+
* capability claim was verified by RUNNING it: the linux64 build on the hub
|
|
772
|
+
* itself, the two macOS builds on an M-series Mac (the x86_64 one under
|
|
773
|
+
* Rosetta, which translates x86 to arm — never the reverse, which is why the
|
|
774
|
+
* darwin split matters at all).
|
|
775
|
+
*
|
|
776
|
+
* linux-arm64 is hashed but NOT executed — there is no arm64 Linux node in
|
|
777
|
+
* this cluster yet. Its claim is taken from the project's build script, so the
|
|
778
|
+
* first ARM node to boot will either confirm it or trip the assertion, which is
|
|
779
|
+
* the outcome the assertion exists for.
|
|
780
|
+
*/
|
|
781
|
+
var FFMPEG_ARTIFACTS = {
|
|
782
|
+
"linux-x64": {
|
|
783
|
+
url: portable("linux64"),
|
|
784
|
+
archiveFormat: "tar.xz",
|
|
785
|
+
sha256: "1fd859927053c44a4f2dbf67ae8b9ba8d29fb3b8930df0dd57d91aa60589363d",
|
|
786
|
+
sizeBytes: 60255400,
|
|
787
|
+
expectedHwaccels: [
|
|
788
|
+
"vaapi",
|
|
789
|
+
"qsv",
|
|
790
|
+
"drm",
|
|
791
|
+
"vulkan",
|
|
792
|
+
"opencl"
|
|
793
|
+
]
|
|
794
|
+
},
|
|
795
|
+
"linux-arm64": {
|
|
796
|
+
url: portable("linuxarm64"),
|
|
797
|
+
archiveFormat: "tar.xz",
|
|
798
|
+
sha256: "1bd4fafaf4c309cad896d3479152c31d6b7604d4be8b8526882ea16cc6d5f589",
|
|
799
|
+
sizeBytes: 53958064,
|
|
800
|
+
expectedHwaccels: ["vaapi", "drm"]
|
|
801
|
+
},
|
|
802
|
+
"darwin-arm64": {
|
|
803
|
+
url: portable("macarm64"),
|
|
804
|
+
archiveFormat: "tar.xz",
|
|
805
|
+
sha256: "b2ac80bb184e9a2f3f7c236876b2f56a5639596a95b42f41ced34fab66ad720d",
|
|
806
|
+
sizeBytes: 32896684,
|
|
807
|
+
expectedHwaccels: ["videotoolbox"]
|
|
808
|
+
},
|
|
809
|
+
"darwin-x64": {
|
|
810
|
+
url: portable("mac64"),
|
|
811
|
+
archiveFormat: "tar.xz",
|
|
812
|
+
sha256: "021cd321ea169722cd2e4aca35884305449666a0d231d3378af7f87d6a5626e5",
|
|
813
|
+
sizeBytes: 37852180,
|
|
814
|
+
expectedHwaccels: ["videotoolbox"]
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
/** The artifact for this node, or a refusal naming what was asked for. */
|
|
818
|
+
function getFfmpegArtifact(platform, arch) {
|
|
819
|
+
const artifact = FFMPEG_ARTIFACTS[`${platform}-${arch}`];
|
|
820
|
+
if (!artifact) throw new Error(`Unsupported platform/architecture for ffmpeg: ${platform}-${arch}. Point CAMSTACK_FFMPEG_PATH at a binary this node can run.`);
|
|
821
|
+
return artifact;
|
|
822
|
+
}
|
|
823
|
+
/** The URL alone, for a caller that wants nothing else. */
|
|
824
|
+
function getFfmpegDownloadUrl(platform, arch) {
|
|
825
|
+
return getFfmpegArtifact(platform, arch).url;
|
|
826
|
+
}
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/deps/ffmpeg-binary-source.ts
|
|
829
|
+
/**
|
|
830
|
+
* WHICH ffmpeg a node runs — one rule, on every node.
|
|
831
|
+
*
|
|
832
|
+
* ## The rule
|
|
833
|
+
*
|
|
834
|
+
* 1. `CAMSTACK_FFMPEG_PATH`, when the operator set it and it exists. Pointing
|
|
835
|
+
* at your own build is a deliberate act and it wins over everything.
|
|
836
|
+
* 2. The pinned build this node has already downloaded, named by release
|
|
837
|
+
* (`ffmpeg-<release>`), in `<dataDir>/deps`.
|
|
838
|
+
* 3. Nothing — the caller downloads it. See `ffmpeg-artifacts.ts`.
|
|
839
|
+
*
|
|
840
|
+
* The system PATH is not on that list and must never be. A host binary is an
|
|
841
|
+
* unknown version with unknown vendor libraries, and it changes under us on any
|
|
842
|
+
* host update; a self-hosted NVR cannot assume the host has ffmpeg at all
|
|
843
|
+
* (D536).
|
|
844
|
+
*
|
|
845
|
+
* ## Why the container image is not special any more
|
|
846
|
+
*
|
|
847
|
+
* It was, for one day: the image installed ffmpeg, CamStack preferred it, and
|
|
848
|
+
* that made the answer depend on where a node ran — an image node got VAAPI, a
|
|
849
|
+
* native one got a portable build with none. Now every node downloads the same
|
|
850
|
+
* pinned build, so:
|
|
851
|
+
*
|
|
852
|
+
* - the ffmpeg version is DATA, not code: changing it is a framework bump, not
|
|
853
|
+
* an image rebuild and a container swap on every machine;
|
|
854
|
+
* - we ship no ffmpeg binary, so we convey no GPL work — the node fetches it
|
|
855
|
+
* from the project that publishes it;
|
|
856
|
+
* - one binary, one set of capabilities, everywhere. A bug that depends on
|
|
857
|
+
* which ffmpeg answered stops being possible.
|
|
858
|
+
*
|
|
859
|
+
* What the image still owes is the DRIVERS, which no binary can bring: libva
|
|
860
|
+
* plus the iHD media driver for Intel. Measured 2026-09-19 — the portable build
|
|
861
|
+
* encodes VAAPI inside our container and ABORTS on the bare Unraid host, where
|
|
862
|
+
* `libva-drm.so.2` does not exist.
|
|
863
|
+
*
|
|
864
|
+
* ## Absent is absent
|
|
865
|
+
*
|
|
866
|
+
* The naming carries the release, so two versions coexist during a change and a
|
|
867
|
+
* rollback is a file that is already on disk rather than a download.
|
|
868
|
+
*/
|
|
869
|
+
/** Operator override. Unset on every node unless a human deliberately set it. */
|
|
870
|
+
var FFMPEG_PATH_ENV = "CAMSTACK_FFMPEG_PATH";
|
|
871
|
+
/**
|
|
872
|
+
* The file name the pinned build is stored under.
|
|
873
|
+
*
|
|
874
|
+
* Versioned on purpose: a download is then idempotent, two releases can sit
|
|
875
|
+
* side by side while a change rolls through a cluster, and going back is a path
|
|
876
|
+
* that already exists instead of a fetch that has to succeed.
|
|
877
|
+
*/
|
|
878
|
+
function ffmpegBinaryName(platform) {
|
|
879
|
+
return `ffmpeg-${FFMPEG_RELEASE}${platform === "win32" ? ".exe" : ""}`;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The binary to use, or `null` when it has to be downloaded first.
|
|
883
|
+
*
|
|
884
|
+
* An override that does not exist is NOT silently skipped — it is an operator
|
|
885
|
+
* mistake, and falling through to a different binary than the one they named
|
|
886
|
+
* would hide it. The caller reports it and carries on with the pinned build,
|
|
887
|
+
* which is the only safe direction: a typo must not stop a node serving video.
|
|
888
|
+
*/
|
|
889
|
+
function chooseFfmpegBinary(input) {
|
|
890
|
+
const override = input.override?.trim() ?? "";
|
|
891
|
+
if (override.length > 0 && input.exists(override)) return {
|
|
892
|
+
origin: "override",
|
|
893
|
+
path: override
|
|
894
|
+
};
|
|
895
|
+
if (input.exists(input.pinnedPath)) return {
|
|
896
|
+
origin: "downloaded",
|
|
897
|
+
path: input.pinnedPath
|
|
898
|
+
};
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
//#endregion
|
|
717
902
|
//#region src/catalogs/sensor-active-state.ts
|
|
718
903
|
/**
|
|
719
904
|
* LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
|
|
@@ -2268,6 +2453,18 @@ Object.defineProperty(exports, "ExpressionParseError", {
|
|
|
2268
2453
|
return ExpressionParseError;
|
|
2269
2454
|
}
|
|
2270
2455
|
});
|
|
2456
|
+
Object.defineProperty(exports, "FFMPEG_PATH_ENV", {
|
|
2457
|
+
enumerable: true,
|
|
2458
|
+
get: function() {
|
|
2459
|
+
return FFMPEG_PATH_ENV;
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
Object.defineProperty(exports, "FFMPEG_RELEASE", {
|
|
2463
|
+
enumerable: true,
|
|
2464
|
+
get: function() {
|
|
2465
|
+
return FFMPEG_RELEASE;
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2271
2468
|
Object.defineProperty(exports, "Fmp4BoxSplitter", {
|
|
2272
2469
|
enumerable: true,
|
|
2273
2470
|
get: function() {
|
|
@@ -2394,6 +2591,12 @@ Object.defineProperty(exports, "canonicalHash", {
|
|
|
2394
2591
|
return canonicalHash;
|
|
2395
2592
|
}
|
|
2396
2593
|
});
|
|
2594
|
+
Object.defineProperty(exports, "chooseFfmpegBinary", {
|
|
2595
|
+
enumerable: true,
|
|
2596
|
+
get: function() {
|
|
2597
|
+
return chooseFfmpegBinary;
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2397
2600
|
Object.defineProperty(exports, "cloneStatefulExpressionMemory", {
|
|
2398
2601
|
enumerable: true,
|
|
2399
2602
|
get: function() {
|
|
@@ -2520,12 +2723,30 @@ Object.defineProperty(exports, "featureSources", {
|
|
|
2520
2723
|
return featureSources;
|
|
2521
2724
|
}
|
|
2522
2725
|
});
|
|
2726
|
+
Object.defineProperty(exports, "ffmpegBinaryName", {
|
|
2727
|
+
enumerable: true,
|
|
2728
|
+
get: function() {
|
|
2729
|
+
return ffmpegBinaryName;
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2523
2732
|
Object.defineProperty(exports, "fieldReads", {
|
|
2524
2733
|
enumerable: true,
|
|
2525
2734
|
get: function() {
|
|
2526
2735
|
return fieldReads;
|
|
2527
2736
|
}
|
|
2528
2737
|
});
|
|
2738
|
+
Object.defineProperty(exports, "getFfmpegArtifact", {
|
|
2739
|
+
enumerable: true,
|
|
2740
|
+
get: function() {
|
|
2741
|
+
return getFfmpegArtifact;
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
Object.defineProperty(exports, "getFfmpegDownloadUrl", {
|
|
2745
|
+
enumerable: true,
|
|
2746
|
+
get: function() {
|
|
2747
|
+
return getFfmpegDownloadUrl;
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2529
2750
|
Object.defineProperty(exports, "invocationFromEncodeProfile", {
|
|
2530
2751
|
enumerable: true,
|
|
2531
2752
|
get: function() {
|
|
@@ -714,6 +714,191 @@ function replaceWithSortedKeys(_key, value) {
|
|
|
714
714
|
return value;
|
|
715
715
|
}
|
|
716
716
|
//#endregion
|
|
717
|
+
//#region src/deps/ffmpeg-artifacts.ts
|
|
718
|
+
/**
|
|
719
|
+
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
720
|
+
*
|
|
721
|
+
* ## Why a download, on every node, including the container
|
|
722
|
+
*
|
|
723
|
+
* The version of ffmpeg used to be CODE: baked into the image, changeable only
|
|
724
|
+
* by rebuilding and rolling it. Now it is DATA — this file states the release,
|
|
725
|
+
* and a node that boots with a different one downloads it. Changing ffmpeg is a
|
|
726
|
+
* framework bump, not an image rebuild.
|
|
727
|
+
*
|
|
728
|
+
* It also settles the licence question by removing it. FFmpeg's licence is
|
|
729
|
+
* chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
|
|
730
|
+
* (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
|
|
731
|
+
* makes the result undistributable. These are GPL v3 builds, and we do not
|
|
732
|
+
* distribute them: the user's own node fetches the archive from the project
|
|
733
|
+
* that publishes it, so we convey no GPL work and inherit none of the
|
|
734
|
+
* obligations that come with conveying one. That is the whole reason the image
|
|
735
|
+
* no longer installs ffmpeg at all — not a technical preference.
|
|
736
|
+
*
|
|
737
|
+
* ## Why jellyfin-ffmpeg
|
|
738
|
+
*
|
|
739
|
+
* One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
|
|
740
|
+
* from one release, with the widest hardware surface of anything maintained:
|
|
741
|
+
* VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
|
|
742
|
+
* MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
|
|
743
|
+
* so the binary is not linked against a driver stack it may not find.
|
|
744
|
+
*
|
|
745
|
+
* **What it still needs from the host, measured 2026-09-19.** On the Unraid
|
|
746
|
+
* HOST, which has no libva, `-vaapi_device` aborts the process outright —
|
|
747
|
+
* `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
|
|
748
|
+
* "Assertion in generated code"' failed`. Inside our container, which installs
|
|
749
|
+
* libva plus the iHD driver, the same command encodes: verified end to end with
|
|
750
|
+
* `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
|
|
751
|
+
* KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
|
|
752
|
+
*
|
|
753
|
+
* ## Why each artifact carries a checksum and a claim
|
|
754
|
+
*
|
|
755
|
+
* The download is now the only source, so `sha256` is verified before anything
|
|
756
|
+
* is extracted — jellyfin publishes no checksums on its release assets, so
|
|
757
|
+
* these are ours, taken at the version bump. And `expectedHwaccels` is what the
|
|
758
|
+
* build is supposed to be able to do: a binary that probes short of its own
|
|
759
|
+
* claim is a source that changed under us, which is precisely what went
|
|
760
|
+
* unnoticed for months when a static build turned out to carry no VAAPI at all
|
|
761
|
+
* (D536).
|
|
762
|
+
*/
|
|
763
|
+
/** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
|
|
764
|
+
var FFMPEG_RELEASE = "8.1.2-5";
|
|
765
|
+
var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
|
|
766
|
+
function portable(target) {
|
|
767
|
+
return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Every artifact below was fetched and hashed on 2026-09-19, and each one's
|
|
771
|
+
* capability claim was verified by RUNNING it: the linux64 build on the hub
|
|
772
|
+
* itself, the two macOS builds on an M-series Mac (the x86_64 one under
|
|
773
|
+
* Rosetta, which translates x86 to arm — never the reverse, which is why the
|
|
774
|
+
* darwin split matters at all).
|
|
775
|
+
*
|
|
776
|
+
* linux-arm64 is hashed but NOT executed — there is no arm64 Linux node in
|
|
777
|
+
* this cluster yet. Its claim is taken from the project's build script, so the
|
|
778
|
+
* first ARM node to boot will either confirm it or trip the assertion, which is
|
|
779
|
+
* the outcome the assertion exists for.
|
|
780
|
+
*/
|
|
781
|
+
var FFMPEG_ARTIFACTS = {
|
|
782
|
+
"linux-x64": {
|
|
783
|
+
url: portable("linux64"),
|
|
784
|
+
archiveFormat: "tar.xz",
|
|
785
|
+
sha256: "1fd859927053c44a4f2dbf67ae8b9ba8d29fb3b8930df0dd57d91aa60589363d",
|
|
786
|
+
sizeBytes: 60255400,
|
|
787
|
+
expectedHwaccels: [
|
|
788
|
+
"vaapi",
|
|
789
|
+
"qsv",
|
|
790
|
+
"drm",
|
|
791
|
+
"vulkan",
|
|
792
|
+
"opencl"
|
|
793
|
+
]
|
|
794
|
+
},
|
|
795
|
+
"linux-arm64": {
|
|
796
|
+
url: portable("linuxarm64"),
|
|
797
|
+
archiveFormat: "tar.xz",
|
|
798
|
+
sha256: "1bd4fafaf4c309cad896d3479152c31d6b7604d4be8b8526882ea16cc6d5f589",
|
|
799
|
+
sizeBytes: 53958064,
|
|
800
|
+
expectedHwaccels: ["vaapi", "drm"]
|
|
801
|
+
},
|
|
802
|
+
"darwin-arm64": {
|
|
803
|
+
url: portable("macarm64"),
|
|
804
|
+
archiveFormat: "tar.xz",
|
|
805
|
+
sha256: "b2ac80bb184e9a2f3f7c236876b2f56a5639596a95b42f41ced34fab66ad720d",
|
|
806
|
+
sizeBytes: 32896684,
|
|
807
|
+
expectedHwaccels: ["videotoolbox"]
|
|
808
|
+
},
|
|
809
|
+
"darwin-x64": {
|
|
810
|
+
url: portable("mac64"),
|
|
811
|
+
archiveFormat: "tar.xz",
|
|
812
|
+
sha256: "021cd321ea169722cd2e4aca35884305449666a0d231d3378af7f87d6a5626e5",
|
|
813
|
+
sizeBytes: 37852180,
|
|
814
|
+
expectedHwaccels: ["videotoolbox"]
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
/** The artifact for this node, or a refusal naming what was asked for. */
|
|
818
|
+
function getFfmpegArtifact(platform, arch) {
|
|
819
|
+
const artifact = FFMPEG_ARTIFACTS[`${platform}-${arch}`];
|
|
820
|
+
if (!artifact) throw new Error(`Unsupported platform/architecture for ffmpeg: ${platform}-${arch}. Point CAMSTACK_FFMPEG_PATH at a binary this node can run.`);
|
|
821
|
+
return artifact;
|
|
822
|
+
}
|
|
823
|
+
/** The URL alone, for a caller that wants nothing else. */
|
|
824
|
+
function getFfmpegDownloadUrl(platform, arch) {
|
|
825
|
+
return getFfmpegArtifact(platform, arch).url;
|
|
826
|
+
}
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/deps/ffmpeg-binary-source.ts
|
|
829
|
+
/**
|
|
830
|
+
* WHICH ffmpeg a node runs — one rule, on every node.
|
|
831
|
+
*
|
|
832
|
+
* ## The rule
|
|
833
|
+
*
|
|
834
|
+
* 1. `CAMSTACK_FFMPEG_PATH`, when the operator set it and it exists. Pointing
|
|
835
|
+
* at your own build is a deliberate act and it wins over everything.
|
|
836
|
+
* 2. The pinned build this node has already downloaded, named by release
|
|
837
|
+
* (`ffmpeg-<release>`), in `<dataDir>/deps`.
|
|
838
|
+
* 3. Nothing — the caller downloads it. See `ffmpeg-artifacts.ts`.
|
|
839
|
+
*
|
|
840
|
+
* The system PATH is not on that list and must never be. A host binary is an
|
|
841
|
+
* unknown version with unknown vendor libraries, and it changes under us on any
|
|
842
|
+
* host update; a self-hosted NVR cannot assume the host has ffmpeg at all
|
|
843
|
+
* (D536).
|
|
844
|
+
*
|
|
845
|
+
* ## Why the container image is not special any more
|
|
846
|
+
*
|
|
847
|
+
* It was, for one day: the image installed ffmpeg, CamStack preferred it, and
|
|
848
|
+
* that made the answer depend on where a node ran — an image node got VAAPI, a
|
|
849
|
+
* native one got a portable build with none. Now every node downloads the same
|
|
850
|
+
* pinned build, so:
|
|
851
|
+
*
|
|
852
|
+
* - the ffmpeg version is DATA, not code: changing it is a framework bump, not
|
|
853
|
+
* an image rebuild and a container swap on every machine;
|
|
854
|
+
* - we ship no ffmpeg binary, so we convey no GPL work — the node fetches it
|
|
855
|
+
* from the project that publishes it;
|
|
856
|
+
* - one binary, one set of capabilities, everywhere. A bug that depends on
|
|
857
|
+
* which ffmpeg answered stops being possible.
|
|
858
|
+
*
|
|
859
|
+
* What the image still owes is the DRIVERS, which no binary can bring: libva
|
|
860
|
+
* plus the iHD media driver for Intel. Measured 2026-09-19 — the portable build
|
|
861
|
+
* encodes VAAPI inside our container and ABORTS on the bare Unraid host, where
|
|
862
|
+
* `libva-drm.so.2` does not exist.
|
|
863
|
+
*
|
|
864
|
+
* ## Absent is absent
|
|
865
|
+
*
|
|
866
|
+
* The naming carries the release, so two versions coexist during a change and a
|
|
867
|
+
* rollback is a file that is already on disk rather than a download.
|
|
868
|
+
*/
|
|
869
|
+
/** Operator override. Unset on every node unless a human deliberately set it. */
|
|
870
|
+
var FFMPEG_PATH_ENV = "CAMSTACK_FFMPEG_PATH";
|
|
871
|
+
/**
|
|
872
|
+
* The file name the pinned build is stored under.
|
|
873
|
+
*
|
|
874
|
+
* Versioned on purpose: a download is then idempotent, two releases can sit
|
|
875
|
+
* side by side while a change rolls through a cluster, and going back is a path
|
|
876
|
+
* that already exists instead of a fetch that has to succeed.
|
|
877
|
+
*/
|
|
878
|
+
function ffmpegBinaryName(platform) {
|
|
879
|
+
return `ffmpeg-${FFMPEG_RELEASE}${platform === "win32" ? ".exe" : ""}`;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The binary to use, or `null` when it has to be downloaded first.
|
|
883
|
+
*
|
|
884
|
+
* An override that does not exist is NOT silently skipped — it is an operator
|
|
885
|
+
* mistake, and falling through to a different binary than the one they named
|
|
886
|
+
* would hide it. The caller reports it and carries on with the pinned build,
|
|
887
|
+
* which is the only safe direction: a typo must not stop a node serving video.
|
|
888
|
+
*/
|
|
889
|
+
function chooseFfmpegBinary(input) {
|
|
890
|
+
const override = input.override?.trim() ?? "";
|
|
891
|
+
if (override.length > 0 && input.exists(override)) return {
|
|
892
|
+
origin: "override",
|
|
893
|
+
path: override
|
|
894
|
+
};
|
|
895
|
+
if (input.exists(input.pinnedPath)) return {
|
|
896
|
+
origin: "downloaded",
|
|
897
|
+
path: input.pinnedPath
|
|
898
|
+
};
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
//#endregion
|
|
717
902
|
//#region src/catalogs/sensor-active-state.ts
|
|
718
903
|
/**
|
|
719
904
|
* LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
|
|
@@ -2076,4 +2261,4 @@ function featureSources(feature) {
|
|
|
2076
2261
|
return featureFieldEntries(feature).map((e) => e.source);
|
|
2077
2262
|
}
|
|
2078
2263
|
//#endregion
|
|
2079
|
-
export { MAX_EXPRESSION_AST_NODES as $, compositionFeatureEnabled as A,
|
|
2264
|
+
export { MAX_EXPRESSION_AST_NODES as $, compositionFeatureEnabled as A, AUDIO_PRESETS as At, createStatefulExpressionMemory as B, resolveStreamMaps as Bt, CompositionTargetSchema as C, chooseFfmpegBinary as Ct, MAX_COMPOSITION_ITEMS as D, getFfmpegDownloadUrl as Dt, MAX_COMPOSITION_FIELDS_PER_FEATURE as E, getFfmpegArtifact as Et, featureFieldEntries as F, buildVideoArgs as Ft, createExpressionScope as G, evaluateStatefulExpressionCall as H, featureSources as I, invocationFromEncodeProfile as It, compileExpressionSafe as J, evaluateAst as K, fieldReads as L, isSoftwareDecode as Lt, compositionReadKey as M, buildAudioArgs as Mt, compositionSliceKey as N, buildFfmpegArgs as Nt, composedDeviceRef as O, canonicalHash as Ot, compositionSourceKey as P, buildInputArgs as Pt, EXPRESSION_IDENTIFIER_RE as Q, EXPRESSION_INJECTED_NOW as R, logBannerArgs as Rt, CompositionSourceRefSchema as S, FFMPEG_PATH_ENV as St, MAX_COMPOSITION_FEATURES as T, FFMPEG_RELEASE as Tt, toExpressionValue as U, evaluateExpressionSource as V, validateExpressionSource as W, tokenize as X, parseExpression as Y, EXPRESSION_COMPILE_CACHE_CAPACITY as Z, CompositionItemEntrySchema as _, evaluateSensorEdge as _t, CUSTOMIZATION_BLOCK_NAME_PREFIX as a, EXPRESSION_BUILTINS as at, CompositionPassthroughFeatureSchema as b, sliceActiveValue as bt, CompositionCommandTargetSchema as c, evaluateRose as ct, CompositionFeatureSchema as d, ExpressionParseError as dt, MAX_EXPRESSION_BINDINGS as et, CompositionFieldReadSchema as f, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as ft, CompositionFromSourceSchema as g, SOURCE_DEVICE_TYPES as gt, CompositionForwardCommandSchema as h, SOURCE_CAP_CHANGED_AT_FIELD as ht, COMPOSITION_ITEM_KEY_RE as i, RESERVED_BINDING_NAMES as it, compositionItemFieldPath as j, audioPlanFromEncodeProfile as jt, composedDeviceStableId as k, Fmp4BoxSplitter as kt, CompositionExistingTargetSchema as l, isStatefulBuiltin as lt, CompositionFieldsFeatureSchema as m, SOURCE_CAP_ACTIVE_FIELD as mt, COMPOSER_ADDON_ID as n, MAX_EXPRESSION_EVAL_STEPS as nt, CompositionBindingSchema as o, EXPRESSION_BUILTIN_NAMES as ot, CompositionFieldSourceSchema as p, SOURCE_CAPS as pt, compileExpression as q, COMPOSITION_ITEM_ARRAY_PATH as r, MAX_EXPRESSION_SOURCE_LENGTH as rt, CompositionCodeSourceSchema as s, evaluateCount as st, COMPOSED_DEVICE_STABLE_ID_PREFIX as t, MAX_EXPRESSION_CALL_ARGS as tt, CompositionExpressionSourceSchema as u, ExpressionEvalError as ut, CompositionLiteralBindingSchema as v, evaluateSensorLevelEdge as vt, MAX_COMPOSITION_CODE_LENGTH as w, ffmpegBinaryName as wt, CompositionSchema as x, sliceChangedAt as xt, CompositionNewTargetSchema as y, isSourceCap as yt, cloneStatefulExpressionMemory as z, pickVideoEncoder as zt };
|
package/dist/constants.d.ts
CHANGED
|
@@ -54,7 +54,6 @@ export interface RuntimeDefaultsShape {
|
|
|
54
54
|
'storage.provider': string;
|
|
55
55
|
'storage.locations': Record<string, string>;
|
|
56
56
|
providers: RuntimeDefaultsProvider[];
|
|
57
|
-
'ffmpeg.binaryPath': string;
|
|
58
57
|
'ffmpeg.hwAccel': string;
|
|
59
58
|
'ffmpeg.threadCount': number;
|
|
60
59
|
'auth.tokenExpiry': string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The notification GIF's filter chain and argv — ONE definition for every
|
|
3
|
+
* process that derives a gif from a notification video.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this lives in `@camstack/types`
|
|
6
|
+
*
|
|
7
|
+
* Two addons derive a notification gif from an mp4, and they may never import
|
|
8
|
+
* each other:
|
|
9
|
+
*
|
|
10
|
+
* - the stream broker (`addon-pipeline`), from the clip it just cut out of its
|
|
11
|
+
* own ring (`gif-from-mp4.ts`);
|
|
12
|
+
* - the notification centre (`addon-post-analysis`), from a clip cut out of
|
|
13
|
+
* the RECORDINGS when the moment is older than any ring holds
|
|
14
|
+
* ([D682](../../../../docs/decisions/adr-0682-a-pick-ups-footage-is-cut-around-the-pick-up.md)).
|
|
15
|
+
*
|
|
16
|
+
* Two copies of this chain would be two gifs of the same kind of moment that
|
|
17
|
+
* look different depending on how old the moment was — the class of defect
|
|
18
|
+
* D85/D91 already paid for once. So the chain is here, pure and dependency
|
|
19
|
+
* free, and each caller owns only its own process lifecycle.
|
|
20
|
+
*
|
|
21
|
+
* ## The palette was never the problem
|
|
22
|
+
*
|
|
23
|
+
* The `split` + `palettegen(stats_mode=diff)` + `paletteuse(bayer)` pair is
|
|
24
|
+
* right and stays. The measured cause of "the gif looks de-pixellated" was a
|
|
25
|
+
* 640×360 substream scaled DOWN to 480 px, upstream of the palette. The `split`
|
|
26
|
+
* form is a single READ of the input, not a lesser two-pass.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Format a frame rate for an ffmpeg filter argument.
|
|
30
|
+
*
|
|
31
|
+
* `12 / 4` must reach the filter as `3` and `15 / 4` as `3.75`, not as a float
|
|
32
|
+
* tail that `av_parse_video_rate` then has to guess at.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ffmpegRate(rate: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* The factor the derive must apply ON TOP of the speed the mp4 already carries.
|
|
37
|
+
*
|
|
38
|
+
* The gif is derived from a muxed mp4, so if that mux already ran `setpts` at
|
|
39
|
+
* the caller's `speed`, applying the absolute gif rate again would compound
|
|
40
|
+
* them — a 4× gif beside a 2× video would come out at 8×. Discounting here is
|
|
41
|
+
* what keeps the gif rate ABSOLUTE against real time whatever the video does.
|
|
42
|
+
*/
|
|
43
|
+
export declare function gifDeriveSpeed(gifSpeed: number, videoSpeed: number): number;
|
|
44
|
+
/** What shapes one gif derive. */
|
|
45
|
+
export interface GifDeriveShape {
|
|
46
|
+
readonly maxWidth: number;
|
|
47
|
+
/** The gif's PLAYBACK frame rate. */
|
|
48
|
+
readonly fps: number;
|
|
49
|
+
/** The rate applied by THIS derive, on top of what the mp4 carries. */
|
|
50
|
+
readonly speed: number;
|
|
51
|
+
}
|
|
52
|
+
/** The argv input: a streamable mp4 on stdin (the default) or a file path. */
|
|
53
|
+
export interface GifDeriveArgsInput extends GifDeriveShape {
|
|
54
|
+
/** `pipe:0` when absent. A file path for an mp4 that is not streamable. */
|
|
55
|
+
readonly input?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The filter chain. Extracted so a test can assert the palette pair without
|
|
59
|
+
* spawning ffmpeg — an argv test cannot see quality, but it can see a filter
|
|
60
|
+
* that somebody deleted.
|
|
61
|
+
*
|
|
62
|
+
* `fps` BEFORE `setpts`: decimating an already sped-up timeline yields `speed`
|
|
63
|
+
* times the frames asked for, and a gif pays for every one. So the decimation
|
|
64
|
+
* runs at `fps / speed` in SOURCE time and `setpts` multiplies what survived.
|
|
65
|
+
* The trailing `fps` re-declares the rate, which is NOT redundant: `setpts`
|
|
66
|
+
* leaves the filter link advertising the sampled rate and the gif muxer
|
|
67
|
+
* believes the link — measured on 615 as a 3.01 s gif of ten frames.
|
|
68
|
+
*/
|
|
69
|
+
export declare function gifFilterComplex(input: GifDeriveShape): string;
|
|
70
|
+
/** The full argv: mp4 in, gif on stdout. */
|
|
71
|
+
export declare function gifFromMp4Args(input: GifDeriveArgsInput): readonly string[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a runtime site gets the ffmpeg it spawns: the node's PINNED build, asked
|
|
3
|
+
* of `ctx.deps.ensureFfmpeg()` and never looked up on PATH (D540).
|
|
4
|
+
*
|
|
5
|
+
* Why a memo and not a bare `ensureFfmpeg()` per spawn: every call takes the
|
|
6
|
+
* deps file lock, probes the binary (`-version`, `-hwaccels`, `-encoders` —
|
|
7
|
+
* three children) and logs the resolution. That is right once per process and
|
|
8
|
+
* wrong once per still, per fragment or per preview.
|
|
9
|
+
*
|
|
10
|
+
* Why a memo and not a string resolved at init with a default: a default is
|
|
11
|
+
* what this repo kept writing — `ffmpegPath ?? 'ffmpeg'`, `binaryPath =
|
|
12
|
+
* 'ffmpeg'` — and every one of them turned "the pinned build is unavailable"
|
|
13
|
+
* into "run the host's binary" without a line in the log. A resolver that
|
|
14
|
+
* REJECTS has no such value to fall back to; the caller must say what it
|
|
15
|
+
* refused. `scripts/check-no-bare-ffmpeg.ts` refuses the default at commit.
|
|
16
|
+
*/
|
|
17
|
+
/** The pinned ffmpeg for this node; rejects when there is none to be had. */
|
|
18
|
+
export type PinnedFfmpegResolver = () => Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Memoize `ensure` on SUCCESS only.
|
|
21
|
+
*
|
|
22
|
+
* A failure is not remembered: the next caller asks again, so a node whose
|
|
23
|
+
* first download failed (no network at boot) recovers on the next spawn that
|
|
24
|
+
* needs it — with no timer and no retry loop, because the ask is driven by
|
|
25
|
+
* work that actually wants the binary.
|
|
26
|
+
*/
|
|
27
|
+
export declare function memoizePinnedFfmpeg(ensure: () => Promise<string>): PinnedFfmpegResolver;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spawn a one-shot ffmpeg site uses: the spec's seam when one is injected,
|
|
3
|
+
* else the node's PINNED binary (D540), else `null` — and the site refuses by
|
|
4
|
+
* name. There is no fourth answer: the `?? 'ffmpeg'` this replaces made "no
|
|
5
|
+
* pinned build" mean "run the host's", silently, at every clip site that had it.
|
|
6
|
+
*
|
|
7
|
+
* Node-only (`node:child_process`), so it lives under `@camstack/types/node`.
|
|
8
|
+
*/
|
|
9
|
+
import { type ChildProcess, type SpawnOptions } from 'node:child_process';
|
|
10
|
+
/** One ffmpeg child from its argv. */
|
|
11
|
+
export type FfmpegArgvSpawn = (args: readonly string[]) => ChildProcess;
|
|
12
|
+
/** The detail a site refusing for want of a binary puts in its refusal. */
|
|
13
|
+
export declare const NO_PINNED_FFMPEG_DETAIL = "no pinned ffmpeg on this node (D540)";
|
|
14
|
+
/** What a site gets when it asks for its spawn: one, or the refusal to name. */
|
|
15
|
+
export type PinnedFfmpegSpawnAnswer = {
|
|
16
|
+
readonly kind: 'ready';
|
|
17
|
+
readonly spawn: FfmpegArgvSpawn;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'refused';
|
|
20
|
+
readonly detail: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* {@link pinnedFfmpegSpawner}, asked at the point of use through the node's
|
|
24
|
+
* resolver (`memoizePinnedFfmpeg`). A site that keeps the RESOLVER rather than
|
|
25
|
+
* a path captured at init recovers when a later resolution succeeds; a path
|
|
26
|
+
* captured as `null` at boot stayed `null` until the runner restarted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolvePinnedFfmpegSpawner(seam: FfmpegArgvSpawn | undefined, resolveFfmpeg: () => Promise<string>, options: SpawnOptions): Promise<PinnedFfmpegSpawnAnswer>;
|
|
29
|
+
export declare function pinnedFfmpegSpawner(seam: FfmpegArgvSpawn | undefined, ffmpegBinaryPath: string | null, options: SpawnOptions): FfmpegArgvSpawn | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,11 @@ export * from './camera/stream-pick.js';
|
|
|
16
16
|
export * from './ffmpeg/encode-defaults.js';
|
|
17
17
|
export * from './ffmpeg/fmp4-box-splitter.js';
|
|
18
18
|
export * from './ffmpeg/hwaccel.js';
|
|
19
|
+
export * from './ffmpeg/gif-derive.js';
|
|
19
20
|
export * from './ffmpeg/invocation.js';
|
|
20
21
|
export * from './ffmpeg/sharing-key.js';
|
|
22
|
+
export * from './ffmpeg/pinned-binary.js';
|
|
23
|
+
export { FFMPEG_PATH_ENV } from './deps/ffmpeg-binary-source.js';
|
|
21
24
|
export * from './health/wiring-health.js';
|
|
22
25
|
export type * from './interfaces/addon.js';
|
|
23
26
|
export { DEFAULT_ADDON_PLACEMENT, isAgentOnlyPlacement, isDeployableToAgent, isIsolatedBuiltin, resolveAddonExecution, resolveAddonGroup, resolveAddonPlacement, resolveRunnerId, } from './interfaces/addon.js';
|