@camstack/types 1.2.227 → 1.2.229

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.
@@ -252,6 +252,7 @@ export declare const MigrateSwitchReportSchema: z.ZodObject<{
252
252
  "object-detection": "object-detection";
253
253
  "device-audio": "device-audio";
254
254
  "broker-audio": "broker-audio";
255
+ "event-saving": "event-saving";
255
256
  }>;
256
257
  outcome: z.ZodEnum<{
257
258
  off: "off";
@@ -275,6 +276,7 @@ export declare const MigrateDeviceResultSchema: z.ZodObject<{
275
276
  "object-detection": "object-detection";
276
277
  "device-audio": "device-audio";
277
278
  "broker-audio": "broker-audio";
279
+ "event-saving": "event-saving";
278
280
  }>;
279
281
  outcome: z.ZodEnum<{
280
282
  off: "off";
@@ -293,6 +295,7 @@ export declare const MigrateDeviceResultSchema: z.ZodObject<{
293
295
  "object-detection": "object-detection";
294
296
  "device-audio": "device-audio";
295
297
  "broker-audio": "broker-audio";
298
+ "event-saving": "event-saving";
296
299
  }>>>;
297
300
  swapped: z.ZodBoolean;
298
301
  }, z.core.$strip>;
@@ -362,6 +365,7 @@ export declare const deviceManagerCapability: {
362
365
  "object-detection": "object-detection";
363
366
  "device-audio": "device-audio";
364
367
  "broker-audio": "broker-audio";
368
+ "event-saving": "event-saving";
365
369
  }>;
366
370
  outcome: z.ZodEnum<{
367
371
  off: "off";
@@ -380,6 +384,7 @@ export declare const deviceManagerCapability: {
380
384
  "object-detection": "object-detection";
381
385
  "device-audio": "device-audio";
382
386
  "broker-audio": "broker-audio";
387
+ "event-saving": "event-saving";
383
388
  }>>>;
384
389
  swapped: z.ZodBoolean;
385
390
  }, z.core.$strip>, "mutation">;
@@ -519,6 +519,7 @@ declare const CameraStatusSchema: z.ZodObject<{
519
519
  "object-detection": "object-detection";
520
520
  "device-audio": "device-audio";
521
521
  "broker-audio": "broker-audio";
522
+ "event-saving": "event-saving";
522
523
  }>>>;
523
524
  degraded: z.ZodReadonly<z.ZodArray<z.ZodObject<{
524
525
  stage: z.ZodEnum<{
@@ -1264,6 +1265,7 @@ export declare const pipelineOrchestratorCapability: {
1264
1265
  "object-detection": "object-detection";
1265
1266
  "device-audio": "device-audio";
1266
1267
  "broker-audio": "broker-audio";
1268
+ "event-saving": "event-saving";
1267
1269
  }>;
1268
1270
  label: z.ZodString;
1269
1271
  costWhenOff: z.ZodString;
@@ -1324,6 +1326,7 @@ export declare const pipelineOrchestratorCapability: {
1324
1326
  "object-detection": "object-detection";
1325
1327
  "device-audio": "device-audio";
1326
1328
  "broker-audio": "broker-audio";
1329
+ "event-saving": "event-saving";
1327
1330
  }>;
1328
1331
  enabled: z.ZodBoolean;
1329
1332
  }, z.core.$strip>, z.ZodObject<{
@@ -1338,6 +1341,7 @@ export declare const pipelineOrchestratorCapability: {
1338
1341
  "object-detection": "object-detection";
1339
1342
  "device-audio": "device-audio";
1340
1343
  "broker-audio": "broker-audio";
1344
+ "event-saving": "event-saving";
1341
1345
  }>;
1342
1346
  label: z.ZodString;
1343
1347
  costWhenOff: z.ZodString;
@@ -1489,6 +1493,7 @@ export declare const pipelineOrchestratorCapability: {
1489
1493
  "object-detection": "object-detection";
1490
1494
  "device-audio": "device-audio";
1491
1495
  "broker-audio": "broker-audio";
1496
+ "event-saving": "event-saving";
1492
1497
  }>>>;
1493
1498
  degraded: z.ZodReadonly<z.ZodArray<z.ZodObject<{
1494
1499
  stage: z.ZodEnum<{
@@ -1625,6 +1630,7 @@ export declare const pipelineOrchestratorCapability: {
1625
1630
  "object-detection": "object-detection";
1626
1631
  "device-audio": "device-audio";
1627
1632
  "broker-audio": "broker-audio";
1633
+ "event-saving": "event-saving";
1628
1634
  }>>>;
1629
1635
  degraded: z.ZodReadonly<z.ZodArray<z.ZodObject<{
1630
1636
  stage: z.ZodEnum<{
@@ -24,6 +24,18 @@ export interface DownloadOptions {
24
24
  readonly archiveFormat?: 'zip' | 'tar.gz' | 'tar.xz';
25
25
  /** Relative path within archive to the binary (e.g., 'ffmpeg-6.1/bin/ffmpeg') */
26
26
  readonly archiveInnerPath?: string;
27
+ /**
28
+ * SHA-256 the downloaded ARCHIVE must have, verified before a byte of it is
29
+ * extracted.
30
+ *
31
+ * Optional only because the python downloader predates it. For ffmpeg it is
32
+ * not optional in practice: the download is the ONLY source of the binary
33
+ * every transcode runs through, it arrives over a redirect to a third-party
34
+ * host, and the publisher ships no checksums of its own — so ours is the only
35
+ * thing standing between a changed artifact and a silent swap of the most
36
+ * privileged subprocess this system spawns.
37
+ */
38
+ readonly sha256?: string;
27
39
  }
28
40
  /**
29
41
  * Download a binary to the target directory.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The ffmpeg build every CamStack node downloads, per platform and arch.
3
+ *
4
+ * ## Why a download, on every node, including the container
5
+ *
6
+ * The version of ffmpeg used to be CODE: baked into the image, changeable only
7
+ * by rebuilding and rolling it. Now it is DATA — this file states the release,
8
+ * and a node that boots with a different one downloads it. Changing ffmpeg is a
9
+ * framework bump, not an image rebuild.
10
+ *
11
+ * It also settles the licence question by removing it. FFmpeg's licence is
12
+ * chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
13
+ * (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
14
+ * makes the result undistributable. These are GPL v3 builds, and we do not
15
+ * distribute them: the user's own node fetches the archive from the project
16
+ * that publishes it, so we convey no GPL work and inherit none of the
17
+ * obligations that come with conveying one. That is the whole reason the image
18
+ * no longer installs ffmpeg at all — not a technical preference.
19
+ *
20
+ * ## Why jellyfin-ffmpeg
21
+ *
22
+ * One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
23
+ * from one release, with the widest hardware surface of anything maintained:
24
+ * VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
25
+ * MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
26
+ * so the binary is not linked against a driver stack it may not find.
27
+ *
28
+ * **What it still needs from the host, measured 2026-09-19.** On the Unraid
29
+ * HOST, which has no libva, `-vaapi_device` aborts the process outright —
30
+ * `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
31
+ * "Assertion in generated code"' failed`. Inside our container, which installs
32
+ * libva plus the iHD driver, the same command encodes: verified end to end with
33
+ * `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
34
+ * KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
35
+ *
36
+ * ## Why each artifact carries a checksum and a claim
37
+ *
38
+ * The download is now the only source, so `sha256` is verified before anything
39
+ * is extracted — jellyfin publishes no checksums on its release assets, so
40
+ * these are ours, taken at the version bump. And `expectedHwaccels` is what the
41
+ * build is supposed to be able to do: a binary that probes short of its own
42
+ * claim is a source that changed under us, which is precisely what went
43
+ * unnoticed for months when a static build turned out to carry no VAAPI at all
44
+ * (D536).
45
+ */
46
+ /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
47
+ export declare const FFMPEG_RELEASE: "8.1.2-5";
48
+ /** ffmpeg 8.1.2 is what this release builds — for the log line, not resolution. */
49
+ export declare const FFMPEG_VERSION: "8.1.2";
50
+ export interface FfmpegArtifact {
51
+ readonly url: string;
52
+ readonly archiveFormat: 'zip' | 'tar.gz' | 'tar.xz';
53
+ /** SHA-256 of the ARCHIVE, verified before extraction. */
54
+ readonly sha256: string;
55
+ /** Bytes, as published — for the "downloading 60 MB" line. */
56
+ readonly sizeBytes: number;
57
+ /**
58
+ * `-hwaccels` names this build must report. Asserted after download: short of
59
+ * this is a changed source, not a quiet degradation.
60
+ */
61
+ readonly expectedHwaccels: readonly string[];
62
+ }
63
+ /** The artifact for this node, or a refusal naming what was asked for. */
64
+ export declare function getFfmpegArtifact(platform: string, arch: string): FfmpegArtifact;
65
+ /** The URL alone, for a caller that wants nothing else. */
66
+ export declare function getFfmpegDownloadUrl(platform: string, arch: string): string;
@@ -1,74 +1,72 @@
1
1
  /**
2
- * WHICH ffmpeg a node runs, and why it is never the host's.
2
+ * WHICH ffmpeg a node runs — one rule, on every node.
3
3
  *
4
- * CamStack spawns ffmpeg for the broker's egress and derived transcodes, the
5
- * WebRTC transcode leg, the recorder, snapshots and the camera grid. What that
6
- * binary is compiled with decides whether any of them can use the machine's
7
- * hardware, so it is not a detail to leave to whatever the host happens to have
8
- * on its PATH:
4
+ * ## The rule
9
5
  *
10
- * - a host binary is an unknown version with unknown vendor libraries, and it
11
- * changes under us on any host update;
12
- * - a self-hosted NVR cannot assume the host has ffmpeg at all.
6
+ * 1. `CAMSTACK_FFMPEG_PATH`, when the operator set it and it exists. Pointing
7
+ * at your own build is a deliberate act and it wins over everything.
8
+ * 2. The pinned build this node has already downloaded, named by release
9
+ * (`ffmpeg-<release>`), in `<dataDir>/deps`.
10
+ * 3. Nothing — the caller downloads it. See `ffmpeg-artifacts.ts`.
13
11
  *
14
- * So the binary is always OURS. Two ways it can be ours, and which one applies
15
- * is a property of where the node runs — not of an environment variable:
12
+ * The system PATH is not on that list and must never be. A host binary is an
13
+ * unknown version with unknown vendor libraries, and it changes under us on any
14
+ * host update; a self-hosted NVR cannot assume the host has ffmpeg at all
15
+ * (D536).
16
16
  *
17
- * | Node | Binary |
18
- * | --- | --- |
19
- * | our container image | {@link BUNDLED_FFMPEG_PATH}, installed by the image |
20
- * | anything else (a native macOS agent) | the static build we download into `<dataDir>/deps` |
17
+ * ## Why the container image is not special any more
21
18
  *
22
- * `BUNDLED_FFMPEG_PATH` is a CamStack-owned path that only our image creates
23
- * (a link to the ffmpeg the image installs). That is what makes "is this ours?"
24
- * answerable: `/usr/bin/ffmpeg` is ours INSIDE the image and the host's outside
25
- * it, and nothing in the process can tell those apart. A path only we ever
26
- * write can.
19
+ * It was, for one day: the image installed ffmpeg, CamStack preferred it, and
20
+ * that made the answer depend on where a node ran — an image node got VAAPI, a
21
+ * native one got a portable build with none. Now every node downloads the same
22
+ * pinned build, so:
27
23
  *
28
- * The image build is the reason to prefer it over the download: the image
29
- * installs Ubuntu's ffmpeg next to the Intel media driver, so it carries VAAPI
30
- * and QSV. The portable static builds carry neither — measured on the hub,
31
- * 2026-09-18:
24
+ * - the ffmpeg version is DATA, not code: changing it is a framework bump, not
25
+ * an image rebuild and a container swap on every machine;
26
+ * - we ship no ffmpeg binary, so we convey no GPL work — the node fetches it
27
+ * from the project that publishes it;
28
+ * - one binary, one set of capabilities, everywhere. A bug that depends on
29
+ * which ffmpeg answered stops being possible.
32
30
  *
33
- * ```
34
- * /data/deps/ffmpeg 7.0.2 johnvansickle static hwaccels: vdpau
35
- * /usr/bin/ffmpeg 6.1.1 Ubuntu, in the image hwaccels: vdpau cuda vaapi qsv drm opencl vulkan
36
- * ```
31
+ * What the image still owes is the DRIVERS, which no binary can bring: libva
32
+ * plus the iHD media driver for Intel. Measured 2026-09-19 — the portable build
33
+ * encodes VAAPI inside our container and ABORTS on the bare Unraid host, where
34
+ * `libva-drm.so.2` does not exist.
37
35
  *
38
- * A child spawned with the first one asks for `-hwaccel vaapi` and gets
39
- * `Device creation failed: -12` — the name parses, the device cannot exist —
40
- * then transcodes in software with nothing naming the cause.
36
+ * ## Absent is absent
41
37
  *
42
- * {@link FFMPEG_PATH_ENV} is an operator's VOLUNTARY override, and only that:
43
- * unset is the normal, correct state on every node, and nothing in the product
44
- * sets it.
38
+ * The naming carries the release, so two versions coexist during a change and a
39
+ * rollback is a file that is already on disk rather than a download.
45
40
  */
46
- /** Where our container image puts the ffmpeg it installs. Only the image writes this. */
47
- export declare const BUNDLED_FFMPEG_PATH = "/opt/camstack/bin/ffmpeg";
48
41
  /** Operator override. Unset on every node unless a human deliberately set it. */
49
42
  export declare const FFMPEG_PATH_ENV = "CAMSTACK_FFMPEG_PATH";
50
- export type FfmpegBinaryOrigin = 'override' | 'bundled' | 'downloaded';
43
+ export type FfmpegBinaryOrigin = 'override' | 'downloaded';
51
44
  export interface FfmpegBinaryChoice {
52
45
  readonly origin: FfmpegBinaryOrigin;
53
46
  readonly path: string;
54
47
  }
55
48
  export interface FfmpegBinarySourceInput {
56
49
  readonly platform: string;
57
- /** `<dataDir>/deps/ffmpeg`, where a downloaded copy lands. */
58
- readonly downloadedPath: string;
50
+ /** `<dataDir>/deps/ffmpeg-<release>` — where the pinned build lands. */
51
+ readonly pinnedPath: string;
59
52
  /** The raw value of {@link FFMPEG_PATH_ENV}, or null/empty when unset. */
60
53
  readonly override: string | null | undefined;
61
54
  readonly exists: (path: string) => boolean;
62
55
  }
63
56
  /**
64
- * The binary to use, or `null` when there is nothing yet and one must be
65
- * downloaded.
57
+ * The file name the pinned build is stored under.
58
+ *
59
+ * Versioned on purpose: a download is then idempotent, two releases can sit
60
+ * side by side while a change rolls through a cluster, and going back is a path
61
+ * that already exists instead of a fetch that has to succeed.
62
+ */
63
+ export declare function ffmpegBinaryName(platform: string): string;
64
+ /**
65
+ * The binary to use, or `null` when it has to be downloaded first.
66
66
  *
67
67
  * An override that does not exist is NOT silently skipped — it is an operator
68
68
  * mistake, and falling through to a different binary than the one they named
69
- * would hide it. The caller reports it and carries on with the default, which
70
- * is the only safe direction: a typo must not stop a node from serving video.
69
+ * would hide it. The caller reports it and carries on with the pinned build,
70
+ * which is the only safe direction: a typo must not stop a node serving video.
71
71
  */
72
72
  export declare function chooseFfmpegBinary(input: FfmpegBinarySourceInput): FfmpegBinaryChoice | null;
73
- /** The image-provided path for a platform, or `null` where we ship no image. */
74
- export declare function bundledFfmpegPath(platform: string): string | null;
@@ -1,47 +1,18 @@
1
1
  import type { IScopedLogger } from '../interfaces/logging.js';
2
2
  /**
3
- * One artifact per platform+arch: the URL, how it is packed, and what the
4
- * binary inside is SUPPOSED to be able to do.
3
+ * The ffmpeg this node runs, downloading the pinned build if it is not here
4
+ * yet, and reporting what it got.
5
5
  *
6
- * It is a table rather than two functions computing strings because the three
7
- * facts belong together and drifted apart when they did not:
6
+ * Order: the operator's `CAMSTACK_FFMPEG_PATH` → the pinned build already on
7
+ * disk → download it. No PATH, no image copy, no per-platform special case —
8
+ * see `ffmpeg-binary-source.ts` for why the image stopped being special after
9
+ * exactly one day.
8
10
  *
9
- * - the darwin branch computed an arch and then discarded it, handing an
10
- * Intel Mac an arm64 Mach-O it cannot execute under any translation;
11
- * - a single `FFMPEG_VERSION = '7.1'` named the version of nothing — the linux
12
- * URL is unversioned and serves 7.0.2 — so the inner path built from it never
13
- * matched and only the extractor's recursive fallback saved it;
14
- * - and nothing recorded what a download was expected to be capable of, which
15
- * is how a build with no VAAPI at all served every transcode on this fleet
16
- * for months (D536).
17
- *
18
- * `expectedHwaccels` is that missing claim. It is deliberately EMPTY for the
19
- * linux static builds: they carry no hardware support and pretending otherwise
20
- * would make the assertion a lie. That emptiness is the honest description of
21
- * a portable build, and the reason a node that has an image prefers the image's
22
- * ffmpeg over this one.
23
- */
24
- export interface FfmpegArtifact {
25
- readonly url: string;
26
- readonly archiveFormat: 'zip' | 'tar.gz' | 'tar.xz';
27
- /** `-hwaccels` names this artifact is built with. Empty = none, honestly. */
28
- readonly expectedHwaccels: readonly string[];
29
- }
30
- /** The artifact for this node, or a refusal naming what was asked for. */
31
- export declare function getFfmpegArtifact(platform: string, arch: string): FfmpegArtifact;
32
- /** The URL alone, for a caller that wants nothing else. */
33
- export declare function getFfmpegDownloadUrl(platform: string, arch: string): string;
34
- /**
35
- * Ensure an ffmpeg binary WE provide is available on this node, and report
36
- * which one and what it can do.
37
- *
38
- * Order: the operator's voluntary override → the one our image installed → a
39
- * copy we already downloaded → download it. The system PATH is not in that
40
- * list and must never be: see `ffmpeg-binary-source.ts`.
41
- *
42
- * The line it logs is the point as much as the path is. "Which ffmpeg am I
43
- * running, and does it have vaapi" had no answer anywhere in the product, and
44
- * that is what let a hardware-incapable binary serve every transcode on a hub
45
- * with a working iGPU for months, silently in software.
11
+ * The line it logs is half the point. "Which ffmpeg am I running, and does it
12
+ * have vaapi" had no answer anywhere in the product, and that is what let a
13
+ * hardware-incapable binary serve every transcode on a hub with a working iGPU
14
+ * for months. The other half is the assertion beneath it: a build that probes
15
+ * short of what its artifact claims is a source that changed under us, and it
16
+ * says so with both lists.
46
17
  */
47
18
  export declare function ensureFfmpeg(dataDir: string, logger: IScopedLogger): Promise<string>;
@@ -1,3 +1,4 @@
1
1
  export { ensureBinary, downloadBinary, findInPath, getPlatformInfo, buildBinaryPath, } from './binary-downloader.js';
2
- export { ensureFfmpeg, getFfmpegArtifact, getFfmpegDownloadUrl } from './ffmpeg-downloader.js';
2
+ export { ensureFfmpeg } from './ffmpeg-downloader.js';
3
+ export { FFMPEG_RELEASE, getFfmpegArtifact, getFfmpegDownloadUrl } from './ffmpeg-artifacts.js';
3
4
  export { ensurePython, installPythonPackages, installPythonRequirements, getPythonDownloadUrl, PYTHON_VERSION, } from './python-downloader.js';
package/dist/index.js CHANGED
@@ -1072,6 +1072,7 @@ var CameraSwitchIdSchema = zod.z.enum([
1072
1072
  "device-audio",
1073
1073
  "broker-audio",
1074
1074
  "audio-analysis",
1075
+ "event-saving",
1075
1076
  "recording",
1076
1077
  "notifications"
1077
1078
  ]);
@@ -1098,6 +1099,7 @@ var CAMERA_SWITCH_ORDER = [
1098
1099
  "device-audio",
1099
1100
  "broker-audio",
1100
1101
  "audio-analysis",
1102
+ "event-saving",
1101
1103
  "recording",
1102
1104
  "notifications"
1103
1105
  ];
@@ -1182,6 +1184,13 @@ var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
1182
1184
  */
1183
1185
  var PRIVACY_MASK_CAP_NAME = "privacy-mask";
1184
1186
  /**
1187
+ * The wrapper that TURNS DETECTIONS INTO STORED HISTORY — tracks, events and
1188
+ * their media. Named here for the same reason as the others, and with one
1189
+ * extra: it has no gate of its own except this binding, so it is the only
1190
+ * thing standing between a camera's detections and the database.
1191
+ */
1192
+ var PIPELINE_ANALYTICS_CAP_NAME = "pipeline-analytics";
1193
+ /**
1185
1194
  * Every analyser a camera CamStack BUILDS is created with switched off.
1186
1195
  *
1187
1196
  * A grid and a Terminal camera are `DeviceType.Camera`, so every wrapper that
@@ -1210,7 +1219,7 @@ var DERIVED_CAMERA_SILENCED_CAP_NAMES = [
1210
1219
  "motion-detection",
1211
1220
  DETECTION_PIPELINE_CAP_NAME,
1212
1221
  AUDIO_ANALYSIS_CAP_NAME,
1213
- "pipeline-analytics",
1222
+ PIPELINE_ANALYTICS_CAP_NAME,
1214
1223
  "scene-monitor"
1215
1224
  ];
1216
1225
  /**
@@ -1272,6 +1281,16 @@ var CAMERA_SWITCH_CATALOG = {
1272
1281
  },
1273
1282
  countsAsSwitchedOff: true
1274
1283
  },
1284
+ "event-saving": {
1285
+ id: "event-saving",
1286
+ label: "Event saving",
1287
+ costWhenOff: "Off: nothing about this camera is written to its history — no tracks, no events and no event snapshots or clips. Detection keeps running and live boxes still appear, so the camera looks busy while its timeline stays empty, and anything that fires ON an event (notification rules, event-triggered recording) has nothing to fire on. Continuous recording is unaffected, and history already written is kept.",
1288
+ authority: {
1289
+ kind: "wrapper-binding",
1290
+ capName: PIPELINE_ANALYTICS_CAP_NAME
1291
+ },
1292
+ countsAsSwitchedOff: true
1293
+ },
1275
1294
  recording: {
1276
1295
  id: "recording",
1277
1296
  label: "Recording",
@@ -59037,6 +59056,7 @@ exports.OsdSourceValueTypeEnum = OsdSourceValueTypeEnum;
59037
59056
  exports.OsdStatusSchema = OsdStatusSchema;
59038
59057
  exports.PET_FEEDER_MANUAL_FEED_MAX = PET_FEEDER_MANUAL_FEED_MAX;
59039
59058
  exports.PET_FEEDER_MANUAL_FEED_MIN = PET_FEEDER_MANUAL_FEED_MIN;
59059
+ exports.PIPELINE_ANALYTICS_CAP_NAME = PIPELINE_ANALYTICS_CAP_NAME;
59040
59060
  exports.PIPELINE_FLOW_CAPABILITY_NAMES = PIPELINE_FLOW_CAPABILITY_NAMES;
59041
59061
  exports.PIPELINE_OWNER_CAPABILITY_NAMES = PIPELINE_OWNER_CAPABILITY_NAMES;
59042
59062
  exports.PRIVACY_MASK_CAP_NAME = PRIVACY_MASK_CAP_NAME;