@camstack/addon-decoder-ffmpeg 1.2.18 → 1.2.20

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.
Files changed (3) hide show
  1. package/dist/index.js +186 -28
  2. package/dist/index.mjs +186 -28
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ Object.defineProperties(exports, {
4
4
  });
5
5
  let node_crypto = require("node:crypto");
6
6
  let node_child_process = require("node:child_process");
7
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
7
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
8
8
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
9
9
  EventCategory["SystemBoot"] = "system.boot";
10
10
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -21,9 +21,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
21
21
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
22
22
  /**
23
23
  * A newer addon or server-root package version was found by the
24
- * authoritative registry check. Emitted once per
25
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
26
- * polling of the same result is deduplicated by the checker.
24
+ * authoritative registry check. Emitted once when any observed
25
+ * `latestVersion` changes (or a package/node first appears behind);
26
+ * the payload carries the full currently-available list. Repeated
27
+ * polling of the same latests is silent.
27
28
  */
28
29
  EventCategory["UpdateAvailable"] = "update.available";
29
30
  /**
@@ -42,6 +43,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
42
43
  EventCategory["AddonInstalled"] = "addon.installed";
43
44
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
44
45
  EventCategory["AddonCrashed"] = "addon.crashed";
46
+ /**
47
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
48
+ *
49
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
50
+ * respawned"); this is the one that never resolves itself. Emitted exactly
51
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
52
+ * addons it hosted and the crash count that tripped the breaker.
53
+ *
54
+ * It exists because a runner marked terminally `failed` used to be SILENT:
55
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
56
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
57
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
58
+ * operator's phone told him. The Notification Center's system-event intake
59
+ * consumes this category and maps it to the `addon-crash-loop` kind.
60
+ */
61
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
45
62
  EventCategory["AddonError"] = "addon.error";
46
63
  EventCategory["AddonPageReady"] = "addon.page-ready";
47
64
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7549,6 +7566,10 @@ var RecordingBandSchema = object({
7549
7566
  preBufferSec: number().min(0).optional(),
7550
7567
  postBufferSec: number().min(0).optional()
7551
7568
  });
7569
+ ({
7570
+ preBufferSec: 10,
7571
+ postBufferSec: 30
7572
+ }).postBufferSec * 1e3;
7552
7573
  /**
7553
7574
  * Per-device retention overrides. Every field is optional; an unset or `0`
7554
7575
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7596,6 +7617,12 @@ var RecordingConfigSchema = object({
7596
7617
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7597
7618
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7598
7619
  mode: RecordingStorageModeSchema.optional(),
7620
+ /**
7621
+ * Which assigned broker slots to record. Absent / empty = {@link
7622
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7623
+ * camera's currently assigned slots — never `mid` unless the operator
7624
+ * picks it, and never a slot the broker has not assigned.
7625
+ */
7599
7626
  profiles: array(CamProfileSchema).optional(),
7600
7627
  segmentSeconds: number().int().positive().optional(),
7601
7628
  /**
@@ -7676,7 +7703,11 @@ var RelocateFootageInputSchema = object({
7676
7703
  profiles: array(string()).optional(),
7677
7704
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7678
7705
  * never allowed to starve live writers. */
7679
- throttleMbps: number().min(1).max(1e3).optional()
7706
+ throttleMbps: number().min(1).max(1e3).optional(),
7707
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7708
+ * pile. Used when a full drain is too expensive and the operator only
7709
+ * wants the recent window on the new disk. */
7710
+ sinceMs: number().int().optional()
7680
7711
  });
7681
7712
  /** Internal, lease-scoped participant operation. It is intentionally separate
7682
7713
  * from persistent recording settings: a migration never changes
@@ -14369,6 +14400,7 @@ var NcSystemEventKindSchema = _enum([
14369
14400
  "node-offline",
14370
14401
  "node-inference-unavailable",
14371
14402
  "detection-blind",
14403
+ "addon-crash-loop",
14372
14404
  "addon-update-available",
14373
14405
  "server-update-available",
14374
14406
  "alarm-triggered",
@@ -14376,6 +14408,9 @@ var NcSystemEventKindSchema = _enum([
14376
14408
  "alarm-disarmed",
14377
14409
  "alarm-arming",
14378
14410
  "alarm-arm-refused",
14411
+ "addon-updated",
14412
+ "server-updated",
14413
+ "export-completed",
14379
14414
  "camera-online",
14380
14415
  "camera-offline",
14381
14416
  "camera-disabled",
@@ -16269,13 +16304,19 @@ var RetrainStatusSchema = _enum([
16269
16304
  * "never marked" from "already trained" must read `retrainStatus`.
16270
16305
  *
16271
16306
  * `debug` does NOT pin; it is attention, not durability.
16307
+ *
16308
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16309
+ * A favourited track is skipped by retention the same way `staging` is, but
16310
+ * it does not enter `none|staging|trained` and has no staging budget.
16272
16311
  */
16273
16312
  var TrackFlagFields = {
16274
16313
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16275
16314
  * `'staging'`. */
16276
16315
  markForTrain: boolean().optional(),
16277
16316
  /** Operator marked this track for diagnostic attention. */
16278
- debug: boolean().optional()
16317
+ debug: boolean().optional(),
16318
+ /** Operator favourited this track. Pins it against pruning. */
16319
+ favourited: boolean().optional()
16279
16320
  };
16280
16321
  /**
16281
16322
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16300,6 +16341,7 @@ var TrackFlagsSchema = object({
16300
16341
  trackId: string(),
16301
16342
  markForTrain: boolean(),
16302
16343
  debug: boolean(),
16344
+ favourited: boolean(),
16303
16345
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16304
16346
  * a track row) because this shape is only ever produced by the write body,
16305
16347
  * which always knows it — and a surface that has just written needs to render
@@ -16932,6 +16974,12 @@ var TrackCascadeCountsSchema = object({
16932
16974
  /** Per-track CLIP search vectors removed (best-effort). */
16933
16975
  embeddings: number().int()
16934
16976
  });
16977
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16978
+ var DiskReconcileCountsSchema = object({
16979
+ mediaDropped: number().int(),
16980
+ tracks: number().int(),
16981
+ events: number().int()
16982
+ });
16935
16983
  /** Event-store footprint for one camera. */
16936
16984
  var EventStoreDeviceFootprintSchema = object({
16937
16985
  deviceId: number(),
@@ -17108,6 +17156,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17108
17156
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17109
17157
  kind: "mutation",
17110
17158
  auth: "admin"
17159
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17160
+ kind: "mutation",
17161
+ auth: "admin"
17111
17162
  }), method(object({
17112
17163
  deviceId: number(),
17113
17164
  trackIds: array(string()).min(1)
@@ -18618,6 +18669,24 @@ var CameraStatusDegradationSchema = object({
18618
18669
  *
18619
18670
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18620
18671
  */
18672
+ var DiskReconcileJobSchema = object({
18673
+ state: _enum([
18674
+ "idle",
18675
+ "running",
18676
+ "done",
18677
+ "error"
18678
+ ]),
18679
+ total: number().int().nonnegative(),
18680
+ completed: number().int().nonnegative(),
18681
+ currentDeviceId: number().int().nullable(),
18682
+ failed: array(number().int()).readonly(),
18683
+ mediaDropped: number().int().nonnegative(),
18684
+ tracks: number().int().nonnegative(),
18685
+ events: number().int().nonnegative(),
18686
+ startedAtMs: number().int().nullable(),
18687
+ finishedAtMs: number().int().nullable(),
18688
+ error: string().nullable()
18689
+ });
18621
18690
  var CameraStatusSchema = object({
18622
18691
  deviceId: number(),
18623
18692
  assignment: CameraAssignmentStatusSchema,
@@ -18849,7 +18918,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18849
18918
  }), CameraSwitchGroupSchema, {
18850
18919
  kind: "mutation",
18851
18920
  auth: "admin"
18852
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18921
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
18922
+ kind: "mutation",
18923
+ auth: "admin"
18924
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18853
18925
  name: string(),
18854
18926
  description: string().optional(),
18855
18927
  config: CameraPipelineConfigSchema
@@ -19254,7 +19326,14 @@ targets: array(object({
19254
19326
  "sleeping",
19255
19327
  "unreachable",
19256
19328
  "waking"
19257
- ]).nullable()
19329
+ ]).nullable(),
19330
+ /** A battery camera (whatever its current state). An AWAKE battery
19331
+ * camera is deliberately NOT recaptured on the poll cadence — every
19332
+ * capture is a camera hit that would keep it out of sleep — so its
19333
+ * cached frame legitimately ages past the currency ceiling while
19334
+ * nothing is streaming. A surface must keep painting it (a fresh
19335
+ * frame is captured at each wake), not blank to "unavailable". */
19336
+ battery: boolean()
19258
19337
  })));
19259
19338
  /**
19260
19339
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -19915,20 +19994,6 @@ var VectorStatsResultSchema = object({
19915
19994
  exact: boolean()
19916
19995
  });
19917
19996
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
19918
- /**
19919
- * `videoclips` — the unified, navigable-clip surface for a camera.
19920
- *
19921
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
19922
- * provider per device, substitutable. The DEFAULT provider (registered by
19923
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
19924
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
19925
- * is a time-WINDOW over existing footage, never a separate file. A camera that
19926
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
19927
- * wrapper provider for its device and serve its own clip catalog + URLs.
19928
- *
19929
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
19930
- * temporal overlap, so the API never decides `continuous` vs `events`.
19931
- */
19932
19997
  var ClipSchema = object({
19933
19998
  /** Opaque, provider-namespaced id. The default provider encodes the time
19934
19999
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -19945,8 +20010,57 @@ var ClipSchema = object({
19945
20010
  startMs: number(),
19946
20011
  endMs: number()
19947
20012
  }),
19948
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
19949
- thumbnail: string().optional()
20013
+ /**
20014
+ * Lazy thumbnail URL, never inlined.
20015
+ *
20016
+ * Recording-derived clips (events-mode keep-window, and the prepared
20017
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20018
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20019
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20020
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20021
+ * mint their own stills.
20022
+ *
20023
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20024
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20025
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20026
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20027
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20028
+ * A read that FAILS drops the claim; it never invents it.
20029
+ */
20030
+ thumbnail: string().optional(),
20031
+ /**
20032
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20033
+ * can be decoded. Present whenever the visit came from recorded availability;
20034
+ * absent on a per-event padded window (there is no footage to promise).
20035
+ *
20036
+ * This is not a thumbnail and not a second byte path: it is the argument to
20037
+ * the recorder's existing still route. The surface — never the provider —
20038
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20039
+ * contiguous range, not of the visit: a visit spans its holes by
20040
+ * construction, so a naive midpoint lands in dead air.
20041
+ */
20042
+ stillAtMs: number().optional(),
20043
+ /**
20044
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20045
+ * first within kind (object → motion → audio), capped at
20046
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20047
+ *
20048
+ * Bounded because it is not a payload the surface pages through: one visit on
20049
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20050
+ * camera-day. Read {@link eventCount} for the true total.
20051
+ */
20052
+ eventIds: array(string()).optional(),
20053
+ /** How many analytics events actually overlap this visit. Differs from
20054
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20055
+ * list is never mistaken for a quiet visit. */
20056
+ eventCount: number().int().nonnegative().optional(),
20057
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20058
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20059
+ * bar keeps showing them via `recording.getAvailability`. */
20060
+ holes: array(object({
20061
+ startMs: number(),
20062
+ endMs: number()
20063
+ })).optional()
19950
20064
  });
19951
20065
  var ClipPlaybackSchema = object({
19952
20066
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20410,7 +20524,14 @@ var SearchResultSchema = object({
20410
20524
  });
20411
20525
  var AutoUpdateSettingsSchema = object({
20412
20526
  channel: ChannelSchema,
20413
- intervalSeconds: number()
20527
+ intervalSeconds: number(),
20528
+ /**
20529
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20530
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20531
+ * about a publish and installing it are different decisions. Clamped
20532
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20533
+ */
20534
+ updateCheckIntervalSeconds: number()
20414
20535
  });
20415
20536
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20416
20537
  var RestartAddonResultSchema = unknown();
@@ -20551,7 +20672,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20551
20672
  auth: "admin"
20552
20673
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20553
20674
  channel: ChannelSchema,
20554
- intervalSeconds: number().min(300).max(86400).optional()
20675
+ intervalSeconds: number().min(300).max(86400).optional(),
20676
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20677
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20555
20678
  }), unknown(), {
20556
20679
  kind: "mutation",
20557
20680
  auth: "admin"
@@ -24544,7 +24667,9 @@ var ExportOptionsSchema = object({
24544
24667
  includeAudio: boolean(),
24545
24668
  maxLifeMs: number().int().positive(),
24546
24669
  deleteAfterDownload: boolean(),
24547
- title: string().max(200).optional()
24670
+ title: string().max(200).optional(),
24671
+ /** Notification-output target ids to ping when this export becomes ready. */
24672
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24548
24673
  }).superRefine((v, ctx) => {
24549
24674
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24550
24675
  code: ZodIssueCode.custom,
@@ -24603,10 +24728,18 @@ var ExportBytesSchema = object({
24603
24728
  });
24604
24729
  method(object({
24605
24730
  deviceId: number(),
24606
- profile: string(),
24731
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24732
+ profile: string().optional(),
24733
+ profiles: array(string()).min(1).optional(),
24607
24734
  fromMs: number(),
24608
24735
  toMs: number(),
24609
24736
  options: ExportOptionsSchema
24737
+ }).superRefine((v, ctx) => {
24738
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24739
+ code: ZodIssueCode.custom,
24740
+ message: "pass profiles[] (min 1) or legacy profile",
24741
+ path: ["profiles"]
24742
+ });
24610
24743
  }), ExportRecordSchema, {
24611
24744
  kind: "mutation",
24612
24745
  auth: "protected"
@@ -29206,6 +29339,12 @@ Object.freeze({
29206
29339
  addonId: null,
29207
29340
  access: "create"
29208
29341
  },
29342
+ "pipelineAnalytics.reconcileFromDisk": {
29343
+ capName: "pipeline-analytics",
29344
+ capScope: "device",
29345
+ addonId: null,
29346
+ access: "create"
29347
+ },
29209
29348
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29210
29349
  capName: "pipeline-analytics",
29211
29350
  capScope: "device",
@@ -29608,6 +29747,12 @@ Object.freeze({
29608
29747
  addonId: null,
29609
29748
  access: "view"
29610
29749
  },
29750
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29751
+ capName: "pipeline-orchestrator",
29752
+ capScope: "system",
29753
+ addonId: null,
29754
+ access: "view"
29755
+ },
29611
29756
  "pipelineOrchestrator.listAgentSettings": {
29612
29757
  capName: "pipeline-orchestrator",
29613
29758
  capScope: "system",
@@ -29632,6 +29777,12 @@ Object.freeze({
29632
29777
  addonId: null,
29633
29778
  access: "create"
29634
29779
  },
29780
+ "pipelineOrchestrator.reconcileFromDisk": {
29781
+ capName: "pipeline-orchestrator",
29782
+ capScope: "system",
29783
+ addonId: null,
29784
+ access: "create"
29785
+ },
29635
29786
  "pipelineOrchestrator.removeAgentSettings": {
29636
29787
  capName: "pipeline-orchestrator",
29637
29788
  capScope: "system",
@@ -32623,6 +32774,11 @@ Object.freeze({
32623
32774
  form: "single",
32624
32775
  optional: true
32625
32776
  }],
32777
+ "pipelineAnalytics.reconcileFromDisk": [{
32778
+ name: "deviceId",
32779
+ form: "single",
32780
+ optional: false
32781
+ }],
32626
32782
  "pipelineAnalytics.restageRetrainTrack": [{
32627
32783
  name: "deviceId",
32628
32784
  form: "single",
@@ -33688,6 +33844,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33688
33844
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33689
33845
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33690
33846
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33847
+ var MB = 1024 * 1024;
33848
+ 1024 * MB, 3072 * MB;
33691
33849
  /**
33692
33850
  * Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
33693
33851
  * drain() returns up to maxCount items in FIFO order and removes them.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
3
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
4
4
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
5
5
  EventCategory["SystemBoot"] = "system.boot";
6
6
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -17,9 +17,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
17
17
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
18
18
  /**
19
19
  * A newer addon or server-root package version was found by the
20
- * authoritative registry check. Emitted once per
21
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
22
- * polling of the same result is deduplicated by the checker.
20
+ * authoritative registry check. Emitted once when any observed
21
+ * `latestVersion` changes (or a package/node first appears behind);
22
+ * the payload carries the full currently-available list. Repeated
23
+ * polling of the same latests is silent.
23
24
  */
24
25
  EventCategory["UpdateAvailable"] = "update.available";
25
26
  /**
@@ -38,6 +39,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
38
39
  EventCategory["AddonInstalled"] = "addon.installed";
39
40
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
40
41
  EventCategory["AddonCrashed"] = "addon.crashed";
42
+ /**
43
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
44
+ *
45
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
46
+ * respawned"); this is the one that never resolves itself. Emitted exactly
47
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
48
+ * addons it hosted and the crash count that tripped the breaker.
49
+ *
50
+ * It exists because a runner marked terminally `failed` used to be SILENT:
51
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
52
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
53
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
54
+ * operator's phone told him. The Notification Center's system-event intake
55
+ * consumes this category and maps it to the `addon-crash-loop` kind.
56
+ */
57
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
41
58
  EventCategory["AddonError"] = "addon.error";
42
59
  EventCategory["AddonPageReady"] = "addon.page-ready";
43
60
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7545,6 +7562,10 @@ var RecordingBandSchema = object({
7545
7562
  preBufferSec: number().min(0).optional(),
7546
7563
  postBufferSec: number().min(0).optional()
7547
7564
  });
7565
+ ({
7566
+ preBufferSec: 10,
7567
+ postBufferSec: 30
7568
+ }).postBufferSec * 1e3;
7548
7569
  /**
7549
7570
  * Per-device retention overrides. Every field is optional; an unset or `0`
7550
7571
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7592,6 +7613,12 @@ var RecordingConfigSchema = object({
7592
7613
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7593
7614
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7594
7615
  mode: RecordingStorageModeSchema.optional(),
7616
+ /**
7617
+ * Which assigned broker slots to record. Absent / empty = {@link
7618
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7619
+ * camera's currently assigned slots — never `mid` unless the operator
7620
+ * picks it, and never a slot the broker has not assigned.
7621
+ */
7595
7622
  profiles: array(CamProfileSchema).optional(),
7596
7623
  segmentSeconds: number().int().positive().optional(),
7597
7624
  /**
@@ -7672,7 +7699,11 @@ var RelocateFootageInputSchema = object({
7672
7699
  profiles: array(string()).optional(),
7673
7700
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7674
7701
  * never allowed to starve live writers. */
7675
- throttleMbps: number().min(1).max(1e3).optional()
7702
+ throttleMbps: number().min(1).max(1e3).optional(),
7703
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7704
+ * pile. Used when a full drain is too expensive and the operator only
7705
+ * wants the recent window on the new disk. */
7706
+ sinceMs: number().int().optional()
7676
7707
  });
7677
7708
  /** Internal, lease-scoped participant operation. It is intentionally separate
7678
7709
  * from persistent recording settings: a migration never changes
@@ -14365,6 +14396,7 @@ var NcSystemEventKindSchema = _enum([
14365
14396
  "node-offline",
14366
14397
  "node-inference-unavailable",
14367
14398
  "detection-blind",
14399
+ "addon-crash-loop",
14368
14400
  "addon-update-available",
14369
14401
  "server-update-available",
14370
14402
  "alarm-triggered",
@@ -14372,6 +14404,9 @@ var NcSystemEventKindSchema = _enum([
14372
14404
  "alarm-disarmed",
14373
14405
  "alarm-arming",
14374
14406
  "alarm-arm-refused",
14407
+ "addon-updated",
14408
+ "server-updated",
14409
+ "export-completed",
14375
14410
  "camera-online",
14376
14411
  "camera-offline",
14377
14412
  "camera-disabled",
@@ -16265,13 +16300,19 @@ var RetrainStatusSchema = _enum([
16265
16300
  * "never marked" from "already trained" must read `retrainStatus`.
16266
16301
  *
16267
16302
  * `debug` does NOT pin; it is attention, not durability.
16303
+ *
16304
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16305
+ * A favourited track is skipped by retention the same way `staging` is, but
16306
+ * it does not enter `none|staging|trained` and has no staging budget.
16268
16307
  */
16269
16308
  var TrackFlagFields = {
16270
16309
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16271
16310
  * `'staging'`. */
16272
16311
  markForTrain: boolean().optional(),
16273
16312
  /** Operator marked this track for diagnostic attention. */
16274
- debug: boolean().optional()
16313
+ debug: boolean().optional(),
16314
+ /** Operator favourited this track. Pins it against pruning. */
16315
+ favourited: boolean().optional()
16275
16316
  };
16276
16317
  /**
16277
16318
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16296,6 +16337,7 @@ var TrackFlagsSchema = object({
16296
16337
  trackId: string(),
16297
16338
  markForTrain: boolean(),
16298
16339
  debug: boolean(),
16340
+ favourited: boolean(),
16299
16341
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16300
16342
  * a track row) because this shape is only ever produced by the write body,
16301
16343
  * which always knows it — and a surface that has just written needs to render
@@ -16928,6 +16970,12 @@ var TrackCascadeCountsSchema = object({
16928
16970
  /** Per-track CLIP search vectors removed (best-effort). */
16929
16971
  embeddings: number().int()
16930
16972
  });
16973
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16974
+ var DiskReconcileCountsSchema = object({
16975
+ mediaDropped: number().int(),
16976
+ tracks: number().int(),
16977
+ events: number().int()
16978
+ });
16931
16979
  /** Event-store footprint for one camera. */
16932
16980
  var EventStoreDeviceFootprintSchema = object({
16933
16981
  deviceId: number(),
@@ -17104,6 +17152,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17104
17152
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17105
17153
  kind: "mutation",
17106
17154
  auth: "admin"
17155
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17156
+ kind: "mutation",
17157
+ auth: "admin"
17107
17158
  }), method(object({
17108
17159
  deviceId: number(),
17109
17160
  trackIds: array(string()).min(1)
@@ -18614,6 +18665,24 @@ var CameraStatusDegradationSchema = object({
18614
18665
  *
18615
18666
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18616
18667
  */
18668
+ var DiskReconcileJobSchema = object({
18669
+ state: _enum([
18670
+ "idle",
18671
+ "running",
18672
+ "done",
18673
+ "error"
18674
+ ]),
18675
+ total: number().int().nonnegative(),
18676
+ completed: number().int().nonnegative(),
18677
+ currentDeviceId: number().int().nullable(),
18678
+ failed: array(number().int()).readonly(),
18679
+ mediaDropped: number().int().nonnegative(),
18680
+ tracks: number().int().nonnegative(),
18681
+ events: number().int().nonnegative(),
18682
+ startedAtMs: number().int().nullable(),
18683
+ finishedAtMs: number().int().nullable(),
18684
+ error: string().nullable()
18685
+ });
18617
18686
  var CameraStatusSchema = object({
18618
18687
  deviceId: number(),
18619
18688
  assignment: CameraAssignmentStatusSchema,
@@ -18845,7 +18914,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18845
18914
  }), CameraSwitchGroupSchema, {
18846
18915
  kind: "mutation",
18847
18916
  auth: "admin"
18848
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18917
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
18918
+ kind: "mutation",
18919
+ auth: "admin"
18920
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
18849
18921
  name: string(),
18850
18922
  description: string().optional(),
18851
18923
  config: CameraPipelineConfigSchema
@@ -19250,7 +19322,14 @@ targets: array(object({
19250
19322
  "sleeping",
19251
19323
  "unreachable",
19252
19324
  "waking"
19253
- ]).nullable()
19325
+ ]).nullable(),
19326
+ /** A battery camera (whatever its current state). An AWAKE battery
19327
+ * camera is deliberately NOT recaptured on the poll cadence — every
19328
+ * capture is a camera hit that would keep it out of sleep — so its
19329
+ * cached frame legitimately ages past the currency ceiling while
19330
+ * nothing is streaming. A surface must keep painting it (a fresh
19331
+ * frame is captured at each wake), not blank to "unavailable". */
19332
+ battery: boolean()
19254
19333
  })));
19255
19334
  /**
19256
19335
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -19911,20 +19990,6 @@ var VectorStatsResultSchema = object({
19911
19990
  exact: boolean()
19912
19991
  });
19913
19992
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
19914
- /**
19915
- * `videoclips` — the unified, navigable-clip surface for a camera.
19916
- *
19917
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
19918
- * provider per device, substitutable. The DEFAULT provider (registered by
19919
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
19920
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
19921
- * is a time-WINDOW over existing footage, never a separate file. A camera that
19922
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
19923
- * wrapper provider for its device and serve its own clip catalog + URLs.
19924
- *
19925
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
19926
- * temporal overlap, so the API never decides `continuous` vs `events`.
19927
- */
19928
19993
  var ClipSchema = object({
19929
19994
  /** Opaque, provider-namespaced id. The default provider encodes the time
19930
19995
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -19941,8 +20006,57 @@ var ClipSchema = object({
19941
20006
  startMs: number(),
19942
20007
  endMs: number()
19943
20008
  }),
19944
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
19945
- thumbnail: string().optional()
20009
+ /**
20010
+ * Lazy thumbnail URL, never inlined.
20011
+ *
20012
+ * Recording-derived clips (events-mode keep-window, and the prepared
20013
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20014
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20015
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20016
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20017
+ * mint their own stills.
20018
+ *
20019
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20020
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20021
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20022
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20023
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20024
+ * A read that FAILS drops the claim; it never invents it.
20025
+ */
20026
+ thumbnail: string().optional(),
20027
+ /**
20028
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20029
+ * can be decoded. Present whenever the visit came from recorded availability;
20030
+ * absent on a per-event padded window (there is no footage to promise).
20031
+ *
20032
+ * This is not a thumbnail and not a second byte path: it is the argument to
20033
+ * the recorder's existing still route. The surface — never the provider —
20034
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20035
+ * contiguous range, not of the visit: a visit spans its holes by
20036
+ * construction, so a naive midpoint lands in dead air.
20037
+ */
20038
+ stillAtMs: number().optional(),
20039
+ /**
20040
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20041
+ * first within kind (object → motion → audio), capped at
20042
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20043
+ *
20044
+ * Bounded because it is not a payload the surface pages through: one visit on
20045
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20046
+ * camera-day. Read {@link eventCount} for the true total.
20047
+ */
20048
+ eventIds: array(string()).optional(),
20049
+ /** How many analytics events actually overlap this visit. Differs from
20050
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20051
+ * list is never mistaken for a quiet visit. */
20052
+ eventCount: number().int().nonnegative().optional(),
20053
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20054
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20055
+ * bar keeps showing them via `recording.getAvailability`. */
20056
+ holes: array(object({
20057
+ startMs: number(),
20058
+ endMs: number()
20059
+ })).optional()
19946
20060
  });
19947
20061
  var ClipPlaybackSchema = object({
19948
20062
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20406,7 +20520,14 @@ var SearchResultSchema = object({
20406
20520
  });
20407
20521
  var AutoUpdateSettingsSchema = object({
20408
20522
  channel: ChannelSchema,
20409
- intervalSeconds: number()
20523
+ intervalSeconds: number(),
20524
+ /**
20525
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20526
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20527
+ * about a publish and installing it are different decisions. Clamped
20528
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20529
+ */
20530
+ updateCheckIntervalSeconds: number()
20410
20531
  });
20411
20532
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20412
20533
  var RestartAddonResultSchema = unknown();
@@ -20547,7 +20668,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20547
20668
  auth: "admin"
20548
20669
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20549
20670
  channel: ChannelSchema,
20550
- intervalSeconds: number().min(300).max(86400).optional()
20671
+ intervalSeconds: number().min(300).max(86400).optional(),
20672
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20673
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20551
20674
  }), unknown(), {
20552
20675
  kind: "mutation",
20553
20676
  auth: "admin"
@@ -24540,7 +24663,9 @@ var ExportOptionsSchema = object({
24540
24663
  includeAudio: boolean(),
24541
24664
  maxLifeMs: number().int().positive(),
24542
24665
  deleteAfterDownload: boolean(),
24543
- title: string().max(200).optional()
24666
+ title: string().max(200).optional(),
24667
+ /** Notification-output target ids to ping when this export becomes ready. */
24668
+ notifyTargetIds: array(string().min(1)).max(20).optional()
24544
24669
  }).superRefine((v, ctx) => {
24545
24670
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
24546
24671
  code: ZodIssueCode.custom,
@@ -24599,10 +24724,18 @@ var ExportBytesSchema = object({
24599
24724
  });
24600
24725
  method(object({
24601
24726
  deviceId: number(),
24602
- profile: string(),
24727
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
24728
+ profile: string().optional(),
24729
+ profiles: array(string()).min(1).optional(),
24603
24730
  fromMs: number(),
24604
24731
  toMs: number(),
24605
24732
  options: ExportOptionsSchema
24733
+ }).superRefine((v, ctx) => {
24734
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
24735
+ code: ZodIssueCode.custom,
24736
+ message: "pass profiles[] (min 1) or legacy profile",
24737
+ path: ["profiles"]
24738
+ });
24606
24739
  }), ExportRecordSchema, {
24607
24740
  kind: "mutation",
24608
24741
  auth: "protected"
@@ -29202,6 +29335,12 @@ Object.freeze({
29202
29335
  addonId: null,
29203
29336
  access: "create"
29204
29337
  },
29338
+ "pipelineAnalytics.reconcileFromDisk": {
29339
+ capName: "pipeline-analytics",
29340
+ capScope: "device",
29341
+ addonId: null,
29342
+ access: "create"
29343
+ },
29205
29344
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29206
29345
  capName: "pipeline-analytics",
29207
29346
  capScope: "device",
@@ -29604,6 +29743,12 @@ Object.freeze({
29604
29743
  addonId: null,
29605
29744
  access: "view"
29606
29745
  },
29746
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
29747
+ capName: "pipeline-orchestrator",
29748
+ capScope: "system",
29749
+ addonId: null,
29750
+ access: "view"
29751
+ },
29607
29752
  "pipelineOrchestrator.listAgentSettings": {
29608
29753
  capName: "pipeline-orchestrator",
29609
29754
  capScope: "system",
@@ -29628,6 +29773,12 @@ Object.freeze({
29628
29773
  addonId: null,
29629
29774
  access: "create"
29630
29775
  },
29776
+ "pipelineOrchestrator.reconcileFromDisk": {
29777
+ capName: "pipeline-orchestrator",
29778
+ capScope: "system",
29779
+ addonId: null,
29780
+ access: "create"
29781
+ },
29631
29782
  "pipelineOrchestrator.removeAgentSettings": {
29632
29783
  capName: "pipeline-orchestrator",
29633
29784
  capScope: "system",
@@ -32619,6 +32770,11 @@ Object.freeze({
32619
32770
  form: "single",
32620
32771
  optional: true
32621
32772
  }],
32773
+ "pipelineAnalytics.reconcileFromDisk": [{
32774
+ name: "deviceId",
32775
+ form: "single",
32776
+ optional: false
32777
+ }],
32622
32778
  "pipelineAnalytics.restageRetrainTrack": [{
32623
32779
  name: "deviceId",
32624
32780
  form: "single",
@@ -33684,6 +33840,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
33684
33840
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
33685
33841
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
33686
33842
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
33843
+ var MB = 1024 * 1024;
33844
+ 1024 * MB, 3072 * MB;
33687
33845
  /**
33688
33846
  * Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
33689
33847
  * drain() returns up to maxCount items in FIFO order and removes them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-ffmpeg",
3
- "version": "1.2.18",
3
+ "version": "1.2.20",
4
4
  "description": "Standalone ffmpeg-subprocess decoder fallback addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",