@camstack/addon-provider-amcrest 0.2.52 → 0.2.54

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/addon.js CHANGED
@@ -7711,7 +7711,8 @@ var OpsLogReasonSchema = _enum([
7711
7711
  "quota",
7712
7712
  "manual",
7713
7713
  "operator",
7714
- "maintenance"
7714
+ "maintenance",
7715
+ "orphaned-device"
7715
7716
  ]);
7716
7717
  /** One audit row, shared verbatim by both domains. */
7717
7718
  var OpsLogEntrySchema = object({
@@ -13006,10 +13007,39 @@ var DevicePersistConfigPayloadSchema = object({
13006
13007
  deviceId: number(),
13007
13008
  data: record(string(), unknown())
13008
13009
  });
13010
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13011
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13012
+ var MigrateSwitchOutcomeSchema = _enum([
13013
+ "off",
13014
+ "on",
13015
+ "not-offered",
13016
+ "unreachable"
13017
+ ]);
13018
+ var MigrateSwitchReportSchema = object({
13019
+ deviceId: number(),
13020
+ switchId: CameraSwitchIdSchema,
13021
+ outcome: MigrateSwitchOutcomeSchema,
13022
+ detail: string().optional()
13023
+ });
13024
+ var MigrateDeviceResultSchema = object({
13025
+ sourceId: number(),
13026
+ targetId: number(),
13027
+ switches: array(MigrateSwitchReportSchema).readonly(),
13028
+ /** Switches that could NOT be turned off on the source. Empty is the only
13029
+ * value meaning the replaced hardware is quiet. */
13030
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13031
+ swapped: boolean()
13032
+ });
13009
13033
  method(object({
13010
13034
  addonId: string(),
13011
13035
  stableId: string()
13012
- }), 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({
13036
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13037
+ sourceId: number(),
13038
+ targetId: number()
13039
+ }), MigrateDeviceResultSchema, {
13040
+ kind: "mutation",
13041
+ auth: "admin"
13042
+ }), 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({
13013
13043
  deviceId: number(),
13014
13044
  name: string()
13015
13045
  }), _void(), {
@@ -19184,6 +19214,34 @@ var KeyEventsForDeviceSchema = object({
19184
19214
  deviceId: number(),
19185
19215
  events: array(KeyEventSchema).readonly()
19186
19216
  });
19217
+ /** One non-empty bucket of the timeline histogram. */
19218
+ var EventDensityBucketSchema = object({
19219
+ bucketStart: number(),
19220
+ motion: number().int(),
19221
+ object: number().int(),
19222
+ audio: number().int()
19223
+ });
19224
+ /**
19225
+ * One camera's row in a `getEventDensityBatch` answer.
19226
+ *
19227
+ * TWO facts, and the timeline draws them differently:
19228
+ *
19229
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19230
+ * claim: read, and nothing happened in the window.
19231
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19232
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19233
+ * not quiet.
19234
+ *
19235
+ * Fanned out per camera the difference was free — one query rejected while the
19236
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19237
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19238
+ * got eleven cannot tell which one is missing, or that any is.
19239
+ */
19240
+ var EventDensityForDeviceSchema = object({
19241
+ deviceId: number(),
19242
+ read: _enum(["read", "unreadable"]),
19243
+ buckets: array(EventDensityBucketSchema).readonly()
19244
+ });
19187
19245
  object({
19188
19246
  trackId: string(),
19189
19247
  className: string(),
@@ -19494,12 +19552,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19494
19552
  since: number(),
19495
19553
  until: number(),
19496
19554
  bucketMs: number().int().positive()
19497
- }), array(object({
19498
- bucketStart: number(),
19499
- motion: number().int(),
19500
- object: number().int(),
19501
- audio: number().int()
19502
- })).readonly()), method(object({
19555
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19556
+ deviceIds: array(number()).min(1).max(200),
19557
+ since: number(),
19558
+ until: number(),
19559
+ bucketMs: number().int().positive()
19560
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19503
19561
  deviceId: number(),
19504
19562
  cutoffMs: number()
19505
19563
  }), object({
@@ -28716,6 +28774,39 @@ var RecordingDaysSchema = object({
28716
28774
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28717
28775
  days: array(number())
28718
28776
  });
28777
+ /**
28778
+ * One camera's row in a `getAvailabilityBatch` answer.
28779
+ *
28780
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
28781
+ * batch collapses the transport, not the work — plus the one thing the singular
28782
+ * method never had to say:
28783
+ *
28784
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
28785
+ * no footage in the window", which is a real claim.
28786
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
28787
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
28788
+ * that emptiness means NOTHING.
28789
+ *
28790
+ * A timeline that renders the second as the first tells an operator there is no
28791
+ * footage when the truth is that nobody looked.
28792
+ */
28793
+ var RecordingAvailabilityForDeviceSchema = object({
28794
+ deviceId: number(),
28795
+ read: _enum(["read", "unreadable"]),
28796
+ ranges: array(RecordingRangeSchema).readonly()
28797
+ });
28798
+ /**
28799
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
28800
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
28801
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
28802
+ * the date-picker's day dots must not spell them the same.
28803
+ */
28804
+ var RecordingDaysForDeviceSchema = object({
28805
+ deviceId: number(),
28806
+ read: _enum(["read", "unreadable"]),
28807
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28808
+ days: array(number()).readonly()
28809
+ });
28719
28810
  var RecordingManifestSchema = object({
28720
28811
  deviceId: number(),
28721
28812
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -28950,6 +29041,13 @@ method(object({
28950
29041
  }), RecordingAvailabilitySchema, {
28951
29042
  kind: "query",
28952
29043
  auth: "protected"
29044
+ }), method(object({
29045
+ deviceIds: array(number()).min(1).max(200),
29046
+ fromMs: number(),
29047
+ toMs: number()
29048
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29049
+ kind: "query",
29050
+ auth: "protected"
28953
29051
  }), method(object({
28954
29052
  deviceId: number(),
28955
29053
  fromMs: number(),
@@ -28958,6 +29056,14 @@ method(object({
28958
29056
  }), RecordingDaysSchema, {
28959
29057
  kind: "query",
28960
29058
  auth: "protected"
29059
+ }), method(object({
29060
+ deviceIds: array(number()).min(1).max(200),
29061
+ fromMs: number(),
29062
+ toMs: number(),
29063
+ tzOffsetMinutes: number()
29064
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29065
+ kind: "query",
29066
+ auth: "protected"
28961
29067
  }), method(object({
28962
29068
  deviceId: number(),
28963
29069
  fromMs: number(),
@@ -34524,6 +34630,12 @@ Object.freeze({
34524
34630
  addonId: null,
34525
34631
  access: "view"
34526
34632
  },
34633
+ "deviceManager.migrateDevice": {
34634
+ capName: "device-manager",
34635
+ capScope: "system",
34636
+ addonId: null,
34637
+ access: "create"
34638
+ },
34527
34639
  "deviceManager.persistConfig": {
34528
34640
  capName: "device-manager",
34529
34641
  capScope: "system",
@@ -36258,6 +36370,12 @@ Object.freeze({
36258
36370
  addonId: null,
36259
36371
  access: "view"
36260
36372
  },
36373
+ "pipelineAnalytics.getEventDensityBatch": {
36374
+ capName: "pipeline-analytics",
36375
+ capScope: "device",
36376
+ addonId: null,
36377
+ access: "view"
36378
+ },
36261
36379
  "pipelineAnalytics.getEventMedia": {
36262
36380
  capName: "pipeline-analytics",
36263
36381
  capScope: "device",
@@ -37380,12 +37498,24 @@ Object.freeze({
37380
37498
  addonId: null,
37381
37499
  access: "view"
37382
37500
  },
37501
+ "recording.getAvailabilityBatch": {
37502
+ capName: "recording",
37503
+ capScope: "system",
37504
+ addonId: null,
37505
+ access: "view"
37506
+ },
37383
37507
  "recording.getDaysWithRecordings": {
37384
37508
  capName: "recording",
37385
37509
  capScope: "system",
37386
37510
  addonId: null,
37387
37511
  access: "view"
37388
37512
  },
37513
+ "recording.getDaysWithRecordingsBatch": {
37514
+ capName: "recording",
37515
+ capScope: "system",
37516
+ addonId: null,
37517
+ access: "view"
37518
+ },
37389
37519
  "recording.getDeviceConfig": {
37390
37520
  capName: "recording",
37391
37521
  capScope: "system",
@@ -39990,6 +40120,11 @@ Object.freeze({
39990
40120
  form: "single",
39991
40121
  optional: false
39992
40122
  }],
40123
+ "pipelineAnalytics.getEventDensityBatch": [{
40124
+ name: "deviceIds",
40125
+ form: "array",
40126
+ optional: false
40127
+ }],
39993
40128
  "pipelineAnalytics.getEventMedia": [{
39994
40129
  name: "deviceId",
39995
40130
  form: "single",
@@ -40405,11 +40540,21 @@ Object.freeze({
40405
40540
  form: "single",
40406
40541
  optional: false
40407
40542
  }],
40543
+ "recording.getAvailabilityBatch": [{
40544
+ name: "deviceIds",
40545
+ form: "array",
40546
+ optional: false
40547
+ }],
40408
40548
  "recording.getDaysWithRecordings": [{
40409
40549
  name: "deviceId",
40410
40550
  form: "single",
40411
40551
  optional: false
40412
40552
  }],
40553
+ "recording.getDaysWithRecordingsBatch": [{
40554
+ name: "deviceIds",
40555
+ form: "array",
40556
+ optional: false
40557
+ }],
40413
40558
  "recording.getDeviceConfig": [{
40414
40559
  name: "deviceId",
40415
40560
  form: "single",
package/dist/addon.mjs CHANGED
@@ -7712,7 +7712,8 @@ var OpsLogReasonSchema = _enum([
7712
7712
  "quota",
7713
7713
  "manual",
7714
7714
  "operator",
7715
- "maintenance"
7715
+ "maintenance",
7716
+ "orphaned-device"
7716
7717
  ]);
7717
7718
  /** One audit row, shared verbatim by both domains. */
7718
7719
  var OpsLogEntrySchema = object({
@@ -13007,10 +13008,39 @@ var DevicePersistConfigPayloadSchema = object({
13007
13008
  deviceId: number(),
13008
13009
  data: record(string(), unknown())
13009
13010
  });
13011
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13012
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13013
+ var MigrateSwitchOutcomeSchema = _enum([
13014
+ "off",
13015
+ "on",
13016
+ "not-offered",
13017
+ "unreachable"
13018
+ ]);
13019
+ var MigrateSwitchReportSchema = object({
13020
+ deviceId: number(),
13021
+ switchId: CameraSwitchIdSchema,
13022
+ outcome: MigrateSwitchOutcomeSchema,
13023
+ detail: string().optional()
13024
+ });
13025
+ var MigrateDeviceResultSchema = object({
13026
+ sourceId: number(),
13027
+ targetId: number(),
13028
+ switches: array(MigrateSwitchReportSchema).readonly(),
13029
+ /** Switches that could NOT be turned off on the source. Empty is the only
13030
+ * value meaning the replaced hardware is quiet. */
13031
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13032
+ swapped: boolean()
13033
+ });
13010
13034
  method(object({
13011
13035
  addonId: string(),
13012
13036
  stableId: string()
13013
- }), 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({
13037
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13038
+ sourceId: number(),
13039
+ targetId: number()
13040
+ }), MigrateDeviceResultSchema, {
13041
+ kind: "mutation",
13042
+ auth: "admin"
13043
+ }), 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({
13014
13044
  deviceId: number(),
13015
13045
  name: string()
13016
13046
  }), _void(), {
@@ -19185,6 +19215,34 @@ var KeyEventsForDeviceSchema = object({
19185
19215
  deviceId: number(),
19186
19216
  events: array(KeyEventSchema).readonly()
19187
19217
  });
19218
+ /** One non-empty bucket of the timeline histogram. */
19219
+ var EventDensityBucketSchema = object({
19220
+ bucketStart: number(),
19221
+ motion: number().int(),
19222
+ object: number().int(),
19223
+ audio: number().int()
19224
+ });
19225
+ /**
19226
+ * One camera's row in a `getEventDensityBatch` answer.
19227
+ *
19228
+ * TWO facts, and the timeline draws them differently:
19229
+ *
19230
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19231
+ * claim: read, and nothing happened in the window.
19232
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19233
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19234
+ * not quiet.
19235
+ *
19236
+ * Fanned out per camera the difference was free — one query rejected while the
19237
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19238
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19239
+ * got eleven cannot tell which one is missing, or that any is.
19240
+ */
19241
+ var EventDensityForDeviceSchema = object({
19242
+ deviceId: number(),
19243
+ read: _enum(["read", "unreadable"]),
19244
+ buckets: array(EventDensityBucketSchema).readonly()
19245
+ });
19188
19246
  object({
19189
19247
  trackId: string(),
19190
19248
  className: string(),
@@ -19495,12 +19553,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19495
19553
  since: number(),
19496
19554
  until: number(),
19497
19555
  bucketMs: number().int().positive()
19498
- }), array(object({
19499
- bucketStart: number(),
19500
- motion: number().int(),
19501
- object: number().int(),
19502
- audio: number().int()
19503
- })).readonly()), method(object({
19556
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19557
+ deviceIds: array(number()).min(1).max(200),
19558
+ since: number(),
19559
+ until: number(),
19560
+ bucketMs: number().int().positive()
19561
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19504
19562
  deviceId: number(),
19505
19563
  cutoffMs: number()
19506
19564
  }), object({
@@ -28717,6 +28775,39 @@ var RecordingDaysSchema = object({
28717
28775
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28718
28776
  days: array(number())
28719
28777
  });
28778
+ /**
28779
+ * One camera's row in a `getAvailabilityBatch` answer.
28780
+ *
28781
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
28782
+ * batch collapses the transport, not the work — plus the one thing the singular
28783
+ * method never had to say:
28784
+ *
28785
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
28786
+ * no footage in the window", which is a real claim.
28787
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
28788
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
28789
+ * that emptiness means NOTHING.
28790
+ *
28791
+ * A timeline that renders the second as the first tells an operator there is no
28792
+ * footage when the truth is that nobody looked.
28793
+ */
28794
+ var RecordingAvailabilityForDeviceSchema = object({
28795
+ deviceId: number(),
28796
+ read: _enum(["read", "unreadable"]),
28797
+ ranges: array(RecordingRangeSchema).readonly()
28798
+ });
28799
+ /**
28800
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
28801
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
28802
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
28803
+ * the date-picker's day dots must not spell them the same.
28804
+ */
28805
+ var RecordingDaysForDeviceSchema = object({
28806
+ deviceId: number(),
28807
+ read: _enum(["read", "unreadable"]),
28808
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28809
+ days: array(number()).readonly()
28810
+ });
28720
28811
  var RecordingManifestSchema = object({
28721
28812
  deviceId: number(),
28722
28813
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -28951,6 +29042,13 @@ method(object({
28951
29042
  }), RecordingAvailabilitySchema, {
28952
29043
  kind: "query",
28953
29044
  auth: "protected"
29045
+ }), method(object({
29046
+ deviceIds: array(number()).min(1).max(200),
29047
+ fromMs: number(),
29048
+ toMs: number()
29049
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29050
+ kind: "query",
29051
+ auth: "protected"
28954
29052
  }), method(object({
28955
29053
  deviceId: number(),
28956
29054
  fromMs: number(),
@@ -28959,6 +29057,14 @@ method(object({
28959
29057
  }), RecordingDaysSchema, {
28960
29058
  kind: "query",
28961
29059
  auth: "protected"
29060
+ }), method(object({
29061
+ deviceIds: array(number()).min(1).max(200),
29062
+ fromMs: number(),
29063
+ toMs: number(),
29064
+ tzOffsetMinutes: number()
29065
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29066
+ kind: "query",
29067
+ auth: "protected"
28962
29068
  }), method(object({
28963
29069
  deviceId: number(),
28964
29070
  fromMs: number(),
@@ -34525,6 +34631,12 @@ Object.freeze({
34525
34631
  addonId: null,
34526
34632
  access: "view"
34527
34633
  },
34634
+ "deviceManager.migrateDevice": {
34635
+ capName: "device-manager",
34636
+ capScope: "system",
34637
+ addonId: null,
34638
+ access: "create"
34639
+ },
34528
34640
  "deviceManager.persistConfig": {
34529
34641
  capName: "device-manager",
34530
34642
  capScope: "system",
@@ -36259,6 +36371,12 @@ Object.freeze({
36259
36371
  addonId: null,
36260
36372
  access: "view"
36261
36373
  },
36374
+ "pipelineAnalytics.getEventDensityBatch": {
36375
+ capName: "pipeline-analytics",
36376
+ capScope: "device",
36377
+ addonId: null,
36378
+ access: "view"
36379
+ },
36262
36380
  "pipelineAnalytics.getEventMedia": {
36263
36381
  capName: "pipeline-analytics",
36264
36382
  capScope: "device",
@@ -37381,12 +37499,24 @@ Object.freeze({
37381
37499
  addonId: null,
37382
37500
  access: "view"
37383
37501
  },
37502
+ "recording.getAvailabilityBatch": {
37503
+ capName: "recording",
37504
+ capScope: "system",
37505
+ addonId: null,
37506
+ access: "view"
37507
+ },
37384
37508
  "recording.getDaysWithRecordings": {
37385
37509
  capName: "recording",
37386
37510
  capScope: "system",
37387
37511
  addonId: null,
37388
37512
  access: "view"
37389
37513
  },
37514
+ "recording.getDaysWithRecordingsBatch": {
37515
+ capName: "recording",
37516
+ capScope: "system",
37517
+ addonId: null,
37518
+ access: "view"
37519
+ },
37390
37520
  "recording.getDeviceConfig": {
37391
37521
  capName: "recording",
37392
37522
  capScope: "system",
@@ -39991,6 +40121,11 @@ Object.freeze({
39991
40121
  form: "single",
39992
40122
  optional: false
39993
40123
  }],
40124
+ "pipelineAnalytics.getEventDensityBatch": [{
40125
+ name: "deviceIds",
40126
+ form: "array",
40127
+ optional: false
40128
+ }],
39994
40129
  "pipelineAnalytics.getEventMedia": [{
39995
40130
  name: "deviceId",
39996
40131
  form: "single",
@@ -40406,11 +40541,21 @@ Object.freeze({
40406
40541
  form: "single",
40407
40542
  optional: false
40408
40543
  }],
40544
+ "recording.getAvailabilityBatch": [{
40545
+ name: "deviceIds",
40546
+ form: "array",
40547
+ optional: false
40548
+ }],
40409
40549
  "recording.getDaysWithRecordings": [{
40410
40550
  name: "deviceId",
40411
40551
  form: "single",
40412
40552
  optional: false
40413
40553
  }],
40554
+ "recording.getDaysWithRecordingsBatch": [{
40555
+ name: "deviceIds",
40556
+ form: "array",
40557
+ optional: false
40558
+ }],
40414
40559
  "recording.getDeviceConfig": [{
40415
40560
  name: "deviceId",
40416
40561
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.52",
3
+ "version": "0.2.54",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",