@camstack/addon-decoder-ffmpeg 1.2.51 → 1.2.53

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.
package/dist/index.js CHANGED
@@ -7719,7 +7719,8 @@ var OpsLogReasonSchema = _enum([
7719
7719
  "quota",
7720
7720
  "manual",
7721
7721
  "operator",
7722
- "maintenance"
7722
+ "maintenance",
7723
+ "orphaned-device"
7723
7724
  ]);
7724
7725
  /** One audit row, shared verbatim by both domains. */
7725
7726
  var OpsLogEntrySchema = object({
@@ -12880,10 +12881,39 @@ var DevicePersistConfigPayloadSchema = object({
12880
12881
  deviceId: number(),
12881
12882
  data: record(string(), unknown())
12882
12883
  });
12884
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12885
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12886
+ var MigrateSwitchOutcomeSchema = _enum([
12887
+ "off",
12888
+ "on",
12889
+ "not-offered",
12890
+ "unreachable"
12891
+ ]);
12892
+ var MigrateSwitchReportSchema = object({
12893
+ deviceId: number(),
12894
+ switchId: CameraSwitchIdSchema,
12895
+ outcome: MigrateSwitchOutcomeSchema,
12896
+ detail: string().optional()
12897
+ });
12898
+ var MigrateDeviceResultSchema = object({
12899
+ sourceId: number(),
12900
+ targetId: number(),
12901
+ switches: array(MigrateSwitchReportSchema).readonly(),
12902
+ /** Switches that could NOT be turned off on the source. Empty is the only
12903
+ * value meaning the replaced hardware is quiet. */
12904
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12905
+ swapped: boolean()
12906
+ });
12883
12907
  method(object({
12884
12908
  addonId: string(),
12885
12909
  stableId: string()
12886
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12910
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12911
+ sourceId: number(),
12912
+ targetId: number()
12913
+ }), MigrateDeviceResultSchema, {
12914
+ kind: "mutation",
12915
+ auth: "admin"
12916
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12887
12917
  deviceId: number(),
12888
12918
  name: string()
12889
12919
  }), _void(), {
@@ -18980,6 +19010,34 @@ var KeyEventsForDeviceSchema = object({
18980
19010
  deviceId: number(),
18981
19011
  events: array(KeyEventSchema).readonly()
18982
19012
  });
19013
+ /** One non-empty bucket of the timeline histogram. */
19014
+ var EventDensityBucketSchema = object({
19015
+ bucketStart: number(),
19016
+ motion: number().int(),
19017
+ object: number().int(),
19018
+ audio: number().int()
19019
+ });
19020
+ /**
19021
+ * One camera's row in a `getEventDensityBatch` answer.
19022
+ *
19023
+ * TWO facts, and the timeline draws them differently:
19024
+ *
19025
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19026
+ * claim: read, and nothing happened in the window.
19027
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19028
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19029
+ * not quiet.
19030
+ *
19031
+ * Fanned out per camera the difference was free — one query rejected while the
19032
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19033
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19034
+ * got eleven cannot tell which one is missing, or that any is.
19035
+ */
19036
+ var EventDensityForDeviceSchema = object({
19037
+ deviceId: number(),
19038
+ read: _enum(["read", "unreadable"]),
19039
+ buckets: array(EventDensityBucketSchema).readonly()
19040
+ });
18983
19041
  object({
18984
19042
  trackId: string(),
18985
19043
  className: string(),
@@ -19290,12 +19348,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19290
19348
  since: number(),
19291
19349
  until: number(),
19292
19350
  bucketMs: number().int().positive()
19293
- }), array(object({
19294
- bucketStart: number(),
19295
- motion: number().int(),
19296
- object: number().int(),
19297
- audio: number().int()
19298
- })).readonly()), method(object({
19351
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19352
+ deviceIds: array(number()).min(1).max(200),
19353
+ since: number(),
19354
+ until: number(),
19355
+ bucketMs: number().int().positive()
19356
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19299
19357
  deviceId: number(),
19300
19358
  cutoffMs: number()
19301
19359
  }), object({
@@ -26605,6 +26663,39 @@ var RecordingDaysSchema = object({
26605
26663
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26606
26664
  days: array(number())
26607
26665
  });
26666
+ /**
26667
+ * One camera's row in a `getAvailabilityBatch` answer.
26668
+ *
26669
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26670
+ * batch collapses the transport, not the work — plus the one thing the singular
26671
+ * method never had to say:
26672
+ *
26673
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26674
+ * no footage in the window", which is a real claim.
26675
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26676
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26677
+ * that emptiness means NOTHING.
26678
+ *
26679
+ * A timeline that renders the second as the first tells an operator there is no
26680
+ * footage when the truth is that nobody looked.
26681
+ */
26682
+ var RecordingAvailabilityForDeviceSchema = object({
26683
+ deviceId: number(),
26684
+ read: _enum(["read", "unreadable"]),
26685
+ ranges: array(RecordingRangeSchema).readonly()
26686
+ });
26687
+ /**
26688
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26689
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26690
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26691
+ * the date-picker's day dots must not spell them the same.
26692
+ */
26693
+ var RecordingDaysForDeviceSchema = object({
26694
+ deviceId: number(),
26695
+ read: _enum(["read", "unreadable"]),
26696
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26697
+ days: array(number()).readonly()
26698
+ });
26608
26699
  var RecordingManifestSchema = object({
26609
26700
  deviceId: number(),
26610
26701
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26839,6 +26930,13 @@ method(object({
26839
26930
  }), RecordingAvailabilitySchema, {
26840
26931
  kind: "query",
26841
26932
  auth: "protected"
26933
+ }), method(object({
26934
+ deviceIds: array(number()).min(1).max(200),
26935
+ fromMs: number(),
26936
+ toMs: number()
26937
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26938
+ kind: "query",
26939
+ auth: "protected"
26842
26940
  }), method(object({
26843
26941
  deviceId: number(),
26844
26942
  fromMs: number(),
@@ -26847,6 +26945,14 @@ method(object({
26847
26945
  }), RecordingDaysSchema, {
26848
26946
  kind: "query",
26849
26947
  auth: "protected"
26948
+ }), method(object({
26949
+ deviceIds: array(number()).min(1).max(200),
26950
+ fromMs: number(),
26951
+ toMs: number(),
26952
+ tzOffsetMinutes: number()
26953
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26954
+ kind: "query",
26955
+ auth: "protected"
26850
26956
  }), method(object({
26851
26957
  deviceId: number(),
26852
26958
  fromMs: number(),
@@ -30310,6 +30416,12 @@ Object.freeze({
30310
30416
  addonId: null,
30311
30417
  access: "view"
30312
30418
  },
30419
+ "deviceManager.migrateDevice": {
30420
+ capName: "device-manager",
30421
+ capScope: "system",
30422
+ addonId: null,
30423
+ access: "create"
30424
+ },
30313
30425
  "deviceManager.persistConfig": {
30314
30426
  capName: "device-manager",
30315
30427
  capScope: "system",
@@ -32044,6 +32156,12 @@ Object.freeze({
32044
32156
  addonId: null,
32045
32157
  access: "view"
32046
32158
  },
32159
+ "pipelineAnalytics.getEventDensityBatch": {
32160
+ capName: "pipeline-analytics",
32161
+ capScope: "device",
32162
+ addonId: null,
32163
+ access: "view"
32164
+ },
32047
32165
  "pipelineAnalytics.getEventMedia": {
32048
32166
  capName: "pipeline-analytics",
32049
32167
  capScope: "device",
@@ -33166,12 +33284,24 @@ Object.freeze({
33166
33284
  addonId: null,
33167
33285
  access: "view"
33168
33286
  },
33287
+ "recording.getAvailabilityBatch": {
33288
+ capName: "recording",
33289
+ capScope: "system",
33290
+ addonId: null,
33291
+ access: "view"
33292
+ },
33169
33293
  "recording.getDaysWithRecordings": {
33170
33294
  capName: "recording",
33171
33295
  capScope: "system",
33172
33296
  addonId: null,
33173
33297
  access: "view"
33174
33298
  },
33299
+ "recording.getDaysWithRecordingsBatch": {
33300
+ capName: "recording",
33301
+ capScope: "system",
33302
+ addonId: null,
33303
+ access: "view"
33304
+ },
33175
33305
  "recording.getDeviceConfig": {
33176
33306
  capName: "recording",
33177
33307
  capScope: "system",
@@ -35776,6 +35906,11 @@ Object.freeze({
35776
35906
  form: "single",
35777
35907
  optional: false
35778
35908
  }],
35909
+ "pipelineAnalytics.getEventDensityBatch": [{
35910
+ name: "deviceIds",
35911
+ form: "array",
35912
+ optional: false
35913
+ }],
35779
35914
  "pipelineAnalytics.getEventMedia": [{
35780
35915
  name: "deviceId",
35781
35916
  form: "single",
@@ -36191,11 +36326,21 @@ Object.freeze({
36191
36326
  form: "single",
36192
36327
  optional: false
36193
36328
  }],
36329
+ "recording.getAvailabilityBatch": [{
36330
+ name: "deviceIds",
36331
+ form: "array",
36332
+ optional: false
36333
+ }],
36194
36334
  "recording.getDaysWithRecordings": [{
36195
36335
  name: "deviceId",
36196
36336
  form: "single",
36197
36337
  optional: false
36198
36338
  }],
36339
+ "recording.getDaysWithRecordingsBatch": [{
36340
+ name: "deviceIds",
36341
+ form: "array",
36342
+ optional: false
36343
+ }],
36199
36344
  "recording.getDeviceConfig": [{
36200
36345
  name: "deviceId",
36201
36346
  form: "single",
package/dist/index.mjs CHANGED
@@ -7715,7 +7715,8 @@ var OpsLogReasonSchema = _enum([
7715
7715
  "quota",
7716
7716
  "manual",
7717
7717
  "operator",
7718
- "maintenance"
7718
+ "maintenance",
7719
+ "orphaned-device"
7719
7720
  ]);
7720
7721
  /** One audit row, shared verbatim by both domains. */
7721
7722
  var OpsLogEntrySchema = object({
@@ -12876,10 +12877,39 @@ var DevicePersistConfigPayloadSchema = object({
12876
12877
  deviceId: number(),
12877
12878
  data: record(string(), unknown())
12878
12879
  });
12880
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12881
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12882
+ var MigrateSwitchOutcomeSchema = _enum([
12883
+ "off",
12884
+ "on",
12885
+ "not-offered",
12886
+ "unreachable"
12887
+ ]);
12888
+ var MigrateSwitchReportSchema = object({
12889
+ deviceId: number(),
12890
+ switchId: CameraSwitchIdSchema,
12891
+ outcome: MigrateSwitchOutcomeSchema,
12892
+ detail: string().optional()
12893
+ });
12894
+ var MigrateDeviceResultSchema = object({
12895
+ sourceId: number(),
12896
+ targetId: number(),
12897
+ switches: array(MigrateSwitchReportSchema).readonly(),
12898
+ /** Switches that could NOT be turned off on the source. Empty is the only
12899
+ * value meaning the replaced hardware is quiet. */
12900
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12901
+ swapped: boolean()
12902
+ });
12879
12903
  method(object({
12880
12904
  addonId: string(),
12881
12905
  stableId: string()
12882
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12906
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12907
+ sourceId: number(),
12908
+ targetId: number()
12909
+ }), MigrateDeviceResultSchema, {
12910
+ kind: "mutation",
12911
+ auth: "admin"
12912
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12883
12913
  deviceId: number(),
12884
12914
  name: string()
12885
12915
  }), _void(), {
@@ -18976,6 +19006,34 @@ var KeyEventsForDeviceSchema = object({
18976
19006
  deviceId: number(),
18977
19007
  events: array(KeyEventSchema).readonly()
18978
19008
  });
19009
+ /** One non-empty bucket of the timeline histogram. */
19010
+ var EventDensityBucketSchema = object({
19011
+ bucketStart: number(),
19012
+ motion: number().int(),
19013
+ object: number().int(),
19014
+ audio: number().int()
19015
+ });
19016
+ /**
19017
+ * One camera's row in a `getEventDensityBatch` answer.
19018
+ *
19019
+ * TWO facts, and the timeline draws them differently:
19020
+ *
19021
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19022
+ * claim: read, and nothing happened in the window.
19023
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19024
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19025
+ * not quiet.
19026
+ *
19027
+ * Fanned out per camera the difference was free — one query rejected while the
19028
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19029
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19030
+ * got eleven cannot tell which one is missing, or that any is.
19031
+ */
19032
+ var EventDensityForDeviceSchema = object({
19033
+ deviceId: number(),
19034
+ read: _enum(["read", "unreadable"]),
19035
+ buckets: array(EventDensityBucketSchema).readonly()
19036
+ });
18979
19037
  object({
18980
19038
  trackId: string(),
18981
19039
  className: string(),
@@ -19286,12 +19344,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19286
19344
  since: number(),
19287
19345
  until: number(),
19288
19346
  bucketMs: number().int().positive()
19289
- }), array(object({
19290
- bucketStart: number(),
19291
- motion: number().int(),
19292
- object: number().int(),
19293
- audio: number().int()
19294
- })).readonly()), method(object({
19347
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19348
+ deviceIds: array(number()).min(1).max(200),
19349
+ since: number(),
19350
+ until: number(),
19351
+ bucketMs: number().int().positive()
19352
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19295
19353
  deviceId: number(),
19296
19354
  cutoffMs: number()
19297
19355
  }), object({
@@ -26601,6 +26659,39 @@ var RecordingDaysSchema = object({
26601
26659
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26602
26660
  days: array(number())
26603
26661
  });
26662
+ /**
26663
+ * One camera's row in a `getAvailabilityBatch` answer.
26664
+ *
26665
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26666
+ * batch collapses the transport, not the work — plus the one thing the singular
26667
+ * method never had to say:
26668
+ *
26669
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26670
+ * no footage in the window", which is a real claim.
26671
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26672
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26673
+ * that emptiness means NOTHING.
26674
+ *
26675
+ * A timeline that renders the second as the first tells an operator there is no
26676
+ * footage when the truth is that nobody looked.
26677
+ */
26678
+ var RecordingAvailabilityForDeviceSchema = object({
26679
+ deviceId: number(),
26680
+ read: _enum(["read", "unreadable"]),
26681
+ ranges: array(RecordingRangeSchema).readonly()
26682
+ });
26683
+ /**
26684
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26685
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26686
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26687
+ * the date-picker's day dots must not spell them the same.
26688
+ */
26689
+ var RecordingDaysForDeviceSchema = object({
26690
+ deviceId: number(),
26691
+ read: _enum(["read", "unreadable"]),
26692
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26693
+ days: array(number()).readonly()
26694
+ });
26604
26695
  var RecordingManifestSchema = object({
26605
26696
  deviceId: number(),
26606
26697
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26835,6 +26926,13 @@ method(object({
26835
26926
  }), RecordingAvailabilitySchema, {
26836
26927
  kind: "query",
26837
26928
  auth: "protected"
26929
+ }), method(object({
26930
+ deviceIds: array(number()).min(1).max(200),
26931
+ fromMs: number(),
26932
+ toMs: number()
26933
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26934
+ kind: "query",
26935
+ auth: "protected"
26838
26936
  }), method(object({
26839
26937
  deviceId: number(),
26840
26938
  fromMs: number(),
@@ -26843,6 +26941,14 @@ method(object({
26843
26941
  }), RecordingDaysSchema, {
26844
26942
  kind: "query",
26845
26943
  auth: "protected"
26944
+ }), method(object({
26945
+ deviceIds: array(number()).min(1).max(200),
26946
+ fromMs: number(),
26947
+ toMs: number(),
26948
+ tzOffsetMinutes: number()
26949
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26950
+ kind: "query",
26951
+ auth: "protected"
26846
26952
  }), method(object({
26847
26953
  deviceId: number(),
26848
26954
  fromMs: number(),
@@ -30306,6 +30412,12 @@ Object.freeze({
30306
30412
  addonId: null,
30307
30413
  access: "view"
30308
30414
  },
30415
+ "deviceManager.migrateDevice": {
30416
+ capName: "device-manager",
30417
+ capScope: "system",
30418
+ addonId: null,
30419
+ access: "create"
30420
+ },
30309
30421
  "deviceManager.persistConfig": {
30310
30422
  capName: "device-manager",
30311
30423
  capScope: "system",
@@ -32040,6 +32152,12 @@ Object.freeze({
32040
32152
  addonId: null,
32041
32153
  access: "view"
32042
32154
  },
32155
+ "pipelineAnalytics.getEventDensityBatch": {
32156
+ capName: "pipeline-analytics",
32157
+ capScope: "device",
32158
+ addonId: null,
32159
+ access: "view"
32160
+ },
32043
32161
  "pipelineAnalytics.getEventMedia": {
32044
32162
  capName: "pipeline-analytics",
32045
32163
  capScope: "device",
@@ -33162,12 +33280,24 @@ Object.freeze({
33162
33280
  addonId: null,
33163
33281
  access: "view"
33164
33282
  },
33283
+ "recording.getAvailabilityBatch": {
33284
+ capName: "recording",
33285
+ capScope: "system",
33286
+ addonId: null,
33287
+ access: "view"
33288
+ },
33165
33289
  "recording.getDaysWithRecordings": {
33166
33290
  capName: "recording",
33167
33291
  capScope: "system",
33168
33292
  addonId: null,
33169
33293
  access: "view"
33170
33294
  },
33295
+ "recording.getDaysWithRecordingsBatch": {
33296
+ capName: "recording",
33297
+ capScope: "system",
33298
+ addonId: null,
33299
+ access: "view"
33300
+ },
33171
33301
  "recording.getDeviceConfig": {
33172
33302
  capName: "recording",
33173
33303
  capScope: "system",
@@ -35772,6 +35902,11 @@ Object.freeze({
35772
35902
  form: "single",
35773
35903
  optional: false
35774
35904
  }],
35905
+ "pipelineAnalytics.getEventDensityBatch": [{
35906
+ name: "deviceIds",
35907
+ form: "array",
35908
+ optional: false
35909
+ }],
35775
35910
  "pipelineAnalytics.getEventMedia": [{
35776
35911
  name: "deviceId",
35777
35912
  form: "single",
@@ -36187,11 +36322,21 @@ Object.freeze({
36187
36322
  form: "single",
36188
36323
  optional: false
36189
36324
  }],
36325
+ "recording.getAvailabilityBatch": [{
36326
+ name: "deviceIds",
36327
+ form: "array",
36328
+ optional: false
36329
+ }],
36190
36330
  "recording.getDaysWithRecordings": [{
36191
36331
  name: "deviceId",
36192
36332
  form: "single",
36193
36333
  optional: false
36194
36334
  }],
36335
+ "recording.getDaysWithRecordingsBatch": [{
36336
+ name: "deviceIds",
36337
+ form: "array",
36338
+ optional: false
36339
+ }],
36195
36340
  "recording.getDeviceConfig": [{
36196
36341
  name: "deviceId",
36197
36342
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-ffmpeg",
3
- "version": "1.2.51",
3
+ "version": "1.2.53",
4
4
  "description": "Standalone ffmpeg-subprocess decoder fallback addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",