@camstack/addon-provider-hikvision 1.2.59 → 1.2.61

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
@@ -7713,7 +7713,8 @@ var OpsLogReasonSchema = _enum([
7713
7713
  "quota",
7714
7714
  "manual",
7715
7715
  "operator",
7716
- "maintenance"
7716
+ "maintenance",
7717
+ "orphaned-device"
7717
7718
  ]);
7718
7719
  /** One audit row, shared verbatim by both domains. */
7719
7720
  var OpsLogEntrySchema = object({
@@ -13301,10 +13302,39 @@ var DevicePersistConfigPayloadSchema = object({
13301
13302
  deviceId: number(),
13302
13303
  data: record(string(), unknown())
13303
13304
  });
13305
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13306
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13307
+ var MigrateSwitchOutcomeSchema = _enum([
13308
+ "off",
13309
+ "on",
13310
+ "not-offered",
13311
+ "unreachable"
13312
+ ]);
13313
+ var MigrateSwitchReportSchema = object({
13314
+ deviceId: number(),
13315
+ switchId: CameraSwitchIdSchema,
13316
+ outcome: MigrateSwitchOutcomeSchema,
13317
+ detail: string().optional()
13318
+ });
13319
+ var MigrateDeviceResultSchema = object({
13320
+ sourceId: number(),
13321
+ targetId: number(),
13322
+ switches: array(MigrateSwitchReportSchema).readonly(),
13323
+ /** Switches that could NOT be turned off on the source. Empty is the only
13324
+ * value meaning the replaced hardware is quiet. */
13325
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13326
+ swapped: boolean()
13327
+ });
13304
13328
  method(object({
13305
13329
  addonId: string(),
13306
13330
  stableId: string()
13307
- }), 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({
13331
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13332
+ sourceId: number(),
13333
+ targetId: number()
13334
+ }), MigrateDeviceResultSchema, {
13335
+ kind: "mutation",
13336
+ auth: "admin"
13337
+ }), 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({
13308
13338
  deviceId: number(),
13309
13339
  name: string()
13310
13340
  }), _void(), {
@@ -19498,6 +19528,34 @@ var KeyEventsForDeviceSchema = object({
19498
19528
  deviceId: number(),
19499
19529
  events: array(KeyEventSchema).readonly()
19500
19530
  });
19531
+ /** One non-empty bucket of the timeline histogram. */
19532
+ var EventDensityBucketSchema = object({
19533
+ bucketStart: number(),
19534
+ motion: number().int(),
19535
+ object: number().int(),
19536
+ audio: number().int()
19537
+ });
19538
+ /**
19539
+ * One camera's row in a `getEventDensityBatch` answer.
19540
+ *
19541
+ * TWO facts, and the timeline draws them differently:
19542
+ *
19543
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19544
+ * claim: read, and nothing happened in the window.
19545
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19546
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19547
+ * not quiet.
19548
+ *
19549
+ * Fanned out per camera the difference was free — one query rejected while the
19550
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19551
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19552
+ * got eleven cannot tell which one is missing, or that any is.
19553
+ */
19554
+ var EventDensityForDeviceSchema = object({
19555
+ deviceId: number(),
19556
+ read: _enum(["read", "unreadable"]),
19557
+ buckets: array(EventDensityBucketSchema).readonly()
19558
+ });
19501
19559
  object({
19502
19560
  trackId: string(),
19503
19561
  className: string(),
@@ -19808,12 +19866,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19808
19866
  since: number(),
19809
19867
  until: number(),
19810
19868
  bucketMs: number().int().positive()
19811
- }), array(object({
19812
- bucketStart: number(),
19813
- motion: number().int(),
19814
- object: number().int(),
19815
- audio: number().int()
19816
- })).readonly()), method(object({
19869
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19870
+ deviceIds: array(number()).min(1).max(200),
19871
+ since: number(),
19872
+ until: number(),
19873
+ bucketMs: number().int().positive()
19874
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19817
19875
  deviceId: number(),
19818
19876
  cutoffMs: number()
19819
19877
  }), object({
@@ -29075,6 +29133,39 @@ var RecordingDaysSchema = object({
29075
29133
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29076
29134
  days: array(number())
29077
29135
  });
29136
+ /**
29137
+ * One camera's row in a `getAvailabilityBatch` answer.
29138
+ *
29139
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29140
+ * batch collapses the transport, not the work — plus the one thing the singular
29141
+ * method never had to say:
29142
+ *
29143
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29144
+ * no footage in the window", which is a real claim.
29145
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29146
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29147
+ * that emptiness means NOTHING.
29148
+ *
29149
+ * A timeline that renders the second as the first tells an operator there is no
29150
+ * footage when the truth is that nobody looked.
29151
+ */
29152
+ var RecordingAvailabilityForDeviceSchema = object({
29153
+ deviceId: number(),
29154
+ read: _enum(["read", "unreadable"]),
29155
+ ranges: array(RecordingRangeSchema).readonly()
29156
+ });
29157
+ /**
29158
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29159
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29160
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29161
+ * the date-picker's day dots must not spell them the same.
29162
+ */
29163
+ var RecordingDaysForDeviceSchema = object({
29164
+ deviceId: number(),
29165
+ read: _enum(["read", "unreadable"]),
29166
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29167
+ days: array(number()).readonly()
29168
+ });
29078
29169
  var RecordingManifestSchema = object({
29079
29170
  deviceId: number(),
29080
29171
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29309,6 +29400,13 @@ method(object({
29309
29400
  }), RecordingAvailabilitySchema, {
29310
29401
  kind: "query",
29311
29402
  auth: "protected"
29403
+ }), method(object({
29404
+ deviceIds: array(number()).min(1).max(200),
29405
+ fromMs: number(),
29406
+ toMs: number()
29407
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29408
+ kind: "query",
29409
+ auth: "protected"
29312
29410
  }), method(object({
29313
29411
  deviceId: number(),
29314
29412
  fromMs: number(),
@@ -29317,6 +29415,14 @@ method(object({
29317
29415
  }), RecordingDaysSchema, {
29318
29416
  kind: "query",
29319
29417
  auth: "protected"
29418
+ }), method(object({
29419
+ deviceIds: array(number()).min(1).max(200),
29420
+ fromMs: number(),
29421
+ toMs: number(),
29422
+ tzOffsetMinutes: number()
29423
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29424
+ kind: "query",
29425
+ auth: "protected"
29320
29426
  }), method(object({
29321
29427
  deviceId: number(),
29322
29428
  fromMs: number(),
@@ -34910,6 +35016,12 @@ Object.freeze({
34910
35016
  addonId: null,
34911
35017
  access: "view"
34912
35018
  },
35019
+ "deviceManager.migrateDevice": {
35020
+ capName: "device-manager",
35021
+ capScope: "system",
35022
+ addonId: null,
35023
+ access: "create"
35024
+ },
34913
35025
  "deviceManager.persistConfig": {
34914
35026
  capName: "device-manager",
34915
35027
  capScope: "system",
@@ -36644,6 +36756,12 @@ Object.freeze({
36644
36756
  addonId: null,
36645
36757
  access: "view"
36646
36758
  },
36759
+ "pipelineAnalytics.getEventDensityBatch": {
36760
+ capName: "pipeline-analytics",
36761
+ capScope: "device",
36762
+ addonId: null,
36763
+ access: "view"
36764
+ },
36647
36765
  "pipelineAnalytics.getEventMedia": {
36648
36766
  capName: "pipeline-analytics",
36649
36767
  capScope: "device",
@@ -37766,12 +37884,24 @@ Object.freeze({
37766
37884
  addonId: null,
37767
37885
  access: "view"
37768
37886
  },
37887
+ "recording.getAvailabilityBatch": {
37888
+ capName: "recording",
37889
+ capScope: "system",
37890
+ addonId: null,
37891
+ access: "view"
37892
+ },
37769
37893
  "recording.getDaysWithRecordings": {
37770
37894
  capName: "recording",
37771
37895
  capScope: "system",
37772
37896
  addonId: null,
37773
37897
  access: "view"
37774
37898
  },
37899
+ "recording.getDaysWithRecordingsBatch": {
37900
+ capName: "recording",
37901
+ capScope: "system",
37902
+ addonId: null,
37903
+ access: "view"
37904
+ },
37775
37905
  "recording.getDeviceConfig": {
37776
37906
  capName: "recording",
37777
37907
  capScope: "system",
@@ -40376,6 +40506,11 @@ Object.freeze({
40376
40506
  form: "single",
40377
40507
  optional: false
40378
40508
  }],
40509
+ "pipelineAnalytics.getEventDensityBatch": [{
40510
+ name: "deviceIds",
40511
+ form: "array",
40512
+ optional: false
40513
+ }],
40379
40514
  "pipelineAnalytics.getEventMedia": [{
40380
40515
  name: "deviceId",
40381
40516
  form: "single",
@@ -40791,11 +40926,21 @@ Object.freeze({
40791
40926
  form: "single",
40792
40927
  optional: false
40793
40928
  }],
40929
+ "recording.getAvailabilityBatch": [{
40930
+ name: "deviceIds",
40931
+ form: "array",
40932
+ optional: false
40933
+ }],
40794
40934
  "recording.getDaysWithRecordings": [{
40795
40935
  name: "deviceId",
40796
40936
  form: "single",
40797
40937
  optional: false
40798
40938
  }],
40939
+ "recording.getDaysWithRecordingsBatch": [{
40940
+ name: "deviceIds",
40941
+ form: "array",
40942
+ optional: false
40943
+ }],
40799
40944
  "recording.getDeviceConfig": [{
40800
40945
  name: "deviceId",
40801
40946
  form: "single",
package/dist/addon.mjs CHANGED
@@ -7714,7 +7714,8 @@ var OpsLogReasonSchema = _enum([
7714
7714
  "quota",
7715
7715
  "manual",
7716
7716
  "operator",
7717
- "maintenance"
7717
+ "maintenance",
7718
+ "orphaned-device"
7718
7719
  ]);
7719
7720
  /** One audit row, shared verbatim by both domains. */
7720
7721
  var OpsLogEntrySchema = object({
@@ -13302,10 +13303,39 @@ var DevicePersistConfigPayloadSchema = object({
13302
13303
  deviceId: number(),
13303
13304
  data: record(string(), unknown())
13304
13305
  });
13306
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13307
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13308
+ var MigrateSwitchOutcomeSchema = _enum([
13309
+ "off",
13310
+ "on",
13311
+ "not-offered",
13312
+ "unreachable"
13313
+ ]);
13314
+ var MigrateSwitchReportSchema = object({
13315
+ deviceId: number(),
13316
+ switchId: CameraSwitchIdSchema,
13317
+ outcome: MigrateSwitchOutcomeSchema,
13318
+ detail: string().optional()
13319
+ });
13320
+ var MigrateDeviceResultSchema = object({
13321
+ sourceId: number(),
13322
+ targetId: number(),
13323
+ switches: array(MigrateSwitchReportSchema).readonly(),
13324
+ /** Switches that could NOT be turned off on the source. Empty is the only
13325
+ * value meaning the replaced hardware is quiet. */
13326
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13327
+ swapped: boolean()
13328
+ });
13305
13329
  method(object({
13306
13330
  addonId: string(),
13307
13331
  stableId: string()
13308
- }), 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({
13332
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13333
+ sourceId: number(),
13334
+ targetId: number()
13335
+ }), MigrateDeviceResultSchema, {
13336
+ kind: "mutation",
13337
+ auth: "admin"
13338
+ }), 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({
13309
13339
  deviceId: number(),
13310
13340
  name: string()
13311
13341
  }), _void(), {
@@ -19499,6 +19529,34 @@ var KeyEventsForDeviceSchema = object({
19499
19529
  deviceId: number(),
19500
19530
  events: array(KeyEventSchema).readonly()
19501
19531
  });
19532
+ /** One non-empty bucket of the timeline histogram. */
19533
+ var EventDensityBucketSchema = object({
19534
+ bucketStart: number(),
19535
+ motion: number().int(),
19536
+ object: number().int(),
19537
+ audio: number().int()
19538
+ });
19539
+ /**
19540
+ * One camera's row in a `getEventDensityBatch` answer.
19541
+ *
19542
+ * TWO facts, and the timeline draws them differently:
19543
+ *
19544
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19545
+ * claim: read, and nothing happened in the window.
19546
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19547
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19548
+ * not quiet.
19549
+ *
19550
+ * Fanned out per camera the difference was free — one query rejected while the
19551
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19552
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19553
+ * got eleven cannot tell which one is missing, or that any is.
19554
+ */
19555
+ var EventDensityForDeviceSchema = object({
19556
+ deviceId: number(),
19557
+ read: _enum(["read", "unreadable"]),
19558
+ buckets: array(EventDensityBucketSchema).readonly()
19559
+ });
19502
19560
  object({
19503
19561
  trackId: string(),
19504
19562
  className: string(),
@@ -19809,12 +19867,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19809
19867
  since: number(),
19810
19868
  until: number(),
19811
19869
  bucketMs: number().int().positive()
19812
- }), array(object({
19813
- bucketStart: number(),
19814
- motion: number().int(),
19815
- object: number().int(),
19816
- audio: number().int()
19817
- })).readonly()), method(object({
19870
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19871
+ deviceIds: array(number()).min(1).max(200),
19872
+ since: number(),
19873
+ until: number(),
19874
+ bucketMs: number().int().positive()
19875
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19818
19876
  deviceId: number(),
19819
19877
  cutoffMs: number()
19820
19878
  }), object({
@@ -29076,6 +29134,39 @@ var RecordingDaysSchema = object({
29076
29134
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29077
29135
  days: array(number())
29078
29136
  });
29137
+ /**
29138
+ * One camera's row in a `getAvailabilityBatch` answer.
29139
+ *
29140
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29141
+ * batch collapses the transport, not the work — plus the one thing the singular
29142
+ * method never had to say:
29143
+ *
29144
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29145
+ * no footage in the window", which is a real claim.
29146
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29147
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29148
+ * that emptiness means NOTHING.
29149
+ *
29150
+ * A timeline that renders the second as the first tells an operator there is no
29151
+ * footage when the truth is that nobody looked.
29152
+ */
29153
+ var RecordingAvailabilityForDeviceSchema = object({
29154
+ deviceId: number(),
29155
+ read: _enum(["read", "unreadable"]),
29156
+ ranges: array(RecordingRangeSchema).readonly()
29157
+ });
29158
+ /**
29159
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29160
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29161
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29162
+ * the date-picker's day dots must not spell them the same.
29163
+ */
29164
+ var RecordingDaysForDeviceSchema = object({
29165
+ deviceId: number(),
29166
+ read: _enum(["read", "unreadable"]),
29167
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29168
+ days: array(number()).readonly()
29169
+ });
29079
29170
  var RecordingManifestSchema = object({
29080
29171
  deviceId: number(),
29081
29172
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29310,6 +29401,13 @@ method(object({
29310
29401
  }), RecordingAvailabilitySchema, {
29311
29402
  kind: "query",
29312
29403
  auth: "protected"
29404
+ }), method(object({
29405
+ deviceIds: array(number()).min(1).max(200),
29406
+ fromMs: number(),
29407
+ toMs: number()
29408
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29409
+ kind: "query",
29410
+ auth: "protected"
29313
29411
  }), method(object({
29314
29412
  deviceId: number(),
29315
29413
  fromMs: number(),
@@ -29318,6 +29416,14 @@ method(object({
29318
29416
  }), RecordingDaysSchema, {
29319
29417
  kind: "query",
29320
29418
  auth: "protected"
29419
+ }), method(object({
29420
+ deviceIds: array(number()).min(1).max(200),
29421
+ fromMs: number(),
29422
+ toMs: number(),
29423
+ tzOffsetMinutes: number()
29424
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29425
+ kind: "query",
29426
+ auth: "protected"
29321
29427
  }), method(object({
29322
29428
  deviceId: number(),
29323
29429
  fromMs: number(),
@@ -34911,6 +35017,12 @@ Object.freeze({
34911
35017
  addonId: null,
34912
35018
  access: "view"
34913
35019
  },
35020
+ "deviceManager.migrateDevice": {
35021
+ capName: "device-manager",
35022
+ capScope: "system",
35023
+ addonId: null,
35024
+ access: "create"
35025
+ },
34914
35026
  "deviceManager.persistConfig": {
34915
35027
  capName: "device-manager",
34916
35028
  capScope: "system",
@@ -36645,6 +36757,12 @@ Object.freeze({
36645
36757
  addonId: null,
36646
36758
  access: "view"
36647
36759
  },
36760
+ "pipelineAnalytics.getEventDensityBatch": {
36761
+ capName: "pipeline-analytics",
36762
+ capScope: "device",
36763
+ addonId: null,
36764
+ access: "view"
36765
+ },
36648
36766
  "pipelineAnalytics.getEventMedia": {
36649
36767
  capName: "pipeline-analytics",
36650
36768
  capScope: "device",
@@ -37767,12 +37885,24 @@ Object.freeze({
37767
37885
  addonId: null,
37768
37886
  access: "view"
37769
37887
  },
37888
+ "recording.getAvailabilityBatch": {
37889
+ capName: "recording",
37890
+ capScope: "system",
37891
+ addonId: null,
37892
+ access: "view"
37893
+ },
37770
37894
  "recording.getDaysWithRecordings": {
37771
37895
  capName: "recording",
37772
37896
  capScope: "system",
37773
37897
  addonId: null,
37774
37898
  access: "view"
37775
37899
  },
37900
+ "recording.getDaysWithRecordingsBatch": {
37901
+ capName: "recording",
37902
+ capScope: "system",
37903
+ addonId: null,
37904
+ access: "view"
37905
+ },
37776
37906
  "recording.getDeviceConfig": {
37777
37907
  capName: "recording",
37778
37908
  capScope: "system",
@@ -40377,6 +40507,11 @@ Object.freeze({
40377
40507
  form: "single",
40378
40508
  optional: false
40379
40509
  }],
40510
+ "pipelineAnalytics.getEventDensityBatch": [{
40511
+ name: "deviceIds",
40512
+ form: "array",
40513
+ optional: false
40514
+ }],
40380
40515
  "pipelineAnalytics.getEventMedia": [{
40381
40516
  name: "deviceId",
40382
40517
  form: "single",
@@ -40792,11 +40927,21 @@ Object.freeze({
40792
40927
  form: "single",
40793
40928
  optional: false
40794
40929
  }],
40930
+ "recording.getAvailabilityBatch": [{
40931
+ name: "deviceIds",
40932
+ form: "array",
40933
+ optional: false
40934
+ }],
40795
40935
  "recording.getDaysWithRecordings": [{
40796
40936
  name: "deviceId",
40797
40937
  form: "single",
40798
40938
  optional: false
40799
40939
  }],
40940
+ "recording.getDaysWithRecordingsBatch": [{
40941
+ name: "deviceIds",
40942
+ form: "array",
40943
+ optional: false
40944
+ }],
40800
40945
  "recording.getDeviceConfig": [{
40801
40946
  name: "deviceId",
40802
40947
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-hikvision",
3
- "version": "1.2.59",
3
+ "version": "1.2.61",
4
4
  "description": "Hikvision camera device provider addon for CamStack — ISAPI over HTTP(S) with digest auth (snapshot, alarm stream, RTSP discovery)",
5
5
  "keywords": [
6
6
  "camstack",