@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.
@@ -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, invocationFromEncodeProfile as At, createStatefulExpressionMemory as B, CompositionTargetSchema as C, Fmp4BoxSplitter as Ct, MAX_COMPOSITION_ITEMS as D, buildFfmpegArgs as Dt, MAX_COMPOSITION_FIELDS_PER_FEATURE as E, buildAudioArgs as Et, featureFieldEntries as F, createExpressionScope as G, evaluateStatefulExpressionCall as H, featureSources as I, compileExpressionSafe as J, evaluateAst as K, fieldReads as L, compositionReadKey as M, logBannerArgs as Mt, compositionSliceKey as N, pickVideoEncoder as Nt, composedDeviceRef as O, buildInputArgs as Ot, compositionSourceKey as P, resolveStreamMaps as Pt, EXPRESSION_IDENTIFIER_RE as Q, EXPRESSION_INJECTED_NOW as R, CompositionSourceRefSchema as S, canonicalHash as St, MAX_COMPOSITION_FEATURES as T, audioPlanFromEncodeProfile 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, isSoftwareDecode as jt, composedDeviceStableId as k, buildVideoArgs 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, AUDIO_PRESETS as wt, CompositionSchema as x, sliceChangedAt as xt, CompositionNewTargetSchema as y, isSourceCap as yt, cloneStatefulExpressionMemory as z };
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 };
@@ -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';