@camstack/addon-decoder-ffmpeg 1.2.26 → 1.2.27

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 +184 -34
  2. package/dist/index.mjs +184 -34
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5806,6 +5806,13 @@ var BaseAddon = class {
5806
5806
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5807
5807
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5808
5808
  _registeredCapNames = [];
5809
+ /**
5810
+ * True only after `readAddonStore` actually answered. Constructor
5811
+ * defaults look like stored config when the store is down — a forked
5812
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5813
+ * mode, 2026-08-25) is not "the operator chose this".
5814
+ */
5815
+ settingsStoreReady = false;
5809
5816
  /** Default config values. Provided via constructor. */
5810
5817
  defaults;
5811
5818
  constructor(defaults) {
@@ -6206,7 +6213,9 @@ var BaseAddon = class {
6206
6213
  ];
6207
6214
  let lastErr;
6208
6215
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6209
- return await settings.readAddonStore() ?? {};
6216
+ const stored = await settings.readAddonStore() ?? {};
6217
+ this.settingsStoreReady = true;
6218
+ return stored;
6210
6219
  } catch (err) {
6211
6220
  lastErr = err;
6212
6221
  const msg = err instanceof Error ? err.message : String(err);
@@ -6214,6 +6223,7 @@ var BaseAddon = class {
6214
6223
  if (attempt === delaysMs.length) break;
6215
6224
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6216
6225
  }
6226
+ this.settingsStoreReady = false;
6217
6227
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6218
6228
  return {};
6219
6229
  }
@@ -8113,6 +8123,12 @@ var ModelVariantGroupSchema = object({
8113
8123
  */
8114
8124
  resolution: number().int().positive().optional()
8115
8125
  });
8126
+ var ModelProviderIdSchema = _enum([
8127
+ "camstack",
8128
+ "frigate",
8129
+ "scrypted",
8130
+ "custom"
8131
+ ]);
8116
8132
  var ModelCatalogEntrySchema = object({
8117
8133
  id: string(),
8118
8134
  name: string(),
@@ -8210,6 +8226,12 @@ var ModelCatalogEntrySchema = object({
8210
8226
  */
8211
8227
  group: ModelVariantGroupSchema.optional(),
8212
8228
  /**
8229
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8230
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8231
+ * persisted before this field existed (`inferModelProvider` fills those).
8232
+ */
8233
+ provider: ModelProviderIdSchema.optional(),
8234
+ /**
8213
8235
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8214
8236
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8215
8237
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11867,6 +11889,27 @@ var LinkedDeviceSchema = object({
11867
11889
  features: array(string()),
11868
11890
  producesTrackedEvents: boolean().optional()
11869
11891
  });
11892
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11893
+ * The batch answer needs the tag; the single-device answer already has it
11894
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11895
+ var LinkedDevicesForDeviceSchema = object({
11896
+ deviceId: number(),
11897
+ mode: LinkedDevicesModeSchema,
11898
+ devices: array(LinkedDeviceSchema)
11899
+ });
11900
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11901
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11902
+ * object literal is exactly how the three drift apart. */
11903
+ var DeviceBindingsForDeviceSchema = object({
11904
+ deviceId: number(),
11905
+ entries: array(object({
11906
+ capName: string(),
11907
+ kind: _enum(["native", "wrapped"]),
11908
+ providerAddonId: string(),
11909
+ providerNodeId: string(),
11910
+ nativeAddonId: string()
11911
+ }))
11912
+ });
11870
11913
  var SavedDeviceRowSchema = object({
11871
11914
  /** Numeric id reserved at allocateDeviceId time. */
11872
11915
  id: number(),
@@ -12092,11 +12135,25 @@ method(object({
12092
12135
  projection: _enum(["full", "slim"]).optional(),
12093
12136
  /** Return only camera devices. Filtering server-side instead of
12094
12137
  * shipping 293 rows to find 12. */
12095
- isCamera: boolean().optional()
12138
+ isCamera: boolean().optional(),
12139
+ /**
12140
+ * Return only these device ids. For the caller that already KNOWS the
12141
+ * handful it wants and needs a field the id-bearing answer does not
12142
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12143
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12144
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12145
+ * refetches on the reconcile interval, on a phone.
12146
+ *
12147
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12148
+ * keys rather than rejecting them (verified against the live hub
12149
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12150
+ * it answers today and the caller filters as it already does.
12151
+ */
12152
+ deviceIds: array(number()).optional()
12096
12153
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12097
12154
  mode: LinkedDevicesModeSchema,
12098
12155
  devices: array(LinkedDeviceSchema)
12099
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12156
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12100
12157
  deviceId: number(),
12101
12158
  values: record(string(), unknown())
12102
12159
  }), object({ success: literal(true) }), {
@@ -12123,25 +12180,7 @@ method(object({
12123
12180
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12124
12181
  kind: "mutation",
12125
12182
  auth: "admin"
12126
- }), method(object({ deviceId: number() }), object({
12127
- deviceId: number(),
12128
- entries: array(object({
12129
- capName: string(),
12130
- kind: _enum(["native", "wrapped"]),
12131
- providerAddonId: string(),
12132
- providerNodeId: string(),
12133
- nativeAddonId: string()
12134
- }))
12135
- })), method(object({}), array(object({
12136
- deviceId: number(),
12137
- entries: array(object({
12138
- capName: string(),
12139
- kind: _enum(["native", "wrapped"]),
12140
- providerAddonId: string(),
12141
- providerNodeId: string(),
12142
- nativeAddonId: string()
12143
- }))
12144
- }))), method(object({
12183
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12145
12184
  deviceId: number(),
12146
12185
  capName: string(),
12147
12186
  wrapperAddonId: string(),
@@ -14513,12 +14552,15 @@ var NcOccupancyConditionSchema = object({
14513
14552
  * there is no second switch that can disagree with the first and every rule
14514
14553
  * authored before the decision migrates for free (`audioModeOf`):
14515
14554
  *
14516
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14517
- * classifier labels with one of them. No window, no percentage:
14518
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14519
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14520
- * the analyzer's (`classificationMinScore`, per device) a label only
14521
- * reaches this condition if the classifier was already confident enough.
14555
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14556
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14557
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14558
+ * frames is the wrong question for a classifier that labels 1–3 frames
14559
+ * per episode. The count window is the brake that drops a single-frame
14560
+ * false positive; the rule's own `throttle` cooldown is the other. The
14561
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14562
+ * per device) — a label only reaches this condition if the classifier was
14563
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14522
14564
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14523
14565
  * the condition: at least `hitPercent`% of the samples over
14524
14566
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14545,14 +14587,22 @@ var NcOccupancyConditionSchema = object({
14545
14587
  * an operator who typed `dog` mean the same thing.
14546
14588
  */
14547
14589
  var NcAudioConditionSchema = object({
14548
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14590
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14549
14591
  labels: array(string().min(1)).min(1).optional(),
14550
14592
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14551
14593
  dbThreshold: number().min(-96).max(0).optional(),
14552
14594
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14553
14595
  hitPercent: number().int().min(1).max(100).default(60),
14554
14596
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14555
- samplingSeconds: number().int().min(1).max(300).default(10)
14597
+ samplingSeconds: number().int().min(1).max(300).default(10),
14598
+ /**
14599
+ * LABEL MODE: how many labelled frames must land inside
14600
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14601
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14602
+ */
14603
+ confirmHits: number().int().min(1).max(20).optional(),
14604
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14605
+ confirmWindowSec: number().int().min(1).max(60).optional()
14556
14606
  });
14557
14607
  /**
14558
14608
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16926,6 +16976,46 @@ var RecentTracksPageSchema = object({
16926
16976
  /** Cursor for the next page, or null when this page is the last. */
16927
16977
  nextCursor: string().nullable()
16928
16978
  });
16979
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16980
+ var LIST_GROUPS_MAX_LIMIT = 100;
16981
+ var AnalyticsGroupRecordSchema = object({
16982
+ id: string(),
16983
+ deviceId: number().int(),
16984
+ openedAt: number().int(),
16985
+ closedAt: number().int(),
16986
+ timestamp: number().int(),
16987
+ memberCount: number().int(),
16988
+ memberTrackIds: array(string()).readonly(),
16989
+ className: string(),
16990
+ classes: array(string()).readonly(),
16991
+ /** Relative event-media path, or null when the group has no picture yet. */
16992
+ mediaUrl: string().nullable(),
16993
+ singleton: boolean()
16994
+ });
16995
+ var AnalyticsGroupMemberSchema = object({
16996
+ trackId: string(),
16997
+ deviceId: number().int(),
16998
+ className: string(),
16999
+ firstSeen: number().int(),
17000
+ lastSeen: number().int(),
17001
+ mediaUrl: string().nullable()
17002
+ });
17003
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17004
+ var ListGroupsQueryInput = object({
17005
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17006
+ deviceIds: array(number()),
17007
+ /** Window lower bound on `closedAt` (inclusive). */
17008
+ since: number().optional(),
17009
+ /** Window upper bound on `openedAt` (inclusive). */
17010
+ until: number().optional(),
17011
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17012
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17013
+ cursor: string().optional()
17014
+ });
17015
+ var ListGroupsPageSchema = object({
17016
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17017
+ nextCursor: string().nullable()
17018
+ });
16929
17019
  var KeyEventQueryInput = object({
16930
17020
  deviceId: number(),
16931
17021
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17001,7 +17091,9 @@ var TrackCascadeCountsSchema = object({
17001
17091
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17002
17092
  plates: number().int(),
17003
17093
  /** Per-track CLIP search vectors removed (best-effort). */
17004
- embeddings: number().int()
17094
+ embeddings: number().int(),
17095
+ /** Group membership + group rows removed with their last member (best-effort). */
17096
+ groups: number().int()
17005
17097
  });
17006
17098
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17007
17099
  var DiskReconcileCountsSchema = object({
@@ -17147,7 +17239,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17147
17239
  * stationary registry). Default false: the timeline lists passages,
17148
17240
  * not parking records (operator decision, 2026-08-15). */
17149
17241
  includeStationary: boolean().optional()
17150
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17242
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17243
+ deviceId: number(),
17244
+ groupId: string().min(1)
17245
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17151
17246
  kind: "mutation",
17152
17247
  auth: "admin"
17153
17248
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17457,7 +17552,8 @@ var PipelineModelOptionSchema = object({
17457
17552
  sizeMB: number()
17458
17553
  })),
17459
17554
  group: ModelVariantGroupSchema.optional(),
17460
- legacy: boolean().optional()
17555
+ legacy: boolean().optional(),
17556
+ provider: ModelProviderIdSchema.optional()
17461
17557
  });
17462
17558
  var ConfigFieldBridge = custom();
17463
17559
  var PipelineAddonSchemaSchema = object({
@@ -24041,7 +24137,12 @@ var PlateInfoSchema = object({
24041
24137
  plateBbox: BoundingBoxSchema.optional(),
24042
24138
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24043
24139
  keyFrameMediaKey: string().optional(),
24044
- base64: string().optional()
24140
+ base64: string().optional(),
24141
+ /**
24142
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24143
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24144
+ */
24145
+ cropUrl: string().optional()
24045
24146
  });
24046
24147
  var MediaFileLiteSchema = object({
24047
24148
  key: string(),
@@ -27681,6 +27782,12 @@ Object.freeze({
27681
27782
  addonId: null,
27682
27783
  access: "view"
27683
27784
  },
27785
+ "deviceManager.getBindingsBatch": {
27786
+ capName: "device-manager",
27787
+ capScope: "system",
27788
+ addonId: null,
27789
+ access: "view"
27790
+ },
27684
27791
  "deviceManager.getChildren": {
27685
27792
  capName: "device-manager",
27686
27793
  capScope: "system",
@@ -27741,6 +27848,12 @@ Object.freeze({
27741
27848
  addonId: null,
27742
27849
  access: "view"
27743
27850
  },
27851
+ "deviceManager.getLinkedDevicesBatch": {
27852
+ capName: "device-manager",
27853
+ capScope: "system",
27854
+ addonId: null,
27855
+ access: "view"
27856
+ },
27744
27857
  "deviceManager.getRoleDisplayDefaults": {
27745
27858
  capName: "device-manager",
27746
27859
  capScope: "system",
@@ -29511,6 +29624,12 @@ Object.freeze({
29511
29624
  addonId: null,
29512
29625
  access: "view"
29513
29626
  },
29627
+ "pipelineAnalytics.getGroup": {
29628
+ capName: "pipeline-analytics",
29629
+ capScope: "device",
29630
+ addonId: null,
29631
+ access: "view"
29632
+ },
29514
29633
  "pipelineAnalytics.getKeyEvents": {
29515
29634
  capName: "pipeline-analytics",
29516
29635
  capScope: "device",
@@ -29595,6 +29714,12 @@ Object.freeze({
29595
29714
  addonId: null,
29596
29715
  access: "view"
29597
29716
  },
29717
+ "pipelineAnalytics.listGroups": {
29718
+ capName: "pipeline-analytics",
29719
+ capScope: "device",
29720
+ addonId: null,
29721
+ access: "view"
29722
+ },
29598
29723
  "pipelineAnalytics.listOpsLog": {
29599
29724
  capName: "pipeline-analytics",
29600
29725
  capScope: "device",
@@ -32376,6 +32501,11 @@ Object.freeze({
32376
32501
  form: "single",
32377
32502
  optional: false
32378
32503
  }],
32504
+ "deviceManager.getBindingsBatch": [{
32505
+ name: "deviceIds",
32506
+ form: "array",
32507
+ optional: false
32508
+ }],
32379
32509
  "deviceManager.getChildren": [{
32380
32510
  name: "parentDeviceId",
32381
32511
  form: "single",
@@ -32421,6 +32551,11 @@ Object.freeze({
32421
32551
  form: "single",
32422
32552
  optional: false
32423
32553
  }],
32554
+ "deviceManager.getLinkedDevicesBatch": [{
32555
+ name: "deviceIds",
32556
+ form: "array",
32557
+ optional: false
32558
+ }],
32424
32559
  "deviceManager.getSettingsSchema": [{
32425
32560
  name: "deviceId",
32426
32561
  form: "single",
@@ -32441,6 +32576,11 @@ Object.freeze({
32441
32576
  form: "single",
32442
32577
  optional: false
32443
32578
  }],
32579
+ "deviceManager.listAll": [{
32580
+ name: "deviceIds",
32581
+ form: "array",
32582
+ optional: true
32583
+ }],
32444
32584
  "deviceManager.loadConfig": [{
32445
32585
  name: "deviceId",
32446
32586
  form: "single",
@@ -33014,6 +33154,11 @@ Object.freeze({
33014
33154
  form: "single",
33015
33155
  optional: false
33016
33156
  }],
33157
+ "pipelineAnalytics.getGroup": [{
33158
+ name: "deviceId",
33159
+ form: "single",
33160
+ optional: false
33161
+ }],
33017
33162
  "pipelineAnalytics.getKeyEvents": [{
33018
33163
  name: "deviceId",
33019
33164
  form: "single",
@@ -33069,6 +33214,11 @@ Object.freeze({
33069
33214
  form: "array",
33070
33215
  optional: false
33071
33216
  }],
33217
+ "pipelineAnalytics.listGroups": [{
33218
+ name: "deviceIds",
33219
+ form: "array",
33220
+ optional: false
33221
+ }],
33072
33222
  "pipelineAnalytics.listOpsLog": [{
33073
33223
  name: "deviceId",
33074
33224
  form: "single",
package/dist/index.mjs CHANGED
@@ -5802,6 +5802,13 @@ var BaseAddon = class {
5802
5802
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5803
5803
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5804
5804
  _registeredCapNames = [];
5805
+ /**
5806
+ * True only after `readAddonStore` actually answered. Constructor
5807
+ * defaults look like stored config when the store is down — a forked
5808
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5809
+ * mode, 2026-08-25) is not "the operator chose this".
5810
+ */
5811
+ settingsStoreReady = false;
5805
5812
  /** Default config values. Provided via constructor. */
5806
5813
  defaults;
5807
5814
  constructor(defaults) {
@@ -6202,7 +6209,9 @@ var BaseAddon = class {
6202
6209
  ];
6203
6210
  let lastErr;
6204
6211
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6205
- return await settings.readAddonStore() ?? {};
6212
+ const stored = await settings.readAddonStore() ?? {};
6213
+ this.settingsStoreReady = true;
6214
+ return stored;
6206
6215
  } catch (err) {
6207
6216
  lastErr = err;
6208
6217
  const msg = err instanceof Error ? err.message : String(err);
@@ -6210,6 +6219,7 @@ var BaseAddon = class {
6210
6219
  if (attempt === delaysMs.length) break;
6211
6220
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6212
6221
  }
6222
+ this.settingsStoreReady = false;
6213
6223
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6214
6224
  return {};
6215
6225
  }
@@ -8109,6 +8119,12 @@ var ModelVariantGroupSchema = object({
8109
8119
  */
8110
8120
  resolution: number().int().positive().optional()
8111
8121
  });
8122
+ var ModelProviderIdSchema = _enum([
8123
+ "camstack",
8124
+ "frigate",
8125
+ "scrypted",
8126
+ "custom"
8127
+ ]);
8112
8128
  var ModelCatalogEntrySchema = object({
8113
8129
  id: string(),
8114
8130
  name: string(),
@@ -8206,6 +8222,12 @@ var ModelCatalogEntrySchema = object({
8206
8222
  */
8207
8223
  group: ModelVariantGroupSchema.optional(),
8208
8224
  /**
8225
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8226
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8227
+ * persisted before this field existed (`inferModelProvider` fills those).
8228
+ */
8229
+ provider: ModelProviderIdSchema.optional(),
8230
+ /**
8209
8231
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8210
8232
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8211
8233
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11863,6 +11885,27 @@ var LinkedDeviceSchema = object({
11863
11885
  features: array(string()),
11864
11886
  producesTrackedEvents: boolean().optional()
11865
11887
  });
11888
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11889
+ * The batch answer needs the tag; the single-device answer already has it
11890
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11891
+ var LinkedDevicesForDeviceSchema = object({
11892
+ deviceId: number(),
11893
+ mode: LinkedDevicesModeSchema,
11894
+ devices: array(LinkedDeviceSchema)
11895
+ });
11896
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11897
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11898
+ * object literal is exactly how the three drift apart. */
11899
+ var DeviceBindingsForDeviceSchema = object({
11900
+ deviceId: number(),
11901
+ entries: array(object({
11902
+ capName: string(),
11903
+ kind: _enum(["native", "wrapped"]),
11904
+ providerAddonId: string(),
11905
+ providerNodeId: string(),
11906
+ nativeAddonId: string()
11907
+ }))
11908
+ });
11866
11909
  var SavedDeviceRowSchema = object({
11867
11910
  /** Numeric id reserved at allocateDeviceId time. */
11868
11911
  id: number(),
@@ -12088,11 +12131,25 @@ method(object({
12088
12131
  projection: _enum(["full", "slim"]).optional(),
12089
12132
  /** Return only camera devices. Filtering server-side instead of
12090
12133
  * shipping 293 rows to find 12. */
12091
- isCamera: boolean().optional()
12134
+ isCamera: boolean().optional(),
12135
+ /**
12136
+ * Return only these device ids. For the caller that already KNOWS the
12137
+ * handful it wants and needs a field the id-bearing answer does not
12138
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12139
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12140
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12141
+ * refetches on the reconcile interval, on a phone.
12142
+ *
12143
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12144
+ * keys rather than rejecting them (verified against the live hub
12145
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12146
+ * it answers today and the caller filters as it already does.
12147
+ */
12148
+ deviceIds: array(number()).optional()
12092
12149
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12093
12150
  mode: LinkedDevicesModeSchema,
12094
12151
  devices: array(LinkedDeviceSchema)
12095
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12152
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12096
12153
  deviceId: number(),
12097
12154
  values: record(string(), unknown())
12098
12155
  }), object({ success: literal(true) }), {
@@ -12119,25 +12176,7 @@ method(object({
12119
12176
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12120
12177
  kind: "mutation",
12121
12178
  auth: "admin"
12122
- }), method(object({ deviceId: number() }), object({
12123
- deviceId: number(),
12124
- entries: array(object({
12125
- capName: string(),
12126
- kind: _enum(["native", "wrapped"]),
12127
- providerAddonId: string(),
12128
- providerNodeId: string(),
12129
- nativeAddonId: string()
12130
- }))
12131
- })), method(object({}), array(object({
12132
- deviceId: number(),
12133
- entries: array(object({
12134
- capName: string(),
12135
- kind: _enum(["native", "wrapped"]),
12136
- providerAddonId: string(),
12137
- providerNodeId: string(),
12138
- nativeAddonId: string()
12139
- }))
12140
- }))), method(object({
12179
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12141
12180
  deviceId: number(),
12142
12181
  capName: string(),
12143
12182
  wrapperAddonId: string(),
@@ -14509,12 +14548,15 @@ var NcOccupancyConditionSchema = object({
14509
14548
  * there is no second switch that can disagree with the first and every rule
14510
14549
  * authored before the decision migrates for free (`audioModeOf`):
14511
14550
  *
14512
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14513
- * classifier labels with one of them. No window, no percentage:
14514
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14515
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14516
- * the analyzer's (`classificationMinScore`, per device) a label only
14517
- * reaches this condition if the classifier was already confident enough.
14551
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14552
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14553
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14554
+ * frames is the wrong question for a classifier that labels 1–3 frames
14555
+ * per episode. The count window is the brake that drops a single-frame
14556
+ * false positive; the rule's own `throttle` cooldown is the other. The
14557
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14558
+ * per device) — a label only reaches this condition if the classifier was
14559
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14518
14560
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14519
14561
  * the condition: at least `hitPercent`% of the samples over
14520
14562
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14541,14 +14583,22 @@ var NcOccupancyConditionSchema = object({
14541
14583
  * an operator who typed `dog` mean the same thing.
14542
14584
  */
14543
14585
  var NcAudioConditionSchema = object({
14544
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14586
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14545
14587
  labels: array(string().min(1)).min(1).optional(),
14546
14588
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14547
14589
  dbThreshold: number().min(-96).max(0).optional(),
14548
14590
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14549
14591
  hitPercent: number().int().min(1).max(100).default(60),
14550
14592
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14551
- samplingSeconds: number().int().min(1).max(300).default(10)
14593
+ samplingSeconds: number().int().min(1).max(300).default(10),
14594
+ /**
14595
+ * LABEL MODE: how many labelled frames must land inside
14596
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14597
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14598
+ */
14599
+ confirmHits: number().int().min(1).max(20).optional(),
14600
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14601
+ confirmWindowSec: number().int().min(1).max(60).optional()
14552
14602
  });
14553
14603
  /**
14554
14604
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16922,6 +16972,46 @@ var RecentTracksPageSchema = object({
16922
16972
  /** Cursor for the next page, or null when this page is the last. */
16923
16973
  nextCursor: string().nullable()
16924
16974
  });
16975
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16976
+ var LIST_GROUPS_MAX_LIMIT = 100;
16977
+ var AnalyticsGroupRecordSchema = object({
16978
+ id: string(),
16979
+ deviceId: number().int(),
16980
+ openedAt: number().int(),
16981
+ closedAt: number().int(),
16982
+ timestamp: number().int(),
16983
+ memberCount: number().int(),
16984
+ memberTrackIds: array(string()).readonly(),
16985
+ className: string(),
16986
+ classes: array(string()).readonly(),
16987
+ /** Relative event-media path, or null when the group has no picture yet. */
16988
+ mediaUrl: string().nullable(),
16989
+ singleton: boolean()
16990
+ });
16991
+ var AnalyticsGroupMemberSchema = object({
16992
+ trackId: string(),
16993
+ deviceId: number().int(),
16994
+ className: string(),
16995
+ firstSeen: number().int(),
16996
+ lastSeen: number().int(),
16997
+ mediaUrl: string().nullable()
16998
+ });
16999
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17000
+ var ListGroupsQueryInput = object({
17001
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17002
+ deviceIds: array(number()),
17003
+ /** Window lower bound on `closedAt` (inclusive). */
17004
+ since: number().optional(),
17005
+ /** Window upper bound on `openedAt` (inclusive). */
17006
+ until: number().optional(),
17007
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17008
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17009
+ cursor: string().optional()
17010
+ });
17011
+ var ListGroupsPageSchema = object({
17012
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17013
+ nextCursor: string().nullable()
17014
+ });
16925
17015
  var KeyEventQueryInput = object({
16926
17016
  deviceId: number(),
16927
17017
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16997,7 +17087,9 @@ var TrackCascadeCountsSchema = object({
16997
17087
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16998
17088
  plates: number().int(),
16999
17089
  /** Per-track CLIP search vectors removed (best-effort). */
17000
- embeddings: number().int()
17090
+ embeddings: number().int(),
17091
+ /** Group membership + group rows removed with their last member (best-effort). */
17092
+ groups: number().int()
17001
17093
  });
17002
17094
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17003
17095
  var DiskReconcileCountsSchema = object({
@@ -17143,7 +17235,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17143
17235
  * stationary registry). Default false: the timeline lists passages,
17144
17236
  * not parking records (operator decision, 2026-08-15). */
17145
17237
  includeStationary: boolean().optional()
17146
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17238
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17239
+ deviceId: number(),
17240
+ groupId: string().min(1)
17241
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17147
17242
  kind: "mutation",
17148
17243
  auth: "admin"
17149
17244
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17453,7 +17548,8 @@ var PipelineModelOptionSchema = object({
17453
17548
  sizeMB: number()
17454
17549
  })),
17455
17550
  group: ModelVariantGroupSchema.optional(),
17456
- legacy: boolean().optional()
17551
+ legacy: boolean().optional(),
17552
+ provider: ModelProviderIdSchema.optional()
17457
17553
  });
17458
17554
  var ConfigFieldBridge = custom();
17459
17555
  var PipelineAddonSchemaSchema = object({
@@ -24037,7 +24133,12 @@ var PlateInfoSchema = object({
24037
24133
  plateBbox: BoundingBoxSchema.optional(),
24038
24134
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24039
24135
  keyFrameMediaKey: string().optional(),
24040
- base64: string().optional()
24136
+ base64: string().optional(),
24137
+ /**
24138
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24139
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24140
+ */
24141
+ cropUrl: string().optional()
24041
24142
  });
24042
24143
  var MediaFileLiteSchema = object({
24043
24144
  key: string(),
@@ -27677,6 +27778,12 @@ Object.freeze({
27677
27778
  addonId: null,
27678
27779
  access: "view"
27679
27780
  },
27781
+ "deviceManager.getBindingsBatch": {
27782
+ capName: "device-manager",
27783
+ capScope: "system",
27784
+ addonId: null,
27785
+ access: "view"
27786
+ },
27680
27787
  "deviceManager.getChildren": {
27681
27788
  capName: "device-manager",
27682
27789
  capScope: "system",
@@ -27737,6 +27844,12 @@ Object.freeze({
27737
27844
  addonId: null,
27738
27845
  access: "view"
27739
27846
  },
27847
+ "deviceManager.getLinkedDevicesBatch": {
27848
+ capName: "device-manager",
27849
+ capScope: "system",
27850
+ addonId: null,
27851
+ access: "view"
27852
+ },
27740
27853
  "deviceManager.getRoleDisplayDefaults": {
27741
27854
  capName: "device-manager",
27742
27855
  capScope: "system",
@@ -29507,6 +29620,12 @@ Object.freeze({
29507
29620
  addonId: null,
29508
29621
  access: "view"
29509
29622
  },
29623
+ "pipelineAnalytics.getGroup": {
29624
+ capName: "pipeline-analytics",
29625
+ capScope: "device",
29626
+ addonId: null,
29627
+ access: "view"
29628
+ },
29510
29629
  "pipelineAnalytics.getKeyEvents": {
29511
29630
  capName: "pipeline-analytics",
29512
29631
  capScope: "device",
@@ -29591,6 +29710,12 @@ Object.freeze({
29591
29710
  addonId: null,
29592
29711
  access: "view"
29593
29712
  },
29713
+ "pipelineAnalytics.listGroups": {
29714
+ capName: "pipeline-analytics",
29715
+ capScope: "device",
29716
+ addonId: null,
29717
+ access: "view"
29718
+ },
29594
29719
  "pipelineAnalytics.listOpsLog": {
29595
29720
  capName: "pipeline-analytics",
29596
29721
  capScope: "device",
@@ -32372,6 +32497,11 @@ Object.freeze({
32372
32497
  form: "single",
32373
32498
  optional: false
32374
32499
  }],
32500
+ "deviceManager.getBindingsBatch": [{
32501
+ name: "deviceIds",
32502
+ form: "array",
32503
+ optional: false
32504
+ }],
32375
32505
  "deviceManager.getChildren": [{
32376
32506
  name: "parentDeviceId",
32377
32507
  form: "single",
@@ -32417,6 +32547,11 @@ Object.freeze({
32417
32547
  form: "single",
32418
32548
  optional: false
32419
32549
  }],
32550
+ "deviceManager.getLinkedDevicesBatch": [{
32551
+ name: "deviceIds",
32552
+ form: "array",
32553
+ optional: false
32554
+ }],
32420
32555
  "deviceManager.getSettingsSchema": [{
32421
32556
  name: "deviceId",
32422
32557
  form: "single",
@@ -32437,6 +32572,11 @@ Object.freeze({
32437
32572
  form: "single",
32438
32573
  optional: false
32439
32574
  }],
32575
+ "deviceManager.listAll": [{
32576
+ name: "deviceIds",
32577
+ form: "array",
32578
+ optional: true
32579
+ }],
32440
32580
  "deviceManager.loadConfig": [{
32441
32581
  name: "deviceId",
32442
32582
  form: "single",
@@ -33010,6 +33150,11 @@ Object.freeze({
33010
33150
  form: "single",
33011
33151
  optional: false
33012
33152
  }],
33153
+ "pipelineAnalytics.getGroup": [{
33154
+ name: "deviceId",
33155
+ form: "single",
33156
+ optional: false
33157
+ }],
33013
33158
  "pipelineAnalytics.getKeyEvents": [{
33014
33159
  name: "deviceId",
33015
33160
  form: "single",
@@ -33065,6 +33210,11 @@ Object.freeze({
33065
33210
  form: "array",
33066
33211
  optional: false
33067
33212
  }],
33213
+ "pipelineAnalytics.listGroups": [{
33214
+ name: "deviceIds",
33215
+ form: "array",
33216
+ optional: false
33217
+ }],
33068
33218
  "pipelineAnalytics.listOpsLog": [{
33069
33219
  name: "deviceId",
33070
33220
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-ffmpeg",
3
- "version": "1.2.26",
3
+ "version": "1.2.27",
4
4
  "description": "Standalone ffmpeg-subprocess decoder fallback addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",