@camstack/types 1.2.274 → 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.
@@ -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,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
@@ -19,6 +19,8 @@ export * from './ffmpeg/hwaccel.js';
19
19
  export * from './ffmpeg/gif-derive.js';
20
20
  export * from './ffmpeg/invocation.js';
21
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';
22
24
  export * from './health/wiring-health.js';
23
25
  export type * from './interfaces/addon.js';
24
26
  export { DEFAULT_ADDON_PLACEMENT, isAgentOnlyPlacement, isDeployableToAgent, isIsolatedBuiltin, resolveAddonExecution, resolveAddonGroup, resolveAddonPlacement, resolveRunnerId, } from './interfaces/addon.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_sleep = require("./sleep-WOeeZ4FU.js");
3
3
  const require_event_category = require("./event-category-MbnB-6AN.js");
4
- const require_composition = require("./composition-BRxAnxtU.js");
4
+ const require_composition = require("./composition-DG4jVTfB.js");
5
5
  const require_err_msg = require("./err-msg-Dx2Kor0g.js");
6
6
  const require_enums = require("./enums.js");
7
7
  let zod = require("zod");
@@ -833,6 +833,28 @@ function egressTranscodeSharingKey(request, delivery = egressTransportFromReques
833
833
  return `egress:${require_composition.canonicalHash(canonicalEgressPlan(request, delivery))}`;
834
834
  }
835
835
  //#endregion
836
+ //#region src/ffmpeg/pinned-binary.ts
837
+ /**
838
+ * Memoize `ensure` on SUCCESS only.
839
+ *
840
+ * A failure is not remembered: the next caller asks again, so a node whose
841
+ * first download failed (no network at boot) recovers on the next spawn that
842
+ * needs it — with no timer and no retry loop, because the ask is driven by
843
+ * work that actually wants the binary.
844
+ */
845
+ function memoizePinnedFfmpeg(ensure) {
846
+ let pending = null;
847
+ return () => {
848
+ if (pending !== null) return pending;
849
+ const attempt = ensure();
850
+ pending = attempt;
851
+ attempt.catch(() => {
852
+ if (pending === attempt) pending = null;
853
+ });
854
+ return attempt;
855
+ };
856
+ }
857
+ //#endregion
836
858
  //#region src/health/wiring-health.ts
837
859
  /**
838
860
  * Deep wiring healthcheck — snapshot of active reachability probes across
@@ -45952,7 +45974,6 @@ var RUNTIME_DEFAULTS = {
45952
45974
  models: "camstack-data/models"
45953
45975
  },
45954
45976
  providers: [],
45955
- "ffmpeg.binaryPath": "ffmpeg",
45956
45977
  "ffmpeg.hwAccel": "auto",
45957
45978
  "ffmpeg.threadCount": 0,
45958
45979
  "auth.tokenExpiry": "30d"
@@ -68816,6 +68837,7 @@ exports.ExtensionSourceHealthSchema = ExtensionSourceHealthSchema;
68816
68837
  exports.ExtensionSourceSchema = ExtensionSourceSchema;
68817
68838
  exports.FEEDER_EXAMPLE_ADDON_ID = FEEDER_EXAMPLE_ADDON_ID;
68818
68839
  exports.FEEDER_EXAMPLE_HA_DEVICE_ID = FEEDER_EXAMPLE_HA_DEVICE_ID;
68840
+ exports.FFMPEG_PATH_ENV = require_composition.FFMPEG_PATH_ENV;
68819
68841
  exports.FIRST_LEVEL_MACRO_CLASSES = FIRST_LEVEL_MACRO_CLASSES;
68820
68842
  exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
68821
68843
  exports.FailureContributionSchema = FailureContributionSchema;
@@ -70024,6 +70046,7 @@ exports.maskUrlCredentials = maskUrlCredentials;
70024
70046
  exports.mayReadLocation = mayReadLocation;
70025
70047
  exports.mayWriteToLocation = mayWriteToLocation;
70026
70048
  exports.mediaPlayerCapability = mediaPlayerCapability;
70049
+ exports.memoizePinnedFfmpeg = memoizePinnedFfmpeg;
70027
70050
  exports.mergeSourceInfo = mergeSourceInfo;
70028
70051
  exports.meshNetworkCapability = meshNetworkCapability;
70029
70052
  exports.method = require_sleep.method;