@camstack/addon-provider-hikvision 1.2.25 → 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.
- package/README.md +0 -1
- package/dist/addon.js +172 -38
- package/dist/addon.mjs +172 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,4 +35,3 @@ The probe runs once per camera lifetime (after the first successful `publishToBr
|
|
|
35
35
|
## References
|
|
36
36
|
|
|
37
37
|
- ISAPI Service v2 & v3 specs (Hikvision ISAPI doc package — vendor download).
|
|
38
|
-
- Scrypted Hikvision plugin — `plugins/hikvision/src/` in the [Scrypted](https://github.com/koush/scrypted) repository (port reference for endpoint patterns + alarm-stream parsing).
|
package/dist/addon.js
CHANGED
|
@@ -6,7 +6,7 @@ let node_crypto = require("node:crypto");
|
|
|
6
6
|
let node_http = require("node:http");
|
|
7
7
|
let node_https = require("node:https");
|
|
8
8
|
let node_os = require("node:os");
|
|
9
|
-
//#region ../types/dist/event-category-
|
|
9
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
10
10
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
11
11
|
EventCategory["SystemBoot"] = "system.boot";
|
|
12
12
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -23,9 +23,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
23
23
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
24
24
|
/**
|
|
25
25
|
* A newer addon or server-root package version was found by the
|
|
26
|
-
* authoritative registry check. Emitted once
|
|
27
|
-
* `(
|
|
28
|
-
*
|
|
26
|
+
* authoritative registry check. Emitted once when any observed
|
|
27
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
28
|
+
* the payload carries the full currently-available list. Repeated
|
|
29
|
+
* polling of the same latests is silent.
|
|
29
30
|
*/
|
|
30
31
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
31
32
|
/**
|
|
@@ -7543,6 +7544,10 @@ var RecordingBandSchema = object({
|
|
|
7543
7544
|
preBufferSec: number().min(0).optional(),
|
|
7544
7545
|
postBufferSec: number().min(0).optional()
|
|
7545
7546
|
});
|
|
7547
|
+
({
|
|
7548
|
+
preBufferSec: 10,
|
|
7549
|
+
postBufferSec: 30
|
|
7550
|
+
}).postBufferSec * 1e3;
|
|
7546
7551
|
/**
|
|
7547
7552
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7548
7553
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7590,6 +7595,12 @@ var RecordingConfigSchema = object({
|
|
|
7590
7595
|
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7591
7596
|
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7592
7597
|
mode: RecordingStorageModeSchema.optional(),
|
|
7598
|
+
/**
|
|
7599
|
+
* Which assigned broker slots to record. Absent / empty = {@link
|
|
7600
|
+
* DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
|
|
7601
|
+
* camera's currently assigned slots — never `mid` unless the operator
|
|
7602
|
+
* picks it, and never a slot the broker has not assigned.
|
|
7603
|
+
*/
|
|
7593
7604
|
profiles: array(CamProfileSchema).optional(),
|
|
7594
7605
|
segmentSeconds: number().int().positive().optional(),
|
|
7595
7606
|
/**
|
|
@@ -7670,7 +7681,11 @@ var RelocateFootageInputSchema = object({
|
|
|
7670
7681
|
profiles: array(string()).optional(),
|
|
7671
7682
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7672
7683
|
* never allowed to starve live writers. */
|
|
7673
|
-
throttleMbps: number().min(1).max(1e3).optional()
|
|
7684
|
+
throttleMbps: number().min(1).max(1e3).optional(),
|
|
7685
|
+
/** Move only segments whose startMs is >= this. Absent = the whole source
|
|
7686
|
+
* pile. Used when a full drain is too expensive and the operator only
|
|
7687
|
+
* wants the recent window on the new disk. */
|
|
7688
|
+
sinceMs: number().int().optional()
|
|
7674
7689
|
});
|
|
7675
7690
|
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7676
7691
|
* from persistent recording settings: a migration never changes
|
|
@@ -14729,6 +14744,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14729
14744
|
"alarm-disarmed",
|
|
14730
14745
|
"alarm-arming",
|
|
14731
14746
|
"alarm-arm-refused",
|
|
14747
|
+
"addon-updated",
|
|
14748
|
+
"server-updated",
|
|
14749
|
+
"export-completed",
|
|
14732
14750
|
"camera-online",
|
|
14733
14751
|
"camera-offline",
|
|
14734
14752
|
"camera-disabled",
|
|
@@ -16622,13 +16640,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16622
16640
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16623
16641
|
*
|
|
16624
16642
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16643
|
+
*
|
|
16644
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16645
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16646
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16625
16647
|
*/
|
|
16626
16648
|
var TrackFlagFields = {
|
|
16627
16649
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16628
16650
|
* `'staging'`. */
|
|
16629
16651
|
markForTrain: boolean().optional(),
|
|
16630
16652
|
/** Operator marked this track for diagnostic attention. */
|
|
16631
|
-
debug: boolean().optional()
|
|
16653
|
+
debug: boolean().optional(),
|
|
16654
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16655
|
+
favourited: boolean().optional()
|
|
16632
16656
|
};
|
|
16633
16657
|
/**
|
|
16634
16658
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16653,6 +16677,7 @@ var TrackFlagsSchema = object({
|
|
|
16653
16677
|
trackId: string(),
|
|
16654
16678
|
markForTrain: boolean(),
|
|
16655
16679
|
debug: boolean(),
|
|
16680
|
+
favourited: boolean(),
|
|
16656
16681
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16657
16682
|
* a track row) because this shape is only ever produced by the write body,
|
|
16658
16683
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -17285,6 +17310,12 @@ var TrackCascadeCountsSchema = object({
|
|
|
17285
17310
|
/** Per-track CLIP search vectors removed (best-effort). */
|
|
17286
17311
|
embeddings: number().int()
|
|
17287
17312
|
});
|
|
17313
|
+
/** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
|
|
17314
|
+
var DiskReconcileCountsSchema = object({
|
|
17315
|
+
mediaDropped: number().int(),
|
|
17316
|
+
tracks: number().int(),
|
|
17317
|
+
events: number().int()
|
|
17318
|
+
});
|
|
17288
17319
|
/** Event-store footprint for one camera. */
|
|
17289
17320
|
var EventStoreDeviceFootprintSchema = object({
|
|
17290
17321
|
deviceId: number(),
|
|
@@ -17461,6 +17492,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17461
17492
|
}), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
|
|
17462
17493
|
kind: "mutation",
|
|
17463
17494
|
auth: "admin"
|
|
17495
|
+
}), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
|
|
17496
|
+
kind: "mutation",
|
|
17497
|
+
auth: "admin"
|
|
17464
17498
|
}), method(object({
|
|
17465
17499
|
deviceId: number(),
|
|
17466
17500
|
trackIds: array(string()).min(1)
|
|
@@ -19011,6 +19045,24 @@ var CameraStatusDegradationSchema = object({
|
|
|
19011
19045
|
*
|
|
19012
19046
|
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
19013
19047
|
*/
|
|
19048
|
+
var DiskReconcileJobSchema = object({
|
|
19049
|
+
state: _enum([
|
|
19050
|
+
"idle",
|
|
19051
|
+
"running",
|
|
19052
|
+
"done",
|
|
19053
|
+
"error"
|
|
19054
|
+
]),
|
|
19055
|
+
total: number().int().nonnegative(),
|
|
19056
|
+
completed: number().int().nonnegative(),
|
|
19057
|
+
currentDeviceId: number().int().nullable(),
|
|
19058
|
+
failed: array(number().int()).readonly(),
|
|
19059
|
+
mediaDropped: number().int().nonnegative(),
|
|
19060
|
+
tracks: number().int().nonnegative(),
|
|
19061
|
+
events: number().int().nonnegative(),
|
|
19062
|
+
startedAtMs: number().int().nullable(),
|
|
19063
|
+
finishedAtMs: number().int().nullable(),
|
|
19064
|
+
error: string().nullable()
|
|
19065
|
+
});
|
|
19014
19066
|
var CameraStatusSchema = object({
|
|
19015
19067
|
deviceId: number(),
|
|
19016
19068
|
assignment: CameraAssignmentStatusSchema,
|
|
@@ -19242,7 +19294,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
|
19242
19294
|
}), CameraSwitchGroupSchema, {
|
|
19243
19295
|
kind: "mutation",
|
|
19244
19296
|
auth: "admin"
|
|
19245
|
-
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(),
|
|
19297
|
+
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
|
|
19298
|
+
kind: "mutation",
|
|
19299
|
+
auth: "admin"
|
|
19300
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
|
|
19246
19301
|
name: string(),
|
|
19247
19302
|
description: string().optional(),
|
|
19248
19303
|
config: CameraPipelineConfigSchema
|
|
@@ -20449,8 +20504,28 @@ var ClipSchema = object({
|
|
|
20449
20504
|
startMs: number(),
|
|
20450
20505
|
endMs: number()
|
|
20451
20506
|
}),
|
|
20452
|
-
/**
|
|
20453
|
-
thumbnail
|
|
20507
|
+
/**
|
|
20508
|
+
* Lazy thumbnail URL, never inlined.
|
|
20509
|
+
*
|
|
20510
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20511
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20512
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20513
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20514
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20515
|
+
* mint their own stills.
|
|
20516
|
+
*/
|
|
20517
|
+
thumbnail: string().optional(),
|
|
20518
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20519
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20520
|
+
* instead of minting one clip per marker. */
|
|
20521
|
+
eventIds: array(string()).optional(),
|
|
20522
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20523
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20524
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20525
|
+
holes: array(object({
|
|
20526
|
+
startMs: number(),
|
|
20527
|
+
endMs: number()
|
|
20528
|
+
})).optional()
|
|
20454
20529
|
});
|
|
20455
20530
|
var ClipPlaybackSchema = object({
|
|
20456
20531
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -26869,7 +26944,9 @@ var ExportOptionsSchema = object({
|
|
|
26869
26944
|
includeAudio: boolean(),
|
|
26870
26945
|
maxLifeMs: number().int().positive(),
|
|
26871
26946
|
deleteAfterDownload: boolean(),
|
|
26872
|
-
title: string().max(200).optional()
|
|
26947
|
+
title: string().max(200).optional(),
|
|
26948
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26949
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26873
26950
|
}).superRefine((v, ctx) => {
|
|
26874
26951
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26875
26952
|
code: ZodIssueCode.custom,
|
|
@@ -26928,10 +27005,18 @@ var ExportBytesSchema = object({
|
|
|
26928
27005
|
});
|
|
26929
27006
|
method(object({
|
|
26930
27007
|
deviceId: number(),
|
|
26931
|
-
|
|
27008
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
27009
|
+
profile: string().optional(),
|
|
27010
|
+
profiles: array(string()).min(1).optional(),
|
|
26932
27011
|
fromMs: number(),
|
|
26933
27012
|
toMs: number(),
|
|
26934
27013
|
options: ExportOptionsSchema
|
|
27014
|
+
}).superRefine((v, ctx) => {
|
|
27015
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
27016
|
+
code: ZodIssueCode.custom,
|
|
27017
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
27018
|
+
path: ["profiles"]
|
|
27019
|
+
});
|
|
26935
27020
|
}), ExportRecordSchema, {
|
|
26936
27021
|
kind: "mutation",
|
|
26937
27022
|
auth: "protected"
|
|
@@ -30086,15 +30171,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30086
30171
|
labels: ["probe not implemented"]
|
|
30087
30172
|
};
|
|
30088
30173
|
}
|
|
30089
|
-
/**
|
|
30090
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
30091
|
-
*
|
|
30092
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
30093
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
30094
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
30095
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
30096
|
-
*/
|
|
30097
|
-
restoreConcurrency = 4;
|
|
30098
30174
|
async restoreDevices(savedDevices) {
|
|
30099
30175
|
await this.onRestoreDevices(savedDevices);
|
|
30100
30176
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -30149,14 +30225,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30149
30225
|
});
|
|
30150
30226
|
}
|
|
30151
30227
|
};
|
|
30152
|
-
|
|
30153
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
30154
|
-
for (;;) {
|
|
30155
|
-
const saved = topLevel[nextTopLevel++];
|
|
30156
|
-
if (saved === void 0) return;
|
|
30157
|
-
await restoreOne(saved);
|
|
30158
|
-
}
|
|
30159
|
-
}));
|
|
30228
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
30160
30229
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
30161
30230
|
for (const saved of childRows) {
|
|
30162
30231
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -33560,6 +33629,12 @@ Object.freeze({
|
|
|
33560
33629
|
addonId: null,
|
|
33561
33630
|
access: "create"
|
|
33562
33631
|
},
|
|
33632
|
+
"pipelineAnalytics.reconcileFromDisk": {
|
|
33633
|
+
capName: "pipeline-analytics",
|
|
33634
|
+
capScope: "device",
|
|
33635
|
+
addonId: null,
|
|
33636
|
+
access: "create"
|
|
33637
|
+
},
|
|
33563
33638
|
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
33564
33639
|
capName: "pipeline-analytics",
|
|
33565
33640
|
capScope: "device",
|
|
@@ -33962,6 +34037,12 @@ Object.freeze({
|
|
|
33962
34037
|
addonId: null,
|
|
33963
34038
|
access: "view"
|
|
33964
34039
|
},
|
|
34040
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
34041
|
+
capName: "pipeline-orchestrator",
|
|
34042
|
+
capScope: "system",
|
|
34043
|
+
addonId: null,
|
|
34044
|
+
access: "view"
|
|
34045
|
+
},
|
|
33965
34046
|
"pipelineOrchestrator.listAgentSettings": {
|
|
33966
34047
|
capName: "pipeline-orchestrator",
|
|
33967
34048
|
capScope: "system",
|
|
@@ -33986,6 +34067,12 @@ Object.freeze({
|
|
|
33986
34067
|
addonId: null,
|
|
33987
34068
|
access: "create"
|
|
33988
34069
|
},
|
|
34070
|
+
"pipelineOrchestrator.reconcileFromDisk": {
|
|
34071
|
+
capName: "pipeline-orchestrator",
|
|
34072
|
+
capScope: "system",
|
|
34073
|
+
addonId: null,
|
|
34074
|
+
access: "create"
|
|
34075
|
+
},
|
|
33989
34076
|
"pipelineOrchestrator.removeAgentSettings": {
|
|
33990
34077
|
capName: "pipeline-orchestrator",
|
|
33991
34078
|
capScope: "system",
|
|
@@ -36977,6 +37064,11 @@ Object.freeze({
|
|
|
36977
37064
|
form: "single",
|
|
36978
37065
|
optional: true
|
|
36979
37066
|
}],
|
|
37067
|
+
"pipelineAnalytics.reconcileFromDisk": [{
|
|
37068
|
+
name: "deviceId",
|
|
37069
|
+
form: "single",
|
|
37070
|
+
optional: false
|
|
37071
|
+
}],
|
|
36980
37072
|
"pipelineAnalytics.restageRetrainTrack": [{
|
|
36981
37073
|
name: "deviceId",
|
|
36982
37074
|
form: "single",
|
|
@@ -40697,7 +40789,7 @@ async function pumpAlarmStream(stream, boundary, handlers, idleTimeoutMs = ALARM
|
|
|
40697
40789
|
for (const block of parts.blocks) {
|
|
40698
40790
|
const parsed = parseMultipartBlock(block);
|
|
40699
40791
|
if (!parsed) continue;
|
|
40700
|
-
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml")) {
|
|
40792
|
+
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml") || looksLikeAlertXml(parsed.bodyText)) {
|
|
40701
40793
|
const ev = parseAlertXml(parsed.bodyText);
|
|
40702
40794
|
if (ev) handlers.onEvent(ev);
|
|
40703
40795
|
}
|
|
@@ -40729,14 +40821,24 @@ function splitOnBoundary(buf, dashBoundary) {
|
|
|
40729
40821
|
tail
|
|
40730
40822
|
};
|
|
40731
40823
|
}
|
|
40824
|
+
function looksLikeAlertXml(text) {
|
|
40825
|
+
return text.includes("<eventType>") || text.includes("EventNotificationAlert");
|
|
40826
|
+
}
|
|
40732
40827
|
function parseMultipartBlock(block) {
|
|
40733
|
-
|
|
40734
|
-
|
|
40735
|
-
|
|
40736
|
-
if (headerEnd
|
|
40737
|
-
|
|
40738
|
-
|
|
40739
|
-
|
|
40828
|
+
const headerEnd = findHeaderEnd(block, 0);
|
|
40829
|
+
let headerBlob = "";
|
|
40830
|
+
let bodySlice;
|
|
40831
|
+
if (headerEnd >= 0) {
|
|
40832
|
+
headerBlob = Buffer.from(block.subarray(0, headerEnd)).toString("utf8");
|
|
40833
|
+
bodySlice = block.subarray(headerEnd + 4);
|
|
40834
|
+
} else {
|
|
40835
|
+
const raw = Buffer.from(block).toString("utf8");
|
|
40836
|
+
if (!looksLikeAlertXml(raw)) return null;
|
|
40837
|
+
return {
|
|
40838
|
+
contentType: "",
|
|
40839
|
+
bodyText: raw.replace(/\r?\n--$/m, "").trim()
|
|
40840
|
+
};
|
|
40841
|
+
}
|
|
40740
40842
|
const headers = {};
|
|
40741
40843
|
for (const line of headerBlob.split(/\r?\n/)) {
|
|
40742
40844
|
const idx = line.indexOf(":");
|
|
@@ -40752,6 +40854,28 @@ function findHeaderEnd(buf, from) {
|
|
|
40752
40854
|
for (let i = from; i + 3 < buf.length; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
|
|
40753
40855
|
return -1;
|
|
40754
40856
|
}
|
|
40857
|
+
/**
|
|
40858
|
+
* Event types that imply onboard motion. Compared after `eventType`
|
|
40859
|
+
* is lowercased. Some firmwares emit `regionExit`, others
|
|
40860
|
+
* `regionexiting` — accept both.
|
|
40861
|
+
*/
|
|
40862
|
+
function isHikvisionMotionAlarmType(type) {
|
|
40863
|
+
switch (type) {
|
|
40864
|
+
case "vmd":
|
|
40865
|
+
case "motiondetection":
|
|
40866
|
+
case "linedetection":
|
|
40867
|
+
case "fielddetection":
|
|
40868
|
+
case "regionentrance":
|
|
40869
|
+
case "regionexit":
|
|
40870
|
+
case "regionexiting":
|
|
40871
|
+
case "attendedbaggage":
|
|
40872
|
+
case "unattendedbaggage": return true;
|
|
40873
|
+
default: return false;
|
|
40874
|
+
}
|
|
40875
|
+
}
|
|
40876
|
+
function isHikvisionSmartAlarmType(type) {
|
|
40877
|
+
return type !== "vmd" && type !== "motiondetection" && isHikvisionMotionAlarmType(type);
|
|
40878
|
+
}
|
|
40755
40879
|
function parseAlertXml(xml) {
|
|
40756
40880
|
const type = extractTag(xml, "eventType");
|
|
40757
40881
|
if (!type) return null;
|
|
@@ -44412,7 +44536,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44412
44536
|
handleAlarmEvent(ev) {
|
|
44413
44537
|
const camStreamId = this.resolveAlarmCamStreamId(ev.channelId);
|
|
44414
44538
|
const isMotion = ev.type === "vmd" || ev.type === "motiondetection";
|
|
44415
|
-
const isSmart = ev.type
|
|
44539
|
+
const isSmart = isHikvisionSmartAlarmType(ev.type);
|
|
44416
44540
|
if (isMotion) {
|
|
44417
44541
|
this.handleMotionEvent(ev, camStreamId);
|
|
44418
44542
|
return;
|
|
@@ -44422,7 +44546,17 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44422
44546
|
this.handleMotionEvent(ev, camStreamId);
|
|
44423
44547
|
return;
|
|
44424
44548
|
}
|
|
44425
|
-
|
|
44549
|
+
if (ev.type === "videoloss") {
|
|
44550
|
+
this.ctx.logger.debug("hikvision: unhandled alarm event", {
|
|
44551
|
+
tags: { deviceId: this.id },
|
|
44552
|
+
meta: {
|
|
44553
|
+
type: ev.type,
|
|
44554
|
+
state: ev.state
|
|
44555
|
+
}
|
|
44556
|
+
});
|
|
44557
|
+
return;
|
|
44558
|
+
}
|
|
44559
|
+
this.ctx.logger.info("hikvision: unhandled alarm event", {
|
|
44426
44560
|
tags: { deviceId: this.id },
|
|
44427
44561
|
meta: {
|
|
44428
44562
|
type: ev.type,
|
package/dist/addon.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { networkInterfaces } from "node:os";
|
|
|
7
7
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
8
8
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
9
9
|
//#endregion
|
|
10
|
-
//#region ../types/dist/event-category-
|
|
10
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
11
11
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
12
12
|
EventCategory["SystemBoot"] = "system.boot";
|
|
13
13
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -24,9 +24,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
24
24
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
25
25
|
/**
|
|
26
26
|
* A newer addon or server-root package version was found by the
|
|
27
|
-
* authoritative registry check. Emitted once
|
|
28
|
-
* `(
|
|
29
|
-
*
|
|
27
|
+
* authoritative registry check. Emitted once when any observed
|
|
28
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
29
|
+
* the payload carries the full currently-available list. Repeated
|
|
30
|
+
* polling of the same latests is silent.
|
|
30
31
|
*/
|
|
31
32
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
32
33
|
/**
|
|
@@ -7544,6 +7545,10 @@ var RecordingBandSchema = object({
|
|
|
7544
7545
|
preBufferSec: number().min(0).optional(),
|
|
7545
7546
|
postBufferSec: number().min(0).optional()
|
|
7546
7547
|
});
|
|
7548
|
+
({
|
|
7549
|
+
preBufferSec: 10,
|
|
7550
|
+
postBufferSec: 30
|
|
7551
|
+
}).postBufferSec * 1e3;
|
|
7547
7552
|
/**
|
|
7548
7553
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7549
7554
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7591,6 +7596,12 @@ var RecordingConfigSchema = object({
|
|
|
7591
7596
|
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7592
7597
|
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7593
7598
|
mode: RecordingStorageModeSchema.optional(),
|
|
7599
|
+
/**
|
|
7600
|
+
* Which assigned broker slots to record. Absent / empty = {@link
|
|
7601
|
+
* DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
|
|
7602
|
+
* camera's currently assigned slots — never `mid` unless the operator
|
|
7603
|
+
* picks it, and never a slot the broker has not assigned.
|
|
7604
|
+
*/
|
|
7594
7605
|
profiles: array(CamProfileSchema).optional(),
|
|
7595
7606
|
segmentSeconds: number().int().positive().optional(),
|
|
7596
7607
|
/**
|
|
@@ -7671,7 +7682,11 @@ var RelocateFootageInputSchema = object({
|
|
|
7671
7682
|
profiles: array(string()).optional(),
|
|
7672
7683
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7673
7684
|
* never allowed to starve live writers. */
|
|
7674
|
-
throttleMbps: number().min(1).max(1e3).optional()
|
|
7685
|
+
throttleMbps: number().min(1).max(1e3).optional(),
|
|
7686
|
+
/** Move only segments whose startMs is >= this. Absent = the whole source
|
|
7687
|
+
* pile. Used when a full drain is too expensive and the operator only
|
|
7688
|
+
* wants the recent window on the new disk. */
|
|
7689
|
+
sinceMs: number().int().optional()
|
|
7675
7690
|
});
|
|
7676
7691
|
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7677
7692
|
* from persistent recording settings: a migration never changes
|
|
@@ -14730,6 +14745,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14730
14745
|
"alarm-disarmed",
|
|
14731
14746
|
"alarm-arming",
|
|
14732
14747
|
"alarm-arm-refused",
|
|
14748
|
+
"addon-updated",
|
|
14749
|
+
"server-updated",
|
|
14750
|
+
"export-completed",
|
|
14733
14751
|
"camera-online",
|
|
14734
14752
|
"camera-offline",
|
|
14735
14753
|
"camera-disabled",
|
|
@@ -16623,13 +16641,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16623
16641
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16624
16642
|
*
|
|
16625
16643
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16644
|
+
*
|
|
16645
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16646
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16647
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16626
16648
|
*/
|
|
16627
16649
|
var TrackFlagFields = {
|
|
16628
16650
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16629
16651
|
* `'staging'`. */
|
|
16630
16652
|
markForTrain: boolean().optional(),
|
|
16631
16653
|
/** Operator marked this track for diagnostic attention. */
|
|
16632
|
-
debug: boolean().optional()
|
|
16654
|
+
debug: boolean().optional(),
|
|
16655
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16656
|
+
favourited: boolean().optional()
|
|
16633
16657
|
};
|
|
16634
16658
|
/**
|
|
16635
16659
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16654,6 +16678,7 @@ var TrackFlagsSchema = object({
|
|
|
16654
16678
|
trackId: string(),
|
|
16655
16679
|
markForTrain: boolean(),
|
|
16656
16680
|
debug: boolean(),
|
|
16681
|
+
favourited: boolean(),
|
|
16657
16682
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16658
16683
|
* a track row) because this shape is only ever produced by the write body,
|
|
16659
16684
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -17286,6 +17311,12 @@ var TrackCascadeCountsSchema = object({
|
|
|
17286
17311
|
/** Per-track CLIP search vectors removed (best-effort). */
|
|
17287
17312
|
embeddings: number().int()
|
|
17288
17313
|
});
|
|
17314
|
+
/** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
|
|
17315
|
+
var DiskReconcileCountsSchema = object({
|
|
17316
|
+
mediaDropped: number().int(),
|
|
17317
|
+
tracks: number().int(),
|
|
17318
|
+
events: number().int()
|
|
17319
|
+
});
|
|
17289
17320
|
/** Event-store footprint for one camera. */
|
|
17290
17321
|
var EventStoreDeviceFootprintSchema = object({
|
|
17291
17322
|
deviceId: number(),
|
|
@@ -17462,6 +17493,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17462
17493
|
}), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
|
|
17463
17494
|
kind: "mutation",
|
|
17464
17495
|
auth: "admin"
|
|
17496
|
+
}), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
|
|
17497
|
+
kind: "mutation",
|
|
17498
|
+
auth: "admin"
|
|
17465
17499
|
}), method(object({
|
|
17466
17500
|
deviceId: number(),
|
|
17467
17501
|
trackIds: array(string()).min(1)
|
|
@@ -19012,6 +19046,24 @@ var CameraStatusDegradationSchema = object({
|
|
|
19012
19046
|
*
|
|
19013
19047
|
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
19014
19048
|
*/
|
|
19049
|
+
var DiskReconcileJobSchema = object({
|
|
19050
|
+
state: _enum([
|
|
19051
|
+
"idle",
|
|
19052
|
+
"running",
|
|
19053
|
+
"done",
|
|
19054
|
+
"error"
|
|
19055
|
+
]),
|
|
19056
|
+
total: number().int().nonnegative(),
|
|
19057
|
+
completed: number().int().nonnegative(),
|
|
19058
|
+
currentDeviceId: number().int().nullable(),
|
|
19059
|
+
failed: array(number().int()).readonly(),
|
|
19060
|
+
mediaDropped: number().int().nonnegative(),
|
|
19061
|
+
tracks: number().int().nonnegative(),
|
|
19062
|
+
events: number().int().nonnegative(),
|
|
19063
|
+
startedAtMs: number().int().nullable(),
|
|
19064
|
+
finishedAtMs: number().int().nullable(),
|
|
19065
|
+
error: string().nullable()
|
|
19066
|
+
});
|
|
19015
19067
|
var CameraStatusSchema = object({
|
|
19016
19068
|
deviceId: number(),
|
|
19017
19069
|
assignment: CameraAssignmentStatusSchema,
|
|
@@ -19243,7 +19295,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
|
19243
19295
|
}), CameraSwitchGroupSchema, {
|
|
19244
19296
|
kind: "mutation",
|
|
19245
19297
|
auth: "admin"
|
|
19246
|
-
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(),
|
|
19298
|
+
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
|
|
19299
|
+
kind: "mutation",
|
|
19300
|
+
auth: "admin"
|
|
19301
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
|
|
19247
19302
|
name: string(),
|
|
19248
19303
|
description: string().optional(),
|
|
19249
19304
|
config: CameraPipelineConfigSchema
|
|
@@ -20450,8 +20505,28 @@ var ClipSchema = object({
|
|
|
20450
20505
|
startMs: number(),
|
|
20451
20506
|
endMs: number()
|
|
20452
20507
|
}),
|
|
20453
|
-
/**
|
|
20454
|
-
thumbnail
|
|
20508
|
+
/**
|
|
20509
|
+
* Lazy thumbnail URL, never inlined.
|
|
20510
|
+
*
|
|
20511
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20512
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20513
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20514
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20515
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20516
|
+
* mint their own stills.
|
|
20517
|
+
*/
|
|
20518
|
+
thumbnail: string().optional(),
|
|
20519
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20520
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20521
|
+
* instead of minting one clip per marker. */
|
|
20522
|
+
eventIds: array(string()).optional(),
|
|
20523
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20524
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20525
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20526
|
+
holes: array(object({
|
|
20527
|
+
startMs: number(),
|
|
20528
|
+
endMs: number()
|
|
20529
|
+
})).optional()
|
|
20455
20530
|
});
|
|
20456
20531
|
var ClipPlaybackSchema = object({
|
|
20457
20532
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -26870,7 +26945,9 @@ var ExportOptionsSchema = object({
|
|
|
26870
26945
|
includeAudio: boolean(),
|
|
26871
26946
|
maxLifeMs: number().int().positive(),
|
|
26872
26947
|
deleteAfterDownload: boolean(),
|
|
26873
|
-
title: string().max(200).optional()
|
|
26948
|
+
title: string().max(200).optional(),
|
|
26949
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26950
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26874
26951
|
}).superRefine((v, ctx) => {
|
|
26875
26952
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26876
26953
|
code: ZodIssueCode.custom,
|
|
@@ -26929,10 +27006,18 @@ var ExportBytesSchema = object({
|
|
|
26929
27006
|
});
|
|
26930
27007
|
method(object({
|
|
26931
27008
|
deviceId: number(),
|
|
26932
|
-
|
|
27009
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
27010
|
+
profile: string().optional(),
|
|
27011
|
+
profiles: array(string()).min(1).optional(),
|
|
26933
27012
|
fromMs: number(),
|
|
26934
27013
|
toMs: number(),
|
|
26935
27014
|
options: ExportOptionsSchema
|
|
27015
|
+
}).superRefine((v, ctx) => {
|
|
27016
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
27017
|
+
code: ZodIssueCode.custom,
|
|
27018
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
27019
|
+
path: ["profiles"]
|
|
27020
|
+
});
|
|
26936
27021
|
}), ExportRecordSchema, {
|
|
26937
27022
|
kind: "mutation",
|
|
26938
27023
|
auth: "protected"
|
|
@@ -30087,15 +30172,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30087
30172
|
labels: ["probe not implemented"]
|
|
30088
30173
|
};
|
|
30089
30174
|
}
|
|
30090
|
-
/**
|
|
30091
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
30092
|
-
*
|
|
30093
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
30094
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
30095
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
30096
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
30097
|
-
*/
|
|
30098
|
-
restoreConcurrency = 4;
|
|
30099
30175
|
async restoreDevices(savedDevices) {
|
|
30100
30176
|
await this.onRestoreDevices(savedDevices);
|
|
30101
30177
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -30150,14 +30226,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30150
30226
|
});
|
|
30151
30227
|
}
|
|
30152
30228
|
};
|
|
30153
|
-
|
|
30154
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
30155
|
-
for (;;) {
|
|
30156
|
-
const saved = topLevel[nextTopLevel++];
|
|
30157
|
-
if (saved === void 0) return;
|
|
30158
|
-
await restoreOne(saved);
|
|
30159
|
-
}
|
|
30160
|
-
}));
|
|
30229
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
30161
30230
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
30162
30231
|
for (const saved of childRows) {
|
|
30163
30232
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -33561,6 +33630,12 @@ Object.freeze({
|
|
|
33561
33630
|
addonId: null,
|
|
33562
33631
|
access: "create"
|
|
33563
33632
|
},
|
|
33633
|
+
"pipelineAnalytics.reconcileFromDisk": {
|
|
33634
|
+
capName: "pipeline-analytics",
|
|
33635
|
+
capScope: "device",
|
|
33636
|
+
addonId: null,
|
|
33637
|
+
access: "create"
|
|
33638
|
+
},
|
|
33564
33639
|
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
33565
33640
|
capName: "pipeline-analytics",
|
|
33566
33641
|
capScope: "device",
|
|
@@ -33963,6 +34038,12 @@ Object.freeze({
|
|
|
33963
34038
|
addonId: null,
|
|
33964
34039
|
access: "view"
|
|
33965
34040
|
},
|
|
34041
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
34042
|
+
capName: "pipeline-orchestrator",
|
|
34043
|
+
capScope: "system",
|
|
34044
|
+
addonId: null,
|
|
34045
|
+
access: "view"
|
|
34046
|
+
},
|
|
33966
34047
|
"pipelineOrchestrator.listAgentSettings": {
|
|
33967
34048
|
capName: "pipeline-orchestrator",
|
|
33968
34049
|
capScope: "system",
|
|
@@ -33987,6 +34068,12 @@ Object.freeze({
|
|
|
33987
34068
|
addonId: null,
|
|
33988
34069
|
access: "create"
|
|
33989
34070
|
},
|
|
34071
|
+
"pipelineOrchestrator.reconcileFromDisk": {
|
|
34072
|
+
capName: "pipeline-orchestrator",
|
|
34073
|
+
capScope: "system",
|
|
34074
|
+
addonId: null,
|
|
34075
|
+
access: "create"
|
|
34076
|
+
},
|
|
33990
34077
|
"pipelineOrchestrator.removeAgentSettings": {
|
|
33991
34078
|
capName: "pipeline-orchestrator",
|
|
33992
34079
|
capScope: "system",
|
|
@@ -36978,6 +37065,11 @@ Object.freeze({
|
|
|
36978
37065
|
form: "single",
|
|
36979
37066
|
optional: true
|
|
36980
37067
|
}],
|
|
37068
|
+
"pipelineAnalytics.reconcileFromDisk": [{
|
|
37069
|
+
name: "deviceId",
|
|
37070
|
+
form: "single",
|
|
37071
|
+
optional: false
|
|
37072
|
+
}],
|
|
36981
37073
|
"pipelineAnalytics.restageRetrainTrack": [{
|
|
36982
37074
|
name: "deviceId",
|
|
36983
37075
|
form: "single",
|
|
@@ -40698,7 +40790,7 @@ async function pumpAlarmStream(stream, boundary, handlers, idleTimeoutMs = ALARM
|
|
|
40698
40790
|
for (const block of parts.blocks) {
|
|
40699
40791
|
const parsed = parseMultipartBlock(block);
|
|
40700
40792
|
if (!parsed) continue;
|
|
40701
|
-
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml")) {
|
|
40793
|
+
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml") || looksLikeAlertXml(parsed.bodyText)) {
|
|
40702
40794
|
const ev = parseAlertXml(parsed.bodyText);
|
|
40703
40795
|
if (ev) handlers.onEvent(ev);
|
|
40704
40796
|
}
|
|
@@ -40730,14 +40822,24 @@ function splitOnBoundary(buf, dashBoundary) {
|
|
|
40730
40822
|
tail
|
|
40731
40823
|
};
|
|
40732
40824
|
}
|
|
40825
|
+
function looksLikeAlertXml(text) {
|
|
40826
|
+
return text.includes("<eventType>") || text.includes("EventNotificationAlert");
|
|
40827
|
+
}
|
|
40733
40828
|
function parseMultipartBlock(block) {
|
|
40734
|
-
|
|
40735
|
-
|
|
40736
|
-
|
|
40737
|
-
if (headerEnd
|
|
40738
|
-
|
|
40739
|
-
|
|
40740
|
-
|
|
40829
|
+
const headerEnd = findHeaderEnd(block, 0);
|
|
40830
|
+
let headerBlob = "";
|
|
40831
|
+
let bodySlice;
|
|
40832
|
+
if (headerEnd >= 0) {
|
|
40833
|
+
headerBlob = Buffer.from(block.subarray(0, headerEnd)).toString("utf8");
|
|
40834
|
+
bodySlice = block.subarray(headerEnd + 4);
|
|
40835
|
+
} else {
|
|
40836
|
+
const raw = Buffer.from(block).toString("utf8");
|
|
40837
|
+
if (!looksLikeAlertXml(raw)) return null;
|
|
40838
|
+
return {
|
|
40839
|
+
contentType: "",
|
|
40840
|
+
bodyText: raw.replace(/\r?\n--$/m, "").trim()
|
|
40841
|
+
};
|
|
40842
|
+
}
|
|
40741
40843
|
const headers = {};
|
|
40742
40844
|
for (const line of headerBlob.split(/\r?\n/)) {
|
|
40743
40845
|
const idx = line.indexOf(":");
|
|
@@ -40753,6 +40855,28 @@ function findHeaderEnd(buf, from) {
|
|
|
40753
40855
|
for (let i = from; i + 3 < buf.length; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
|
|
40754
40856
|
return -1;
|
|
40755
40857
|
}
|
|
40858
|
+
/**
|
|
40859
|
+
* Event types that imply onboard motion. Compared after `eventType`
|
|
40860
|
+
* is lowercased. Some firmwares emit `regionExit`, others
|
|
40861
|
+
* `regionexiting` — accept both.
|
|
40862
|
+
*/
|
|
40863
|
+
function isHikvisionMotionAlarmType(type) {
|
|
40864
|
+
switch (type) {
|
|
40865
|
+
case "vmd":
|
|
40866
|
+
case "motiondetection":
|
|
40867
|
+
case "linedetection":
|
|
40868
|
+
case "fielddetection":
|
|
40869
|
+
case "regionentrance":
|
|
40870
|
+
case "regionexit":
|
|
40871
|
+
case "regionexiting":
|
|
40872
|
+
case "attendedbaggage":
|
|
40873
|
+
case "unattendedbaggage": return true;
|
|
40874
|
+
default: return false;
|
|
40875
|
+
}
|
|
40876
|
+
}
|
|
40877
|
+
function isHikvisionSmartAlarmType(type) {
|
|
40878
|
+
return type !== "vmd" && type !== "motiondetection" && isHikvisionMotionAlarmType(type);
|
|
40879
|
+
}
|
|
40756
40880
|
function parseAlertXml(xml) {
|
|
40757
40881
|
const type = extractTag(xml, "eventType");
|
|
40758
40882
|
if (!type) return null;
|
|
@@ -44413,7 +44537,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44413
44537
|
handleAlarmEvent(ev) {
|
|
44414
44538
|
const camStreamId = this.resolveAlarmCamStreamId(ev.channelId);
|
|
44415
44539
|
const isMotion = ev.type === "vmd" || ev.type === "motiondetection";
|
|
44416
|
-
const isSmart = ev.type
|
|
44540
|
+
const isSmart = isHikvisionSmartAlarmType(ev.type);
|
|
44417
44541
|
if (isMotion) {
|
|
44418
44542
|
this.handleMotionEvent(ev, camStreamId);
|
|
44419
44543
|
return;
|
|
@@ -44423,7 +44547,17 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44423
44547
|
this.handleMotionEvent(ev, camStreamId);
|
|
44424
44548
|
return;
|
|
44425
44549
|
}
|
|
44426
|
-
|
|
44550
|
+
if (ev.type === "videoloss") {
|
|
44551
|
+
this.ctx.logger.debug("hikvision: unhandled alarm event", {
|
|
44552
|
+
tags: { deviceId: this.id },
|
|
44553
|
+
meta: {
|
|
44554
|
+
type: ev.type,
|
|
44555
|
+
state: ev.state
|
|
44556
|
+
}
|
|
44557
|
+
});
|
|
44558
|
+
return;
|
|
44559
|
+
}
|
|
44560
|
+
this.ctx.logger.info("hikvision: unhandled alarm event", {
|
|
44427
44561
|
tags: { deviceId: this.id },
|
|
44428
44562
|
meta: {
|
|
44429
44563
|
type: ev.type,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-hikvision",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.27",
|
|
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",
|