@camstack/addon-provider-hikvision 1.2.25 → 1.2.28
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 +461 -100
- package/dist/addon.mjs +461 -100
- package/package.json +1 -1
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-XfKNtfCc.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
|
/**
|
|
@@ -45,6 +46,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
45
46
|
EventCategory["AddonInstalled"] = "addon.installed";
|
|
46
47
|
EventCategory["AddonUninstalled"] = "addon.uninstalled";
|
|
47
48
|
EventCategory["AddonCrashed"] = "addon.crashed";
|
|
49
|
+
/**
|
|
50
|
+
* A RUNNER the D6 crash circuit-breaker gave up on — terminal.
|
|
51
|
+
*
|
|
52
|
+
* `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
|
|
53
|
+
* respawned"); this is the one that never resolves itself. Emitted exactly
|
|
54
|
+
* once per trip, by `process-service.ts`, carrying the node, the runner, the
|
|
55
|
+
* addons it hosted and the crash count that tripped the breaker.
|
|
56
|
+
*
|
|
57
|
+
* It exists because a runner marked terminally `failed` used to be SILENT:
|
|
58
|
+
* on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
|
|
59
|
+
* errors, the breaker stopped respawning it (correctly), `/health` kept
|
|
60
|
+
* returning 200, and fleet-wide recording was gone for 3h15 before the
|
|
61
|
+
* operator's phone told him. The Notification Center's system-event intake
|
|
62
|
+
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
63
|
+
*/
|
|
64
|
+
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
48
65
|
EventCategory["AddonError"] = "addon.error";
|
|
49
66
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
50
67
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -7544,6 +7561,10 @@ var RecordingBandSchema = object({
|
|
|
7544
7561
|
preBufferSec: number().min(0).optional(),
|
|
7545
7562
|
postBufferSec: number().min(0).optional()
|
|
7546
7563
|
});
|
|
7564
|
+
({
|
|
7565
|
+
preBufferSec: 10,
|
|
7566
|
+
postBufferSec: 30
|
|
7567
|
+
}).postBufferSec * 1e3;
|
|
7547
7568
|
/**
|
|
7548
7569
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7549
7570
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7591,6 +7612,12 @@ var RecordingConfigSchema = object({
|
|
|
7591
7612
|
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7592
7613
|
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7593
7614
|
mode: RecordingStorageModeSchema.optional(),
|
|
7615
|
+
/**
|
|
7616
|
+
* Which assigned broker slots to record. Absent / empty = {@link
|
|
7617
|
+
* DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
|
|
7618
|
+
* camera's currently assigned slots — never `mid` unless the operator
|
|
7619
|
+
* picks it, and never a slot the broker has not assigned.
|
|
7620
|
+
*/
|
|
7594
7621
|
profiles: array(CamProfileSchema).optional(),
|
|
7595
7622
|
segmentSeconds: number().int().positive().optional(),
|
|
7596
7623
|
/**
|
|
@@ -7671,7 +7698,11 @@ var RelocateFootageInputSchema = object({
|
|
|
7671
7698
|
profiles: array(string()).optional(),
|
|
7672
7699
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7673
7700
|
* never allowed to starve live writers. */
|
|
7674
|
-
throttleMbps: number().min(1).max(1e3).optional()
|
|
7701
|
+
throttleMbps: number().min(1).max(1e3).optional(),
|
|
7702
|
+
/** Move only segments whose startMs is >= this. Absent = the whole source
|
|
7703
|
+
* pile. Used when a full drain is too expensive and the operator only
|
|
7704
|
+
* wants the recent window on the new disk. */
|
|
7705
|
+
sinceMs: number().int().optional()
|
|
7675
7706
|
});
|
|
7676
7707
|
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7677
7708
|
* from persistent recording settings: a migration never changes
|
|
@@ -14723,6 +14754,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14723
14754
|
"node-offline",
|
|
14724
14755
|
"node-inference-unavailable",
|
|
14725
14756
|
"detection-blind",
|
|
14757
|
+
"addon-crash-loop",
|
|
14726
14758
|
"addon-update-available",
|
|
14727
14759
|
"server-update-available",
|
|
14728
14760
|
"alarm-triggered",
|
|
@@ -14730,6 +14762,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14730
14762
|
"alarm-disarmed",
|
|
14731
14763
|
"alarm-arming",
|
|
14732
14764
|
"alarm-arm-refused",
|
|
14765
|
+
"addon-updated",
|
|
14766
|
+
"server-updated",
|
|
14767
|
+
"export-completed",
|
|
14733
14768
|
"camera-online",
|
|
14734
14769
|
"camera-offline",
|
|
14735
14770
|
"camera-disabled",
|
|
@@ -16623,13 +16658,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16623
16658
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16624
16659
|
*
|
|
16625
16660
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16661
|
+
*
|
|
16662
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16663
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16664
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16626
16665
|
*/
|
|
16627
16666
|
var TrackFlagFields = {
|
|
16628
16667
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16629
16668
|
* `'staging'`. */
|
|
16630
16669
|
markForTrain: boolean().optional(),
|
|
16631
16670
|
/** Operator marked this track for diagnostic attention. */
|
|
16632
|
-
debug: boolean().optional()
|
|
16671
|
+
debug: boolean().optional(),
|
|
16672
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16673
|
+
favourited: boolean().optional()
|
|
16633
16674
|
};
|
|
16634
16675
|
/**
|
|
16635
16676
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16654,6 +16695,7 @@ var TrackFlagsSchema = object({
|
|
|
16654
16695
|
trackId: string(),
|
|
16655
16696
|
markForTrain: boolean(),
|
|
16656
16697
|
debug: boolean(),
|
|
16698
|
+
favourited: boolean(),
|
|
16657
16699
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16658
16700
|
* a track row) because this shape is only ever produced by the write body,
|
|
16659
16701
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -17286,6 +17328,12 @@ var TrackCascadeCountsSchema = object({
|
|
|
17286
17328
|
/** Per-track CLIP search vectors removed (best-effort). */
|
|
17287
17329
|
embeddings: number().int()
|
|
17288
17330
|
});
|
|
17331
|
+
/** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
|
|
17332
|
+
var DiskReconcileCountsSchema = object({
|
|
17333
|
+
mediaDropped: number().int(),
|
|
17334
|
+
tracks: number().int(),
|
|
17335
|
+
events: number().int()
|
|
17336
|
+
});
|
|
17289
17337
|
/** Event-store footprint for one camera. */
|
|
17290
17338
|
var EventStoreDeviceFootprintSchema = object({
|
|
17291
17339
|
deviceId: number(),
|
|
@@ -17462,6 +17510,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17462
17510
|
}), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
|
|
17463
17511
|
kind: "mutation",
|
|
17464
17512
|
auth: "admin"
|
|
17513
|
+
}), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
|
|
17514
|
+
kind: "mutation",
|
|
17515
|
+
auth: "admin"
|
|
17465
17516
|
}), method(object({
|
|
17466
17517
|
deviceId: number(),
|
|
17467
17518
|
trackIds: array(string()).min(1)
|
|
@@ -19012,6 +19063,24 @@ var CameraStatusDegradationSchema = object({
|
|
|
19012
19063
|
*
|
|
19013
19064
|
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
19014
19065
|
*/
|
|
19066
|
+
var DiskReconcileJobSchema = object({
|
|
19067
|
+
state: _enum([
|
|
19068
|
+
"idle",
|
|
19069
|
+
"running",
|
|
19070
|
+
"done",
|
|
19071
|
+
"error"
|
|
19072
|
+
]),
|
|
19073
|
+
total: number().int().nonnegative(),
|
|
19074
|
+
completed: number().int().nonnegative(),
|
|
19075
|
+
currentDeviceId: number().int().nullable(),
|
|
19076
|
+
failed: array(number().int()).readonly(),
|
|
19077
|
+
mediaDropped: number().int().nonnegative(),
|
|
19078
|
+
tracks: number().int().nonnegative(),
|
|
19079
|
+
events: number().int().nonnegative(),
|
|
19080
|
+
startedAtMs: number().int().nullable(),
|
|
19081
|
+
finishedAtMs: number().int().nullable(),
|
|
19082
|
+
error: string().nullable()
|
|
19083
|
+
});
|
|
19015
19084
|
var CameraStatusSchema = object({
|
|
19016
19085
|
deviceId: number(),
|
|
19017
19086
|
assignment: CameraAssignmentStatusSchema,
|
|
@@ -19243,7 +19312,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
|
19243
19312
|
}), CameraSwitchGroupSchema, {
|
|
19244
19313
|
kind: "mutation",
|
|
19245
19314
|
auth: "admin"
|
|
19246
|
-
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(),
|
|
19315
|
+
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
|
|
19316
|
+
kind: "mutation",
|
|
19317
|
+
auth: "admin"
|
|
19318
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
|
|
19247
19319
|
name: string(),
|
|
19248
19320
|
description: string().optional(),
|
|
19249
19321
|
config: CameraPipelineConfigSchema
|
|
@@ -20420,20 +20492,6 @@ var VectorStatsResultSchema = object({
|
|
|
20420
20492
|
exact: boolean()
|
|
20421
20493
|
});
|
|
20422
20494
|
method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
|
|
20423
|
-
/**
|
|
20424
|
-
* `videoclips` — the unified, navigable-clip surface for a camera.
|
|
20425
|
-
*
|
|
20426
|
-
* A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
|
|
20427
|
-
* provider per device, substitutable. The DEFAULT provider (registered by
|
|
20428
|
-
* `addon-post-analysis`, `defaultActive: true`) composes the analytics event
|
|
20429
|
-
* log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
|
|
20430
|
-
* is a time-WINDOW over existing footage, never a separate file. A camera that
|
|
20431
|
-
* exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
|
|
20432
|
-
* wrapper provider for its device and serve its own clip catalog + URLs.
|
|
20433
|
-
*
|
|
20434
|
-
* A `Clip` is purely time-based (subtree-blind): playback resolves segments by
|
|
20435
|
-
* temporal overlap, so the API never decides `continuous` vs `events`.
|
|
20436
|
-
*/
|
|
20437
20495
|
var ClipSchema = object({
|
|
20438
20496
|
/** Opaque, provider-namespaced id. The default provider encodes the time
|
|
20439
20497
|
* window so `getClipPlayback` is self-contained (no event re-query). */
|
|
@@ -20450,8 +20508,57 @@ var ClipSchema = object({
|
|
|
20450
20508
|
startMs: number(),
|
|
20451
20509
|
endMs: number()
|
|
20452
20510
|
}),
|
|
20453
|
-
/**
|
|
20454
|
-
thumbnail
|
|
20511
|
+
/**
|
|
20512
|
+
* Lazy thumbnail URL, never inlined.
|
|
20513
|
+
*
|
|
20514
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20515
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20516
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20517
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20518
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20519
|
+
* mint their own stills.
|
|
20520
|
+
*
|
|
20521
|
+
* **VOUCHED, never fabricated.** A provider emits this only for an event it
|
|
20522
|
+
* has CONFIRMED owns at least one media row (its own, or its owning track's).
|
|
20523
|
+
* Absent is meaningful — "this visit has no event still" — never "we did not
|
|
20524
|
+
* look". Stamping it from a URL template made 35% of one camera's clips point
|
|
20525
|
+
* at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
|
|
20526
|
+
* A read that FAILS drops the claim; it never invents it.
|
|
20527
|
+
*/
|
|
20528
|
+
thumbnail: string().optional(),
|
|
20529
|
+
/**
|
|
20530
|
+
* An instant INSIDE this visit's footage, hole-safe, where a recorded still
|
|
20531
|
+
* can be decoded. Present whenever the visit came from recorded availability;
|
|
20532
|
+
* absent on a per-event padded window (there is no footage to promise).
|
|
20533
|
+
*
|
|
20534
|
+
* This is not a thumbnail and not a second byte path: it is the argument to
|
|
20535
|
+
* the recorder's existing still route. The surface — never the provider —
|
|
20536
|
+
* decides whether to use it. It is the midpoint of the visit's LONGEST
|
|
20537
|
+
* contiguous range, not of the visit: a visit spans its holes by
|
|
20538
|
+
* construction, so a naive midpoint lands in dead air.
|
|
20539
|
+
*/
|
|
20540
|
+
stillAtMs: number().optional(),
|
|
20541
|
+
/**
|
|
20542
|
+
* Analytics event ids that overlap this visit — a BOUNDED sample, newest
|
|
20543
|
+
* first within kind (object → motion → audio), capped at
|
|
20544
|
+
* {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
|
|
20545
|
+
*
|
|
20546
|
+
* Bounded because it is not a payload the surface pages through: one visit on
|
|
20547
|
+
* device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
|
|
20548
|
+
* camera-day. Read {@link eventCount} for the true total.
|
|
20549
|
+
*/
|
|
20550
|
+
eventIds: array(string()).optional(),
|
|
20551
|
+
/** How many analytics events actually overlap this visit. Differs from
|
|
20552
|
+
* `eventIds.length` exactly when the sample was capped — so a truncated
|
|
20553
|
+
* list is never mistaken for a quiet visit. */
|
|
20554
|
+
eventCount: number().int().nonnegative().optional(),
|
|
20555
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20556
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20557
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20558
|
+
holes: array(object({
|
|
20559
|
+
startMs: number(),
|
|
20560
|
+
endMs: number()
|
|
20561
|
+
})).optional()
|
|
20455
20562
|
});
|
|
20456
20563
|
var ClipPlaybackSchema = object({
|
|
20457
20564
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -20915,7 +21022,14 @@ var SearchResultSchema = object({
|
|
|
20915
21022
|
});
|
|
20916
21023
|
var AutoUpdateSettingsSchema = object({
|
|
20917
21024
|
channel: ChannelSchema,
|
|
20918
|
-
intervalSeconds: number()
|
|
21025
|
+
intervalSeconds: number(),
|
|
21026
|
+
/**
|
|
21027
|
+
* Cadence of the "an update exists" POLLER, in seconds. Independent of
|
|
21028
|
+
* `channel`: the poller runs while auto-apply is `off`, because being told
|
|
21029
|
+
* about a publish and installing it are different decisions. Clamped
|
|
21030
|
+
* server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
|
|
21031
|
+
*/
|
|
21032
|
+
updateCheckIntervalSeconds: number()
|
|
20919
21033
|
});
|
|
20920
21034
|
var AddonAutoUpdateSchema = ChannelWithInheritSchema;
|
|
20921
21035
|
var RestartAddonResultSchema = unknown();
|
|
@@ -21056,7 +21170,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
|
|
|
21056
21170
|
auth: "admin"
|
|
21057
21171
|
}), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
|
|
21058
21172
|
channel: ChannelSchema,
|
|
21059
|
-
intervalSeconds: number().min(300).max(86400).optional()
|
|
21173
|
+
intervalSeconds: number().min(300).max(86400).optional(),
|
|
21174
|
+
/** Availability-poll cadence; see AutoUpdateSettingsSchema. */
|
|
21175
|
+
updateCheckIntervalSeconds: number().min(900).max(604800).optional()
|
|
21060
21176
|
}), unknown(), {
|
|
21061
21177
|
kind: "mutation",
|
|
21062
21178
|
auth: "admin"
|
|
@@ -26870,7 +26986,9 @@ var ExportOptionsSchema = object({
|
|
|
26870
26986
|
includeAudio: boolean(),
|
|
26871
26987
|
maxLifeMs: number().int().positive(),
|
|
26872
26988
|
deleteAfterDownload: boolean(),
|
|
26873
|
-
title: string().max(200).optional()
|
|
26989
|
+
title: string().max(200).optional(),
|
|
26990
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26991
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26874
26992
|
}).superRefine((v, ctx) => {
|
|
26875
26993
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26876
26994
|
code: ZodIssueCode.custom,
|
|
@@ -26929,10 +27047,18 @@ var ExportBytesSchema = object({
|
|
|
26929
27047
|
});
|
|
26930
27048
|
method(object({
|
|
26931
27049
|
deviceId: number(),
|
|
26932
|
-
|
|
27050
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
27051
|
+
profile: string().optional(),
|
|
27052
|
+
profiles: array(string()).min(1).optional(),
|
|
26933
27053
|
fromMs: number(),
|
|
26934
27054
|
toMs: number(),
|
|
26935
27055
|
options: ExportOptionsSchema
|
|
27056
|
+
}).superRefine((v, ctx) => {
|
|
27057
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
27058
|
+
code: ZodIssueCode.custom,
|
|
27059
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
27060
|
+
path: ["profiles"]
|
|
27061
|
+
});
|
|
26936
27062
|
}), ExportRecordSchema, {
|
|
26937
27063
|
kind: "mutation",
|
|
26938
27064
|
auth: "protected"
|
|
@@ -30087,15 +30213,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30087
30213
|
labels: ["probe not implemented"]
|
|
30088
30214
|
};
|
|
30089
30215
|
}
|
|
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
30216
|
async restoreDevices(savedDevices) {
|
|
30100
30217
|
await this.onRestoreDevices(savedDevices);
|
|
30101
30218
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -30150,14 +30267,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30150
30267
|
});
|
|
30151
30268
|
}
|
|
30152
30269
|
};
|
|
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
|
-
}));
|
|
30270
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
30161
30271
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
30162
30272
|
for (const saved of childRows) {
|
|
30163
30273
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -33561,6 +33671,12 @@ Object.freeze({
|
|
|
33561
33671
|
addonId: null,
|
|
33562
33672
|
access: "create"
|
|
33563
33673
|
},
|
|
33674
|
+
"pipelineAnalytics.reconcileFromDisk": {
|
|
33675
|
+
capName: "pipeline-analytics",
|
|
33676
|
+
capScope: "device",
|
|
33677
|
+
addonId: null,
|
|
33678
|
+
access: "create"
|
|
33679
|
+
},
|
|
33564
33680
|
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
33565
33681
|
capName: "pipeline-analytics",
|
|
33566
33682
|
capScope: "device",
|
|
@@ -33963,6 +34079,12 @@ Object.freeze({
|
|
|
33963
34079
|
addonId: null,
|
|
33964
34080
|
access: "view"
|
|
33965
34081
|
},
|
|
34082
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
34083
|
+
capName: "pipeline-orchestrator",
|
|
34084
|
+
capScope: "system",
|
|
34085
|
+
addonId: null,
|
|
34086
|
+
access: "view"
|
|
34087
|
+
},
|
|
33966
34088
|
"pipelineOrchestrator.listAgentSettings": {
|
|
33967
34089
|
capName: "pipeline-orchestrator",
|
|
33968
34090
|
capScope: "system",
|
|
@@ -33987,6 +34109,12 @@ Object.freeze({
|
|
|
33987
34109
|
addonId: null,
|
|
33988
34110
|
access: "create"
|
|
33989
34111
|
},
|
|
34112
|
+
"pipelineOrchestrator.reconcileFromDisk": {
|
|
34113
|
+
capName: "pipeline-orchestrator",
|
|
34114
|
+
capScope: "system",
|
|
34115
|
+
addonId: null,
|
|
34116
|
+
access: "create"
|
|
34117
|
+
},
|
|
33990
34118
|
"pipelineOrchestrator.removeAgentSettings": {
|
|
33991
34119
|
capName: "pipeline-orchestrator",
|
|
33992
34120
|
capScope: "system",
|
|
@@ -36978,6 +37106,11 @@ Object.freeze({
|
|
|
36978
37106
|
form: "single",
|
|
36979
37107
|
optional: true
|
|
36980
37108
|
}],
|
|
37109
|
+
"pipelineAnalytics.reconcileFromDisk": [{
|
|
37110
|
+
name: "deviceId",
|
|
37111
|
+
form: "single",
|
|
37112
|
+
optional: false
|
|
37113
|
+
}],
|
|
36981
37114
|
"pipelineAnalytics.restageRetrainTrack": [{
|
|
36982
37115
|
name: "deviceId",
|
|
36983
37116
|
form: "single",
|
|
@@ -38043,6 +38176,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
|
38043
38176
|
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
38044
38177
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
38045
38178
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
38179
|
+
var MB = 1024 * 1024;
|
|
38180
|
+
1024 * MB, 3072 * MB;
|
|
38046
38181
|
//#endregion
|
|
38047
38182
|
//#region src/accessories/base.ts
|
|
38048
38183
|
/**
|
|
@@ -40605,36 +40740,87 @@ function parseTwoWayAudioChannels(xml) {
|
|
|
40605
40740
|
return out;
|
|
40606
40741
|
}
|
|
40607
40742
|
/**
|
|
40608
|
-
*
|
|
40609
|
-
*
|
|
40610
|
-
*
|
|
40611
|
-
*
|
|
40612
|
-
*
|
|
40613
|
-
*
|
|
40614
|
-
*
|
|
40615
|
-
*
|
|
40616
|
-
|
|
40617
|
-
|
|
40618
|
-
|
|
40619
|
-
*
|
|
40620
|
-
*
|
|
40621
|
-
*
|
|
40622
|
-
* the
|
|
40623
|
-
*
|
|
40624
|
-
*
|
|
40625
|
-
*
|
|
40626
|
-
*
|
|
40627
|
-
|
|
40628
|
-
|
|
40629
|
-
|
|
40630
|
-
|
|
40631
|
-
|
|
40632
|
-
|
|
40633
|
-
|
|
40634
|
-
|
|
40635
|
-
|
|
40636
|
-
|
|
40637
|
-
|
|
40743
|
+
* Why the parser adopts the boundary it SEES rather than the one declared.
|
|
40744
|
+
*
|
|
40745
|
+
* Five cameras on `C-R-*` firmware (I91DN/I91DD V5.7.1, I91EV V5.8.10) have
|
|
40746
|
+
* never delivered one motion event here, while the same cameras deliver real
|
|
40747
|
+
* hardware VMD on the same endpoint to another NVR stack. The one dimension
|
|
40748
|
+
* where that stack is more permissive is the multipart boundary: it accepts a
|
|
40749
|
+
* part framed with the declared `--<boundary>` OR with the literal constant
|
|
40750
|
+
* `--boundary`, because "older hikvision send a boundary in the headers, but
|
|
40751
|
+
* then use a totally different constant boundary value".
|
|
40752
|
+
*
|
|
40753
|
+
* We take the general form instead of that special case: if the DECLARED
|
|
40754
|
+
* token has never once matched, adopt the token from the first `--…` line
|
|
40755
|
+
* actually on the wire and use it from then on. It subsumes the literal
|
|
40756
|
+
* `--boundary` case (that IS the observed token on those cameras) and it also
|
|
40757
|
+
* covers the next firmware that invents a third constant, which a hard-coded
|
|
40758
|
+
* fallback would not. The risk is bounded on both sides: adoption is only ever
|
|
40759
|
+
* attempted while the declared token has produced ZERO parts, the candidate
|
|
40760
|
+
* must occupy a whole line of RFC 2046 boundary characters, and the closing
|
|
40761
|
+
* delimiter (`--<token>--`) is rejected.
|
|
40762
|
+
*/
|
|
40763
|
+
function detectWireBoundary(buf) {
|
|
40764
|
+
const text = Buffer.from(buf).toString("binary");
|
|
40765
|
+
const lineRe = /(?:^|\r?\n)--([0-9A-Za-z'()+_,./:=?-]{1,70})\r?\n/g;
|
|
40766
|
+
for (;;) {
|
|
40767
|
+
const m = lineRe.exec(text);
|
|
40768
|
+
if (!m) return null;
|
|
40769
|
+
const token = m[1];
|
|
40770
|
+
if (token.endsWith("--")) continue;
|
|
40771
|
+
return token;
|
|
40772
|
+
}
|
|
40773
|
+
}
|
|
40774
|
+
/**
|
|
40775
|
+
* First occurrence of each kind is logged immediately; further occurrences are
|
|
40776
|
+
* counted and folded into the next line after `intervalMs`. A stream that
|
|
40777
|
+
* drops every part therefore costs one line per minute per kind, not one per
|
|
40778
|
+
* chunk.
|
|
40779
|
+
*/
|
|
40780
|
+
function createAlarmDiagnostics(logger, deviceId, intervalMs) {
|
|
40781
|
+
const lastLoggedAt = /* @__PURE__ */ new Map();
|
|
40782
|
+
const suppressed = /* @__PURE__ */ new Map();
|
|
40783
|
+
const report = (kind, message, meta, level = "warn") => {
|
|
40784
|
+
if (!logger) return;
|
|
40785
|
+
const now = Date.now();
|
|
40786
|
+
const last = lastLoggedAt.get(kind);
|
|
40787
|
+
if (last !== void 0 && now - last < intervalMs) {
|
|
40788
|
+
suppressed.set(kind, (suppressed.get(kind) ?? 0) + 1);
|
|
40789
|
+
return;
|
|
40790
|
+
}
|
|
40791
|
+
const since = suppressed.get(kind) ?? 0;
|
|
40792
|
+
suppressed.set(kind, 0);
|
|
40793
|
+
lastLoggedAt.set(kind, now);
|
|
40794
|
+
const extras = {
|
|
40795
|
+
...deviceId === null ? {} : { tags: { deviceId } },
|
|
40796
|
+
meta: {
|
|
40797
|
+
kind,
|
|
40798
|
+
...meta,
|
|
40799
|
+
...since > 0 ? { suppressedSincePrevious: since } : {}
|
|
40800
|
+
}
|
|
40801
|
+
};
|
|
40802
|
+
if (level === "debug") logger.debug(message, extras);
|
|
40803
|
+
else logger.warn(message, extras);
|
|
40804
|
+
};
|
|
40805
|
+
return { report };
|
|
40806
|
+
}
|
|
40807
|
+
/** First line of a buffer, clipped — the single most useful field when asking
|
|
40808
|
+
* "what is this camera actually sending?". */
|
|
40809
|
+
function firstLineOf(buf, maxChars = 120) {
|
|
40810
|
+
const line = Buffer.from(buf.subarray(0, 1024)).toString("utf8").split(/\r?\n/, 1)[0] ?? "";
|
|
40811
|
+
return line.length > maxChars ? `${line.slice(0, maxChars)}…` : line;
|
|
40812
|
+
}
|
|
40813
|
+
/** First whole line that starts with `--` — i.e. the framing the camera really
|
|
40814
|
+
* uses. `null` when the buffer contains no such line. */
|
|
40815
|
+
function firstDashLineOf(buf, maxChars = 120) {
|
|
40816
|
+
const text = Buffer.from(buf.subarray(0, 4096)).toString("utf8");
|
|
40817
|
+
for (const line of text.split(/\r?\n/)) {
|
|
40818
|
+
if (!line.startsWith("--")) continue;
|
|
40819
|
+
return line.length > maxChars ? `${line.slice(0, maxChars)}…` : line;
|
|
40820
|
+
}
|
|
40821
|
+
return null;
|
|
40822
|
+
}
|
|
40823
|
+
function subscribeAlarms(client, handlers, options = {}) {
|
|
40638
40824
|
const controller = new AbortController();
|
|
40639
40825
|
let settled = false;
|
|
40640
40826
|
const fail = (err) => {
|
|
@@ -40661,7 +40847,7 @@ function subscribeAlarms(client, handlers, idleTimeoutMs = ALARM_STREAM_IDLE_TIM
|
|
|
40661
40847
|
return;
|
|
40662
40848
|
}
|
|
40663
40849
|
handlers.onConnected?.();
|
|
40664
|
-
await pumpAlarmStream(res.body, boundary, handlers,
|
|
40850
|
+
await pumpAlarmStream(res.body, boundary, handlers, options);
|
|
40665
40851
|
fail(/* @__PURE__ */ new Error("alarm stream ended"));
|
|
40666
40852
|
} catch (err) {
|
|
40667
40853
|
fail(err);
|
|
@@ -40673,10 +40859,65 @@ function parseBoundary(contentType) {
|
|
|
40673
40859
|
const m = /boundary\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
|
|
40674
40860
|
return m ? m[1].trim() : null;
|
|
40675
40861
|
}
|
|
40676
|
-
async function pumpAlarmStream(stream,
|
|
40862
|
+
async function pumpAlarmStream(stream, declaredBoundary, handlers, options) {
|
|
40863
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? 3e4;
|
|
40864
|
+
const maxBufferBytes = options.maxBufferBytes ?? 1048576;
|
|
40865
|
+
const retainBytes = options.retainBytes ?? 65536;
|
|
40866
|
+
const unparsedWarnBytes = options.unparsedWarnBytes ?? 32768;
|
|
40867
|
+
const diagnostics = createAlarmDiagnostics(options.logger ?? null, options.deviceId ?? null, options.diagnosticIntervalMs ?? 6e4);
|
|
40677
40868
|
const reader = stream.getReader();
|
|
40678
|
-
|
|
40869
|
+
let activeBoundary = declaredBoundary;
|
|
40870
|
+
let activeDashBoundary = `--${activeBoundary}`;
|
|
40871
|
+
/** Has the ACTIVE token ever framed anything? Gates boundary adoption. */
|
|
40872
|
+
let matchedAnyBoundary = false;
|
|
40873
|
+
/**
|
|
40874
|
+
* Is the fragment that precedes the next boundary junk?
|
|
40875
|
+
*
|
|
40876
|
+
* True only at the head of the stream (RFC 1521 preamble), after we already
|
|
40877
|
+
* emitted the bytes sitting in the buffer, and after the overflow cap cut
|
|
40878
|
+
* the buffer mid-part. It used to be true unconditionally, which meant every
|
|
40879
|
+
* split after the first DISCARDED a completed part — on a stream that
|
|
40880
|
+
* delivers one part per TCP read, that is every event the camera ever sent.
|
|
40881
|
+
*/
|
|
40882
|
+
let dropLeadingFragment = true;
|
|
40679
40883
|
let bufferedBytes = new Uint8Array(0);
|
|
40884
|
+
const dispatchBlock = (block) => {
|
|
40885
|
+
const parsed = parseMultipartBlock(block);
|
|
40886
|
+
if (!parsed.ok) {
|
|
40887
|
+
diagnostics.report("part-unparseable", "hikvision alarm stream: multipart part dropped", {
|
|
40888
|
+
reason: parsed.reason,
|
|
40889
|
+
bytes: block.byteLength,
|
|
40890
|
+
firstLine: firstLineOf(block)
|
|
40891
|
+
});
|
|
40892
|
+
return;
|
|
40893
|
+
}
|
|
40894
|
+
const { contentType, bodyText } = parsed.block;
|
|
40895
|
+
if (!(contentType.startsWith("application/xml") || contentType.startsWith("text/xml") || looksLikeAlertXml(bodyText))) {
|
|
40896
|
+
if (contentType.startsWith("image/")) {
|
|
40897
|
+
diagnostics.report("part-image-ignored", "hikvision alarm stream: image part ignored (not forwarded yet)", {
|
|
40898
|
+
contentType,
|
|
40899
|
+
bytes: block.byteLength
|
|
40900
|
+
}, "debug");
|
|
40901
|
+
return;
|
|
40902
|
+
}
|
|
40903
|
+
diagnostics.report("part-not-alert-xml", "hikvision alarm stream: part dropped — neither an xml Content-Type nor alert-shaped XML", {
|
|
40904
|
+
contentType,
|
|
40905
|
+
bytes: block.byteLength,
|
|
40906
|
+
bodyPreview: bodyText.slice(0, 120)
|
|
40907
|
+
});
|
|
40908
|
+
return;
|
|
40909
|
+
}
|
|
40910
|
+
const ev = parseAlertXml(bodyText);
|
|
40911
|
+
if (!ev) {
|
|
40912
|
+
diagnostics.report("alert-xml-without-event-type", "hikvision alarm stream: alert part dropped — no <eventType> tag", {
|
|
40913
|
+
contentType,
|
|
40914
|
+
bytes: block.byteLength,
|
|
40915
|
+
bodyPreview: bodyText.slice(0, 120)
|
|
40916
|
+
});
|
|
40917
|
+
return;
|
|
40918
|
+
}
|
|
40919
|
+
handlers.onEvent(ev);
|
|
40920
|
+
};
|
|
40680
40921
|
let idleTimer = null;
|
|
40681
40922
|
const armIdle = () => {
|
|
40682
40923
|
if (idleTimeoutMs <= 0) return;
|
|
@@ -40693,16 +40934,57 @@ async function pumpAlarmStream(stream, boundary, handlers, idleTimeoutMs = ALARM
|
|
|
40693
40934
|
armIdle();
|
|
40694
40935
|
if (!value || value.byteLength === 0) continue;
|
|
40695
40936
|
bufferedBytes = concat(bufferedBytes, value);
|
|
40696
|
-
|
|
40697
|
-
|
|
40698
|
-
|
|
40699
|
-
|
|
40700
|
-
|
|
40701
|
-
|
|
40702
|
-
|
|
40703
|
-
|
|
40937
|
+
let split = splitOnBoundary(bufferedBytes, activeDashBoundary, dropLeadingFragment);
|
|
40938
|
+
if (!split.sawBoundary && !matchedAnyBoundary) {
|
|
40939
|
+
const observed = detectWireBoundary(bufferedBytes);
|
|
40940
|
+
if (observed !== null && observed !== activeBoundary) {
|
|
40941
|
+
diagnostics.report("boundary-adopted", "hikvision alarm stream: declared multipart boundary never matched — adopting the boundary seen on the wire", {
|
|
40942
|
+
declaredBoundary,
|
|
40943
|
+
observedBoundary: observed,
|
|
40944
|
+
bufferedBytes: bufferedBytes.byteLength,
|
|
40945
|
+
firstDashLine: firstDashLineOf(bufferedBytes),
|
|
40946
|
+
firstLine: firstLineOf(bufferedBytes)
|
|
40947
|
+
});
|
|
40948
|
+
activeBoundary = observed;
|
|
40949
|
+
activeDashBoundary = `--${observed}`;
|
|
40950
|
+
split = splitOnBoundary(bufferedBytes, activeDashBoundary, dropLeadingFragment);
|
|
40951
|
+
}
|
|
40952
|
+
}
|
|
40953
|
+
if (split.sawBoundary) {
|
|
40954
|
+
matchedAnyBoundary = true;
|
|
40955
|
+
dropLeadingFragment = false;
|
|
40956
|
+
}
|
|
40957
|
+
bufferedBytes = split.tail;
|
|
40958
|
+
for (const block of split.blocks) dispatchBlock(block);
|
|
40959
|
+
if (!dropLeadingFragment) {
|
|
40960
|
+
const early = takeCompletePart(bufferedBytes);
|
|
40961
|
+
if (early) {
|
|
40962
|
+
dispatchBlock(early.block);
|
|
40963
|
+
bufferedBytes = early.rest;
|
|
40964
|
+
dropLeadingFragment = true;
|
|
40704
40965
|
}
|
|
40705
40966
|
}
|
|
40967
|
+
if (!matchedAnyBoundary && bufferedBytes.byteLength >= unparsedWarnBytes) diagnostics.report("boundary-never-matched", "hikvision alarm stream: no multipart boundary found in the buffered bytes — every part is being dropped", {
|
|
40968
|
+
declaredBoundary,
|
|
40969
|
+
activeBoundary,
|
|
40970
|
+
bufferedBytes: bufferedBytes.byteLength,
|
|
40971
|
+
firstDashLine: firstDashLineOf(bufferedBytes),
|
|
40972
|
+
firstLine: firstLineOf(bufferedBytes)
|
|
40973
|
+
});
|
|
40974
|
+
if (bufferedBytes.byteLength > maxBufferBytes) {
|
|
40975
|
+
diagnostics.report("buffer-overflow", "hikvision alarm stream: unparsed multipart buffer hit its cap — discarding the head", {
|
|
40976
|
+
declaredBoundary,
|
|
40977
|
+
activeBoundary,
|
|
40978
|
+
matchedAnyBoundary,
|
|
40979
|
+
bufferedBytes: bufferedBytes.byteLength,
|
|
40980
|
+
maxBufferBytes,
|
|
40981
|
+
retainBytes,
|
|
40982
|
+
firstDashLine: firstDashLineOf(bufferedBytes),
|
|
40983
|
+
firstLine: firstLineOf(bufferedBytes)
|
|
40984
|
+
});
|
|
40985
|
+
bufferedBytes = bufferedBytes.slice(bufferedBytes.byteLength - retainBytes);
|
|
40986
|
+
dropLeadingFragment = true;
|
|
40987
|
+
}
|
|
40706
40988
|
}
|
|
40707
40989
|
} finally {
|
|
40708
40990
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -40717,27 +40999,68 @@ function concat(a, b) {
|
|
|
40717
40999
|
out.set(b, a.byteLength);
|
|
40718
41000
|
return out;
|
|
40719
41001
|
}
|
|
40720
|
-
function splitOnBoundary(buf, dashBoundary) {
|
|
41002
|
+
function splitOnBoundary(buf, dashBoundary, dropLeadingFragment) {
|
|
40721
41003
|
const parts = Buffer.from(buf).toString("binary").split(dashBoundary);
|
|
40722
41004
|
if (parts.length <= 1) return {
|
|
40723
41005
|
blocks: [],
|
|
40724
|
-
tail: buf
|
|
41006
|
+
tail: buf,
|
|
41007
|
+
sawBoundary: false
|
|
40725
41008
|
};
|
|
40726
41009
|
const tail = Buffer.from(parts.pop(), "binary");
|
|
40727
|
-
parts.shift();
|
|
41010
|
+
if (dropLeadingFragment) parts.shift();
|
|
40728
41011
|
return {
|
|
40729
41012
|
blocks: parts.filter((p) => p.length > 0 && !p.startsWith("--")).map((p) => Buffer.from(p, "binary")),
|
|
40730
|
-
tail
|
|
41013
|
+
tail,
|
|
41014
|
+
sawBoundary: true
|
|
40731
41015
|
};
|
|
40732
41016
|
}
|
|
40733
|
-
function
|
|
40734
|
-
|
|
40735
|
-
|
|
40736
|
-
const
|
|
41017
|
+
function parseContentLength(headerBlob) {
|
|
41018
|
+
const m = /^content-length\s*:\s*(\d+)\s*$/im.exec(headerBlob);
|
|
41019
|
+
if (!m) return null;
|
|
41020
|
+
const n = Number.parseInt(m[1], 10);
|
|
41021
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
41022
|
+
}
|
|
41023
|
+
/**
|
|
41024
|
+
* If `buf` (which must start immediately after a boundary) already holds a
|
|
41025
|
+
* whole part per its `Content-Length`, return it and the leftover bytes.
|
|
41026
|
+
* Firmware that omits `Content-Length` falls back to boundary-framed delivery.
|
|
41027
|
+
*/
|
|
41028
|
+
function takeCompletePart(buf) {
|
|
41029
|
+
const headerEnd = findHeaderEnd(buf, 0);
|
|
40737
41030
|
if (headerEnd < 0) return null;
|
|
40738
|
-
const
|
|
40739
|
-
|
|
40740
|
-
const
|
|
41031
|
+
const contentLength = parseContentLength(Buffer.from(buf.subarray(0, headerEnd)).toString("utf8"));
|
|
41032
|
+
if (contentLength === null) return null;
|
|
41033
|
+
const end = headerEnd + 4 + contentLength;
|
|
41034
|
+
if (buf.byteLength < end) return null;
|
|
41035
|
+
return {
|
|
41036
|
+
block: buf.slice(0, end),
|
|
41037
|
+
rest: buf.slice(end)
|
|
41038
|
+
};
|
|
41039
|
+
}
|
|
41040
|
+
function looksLikeAlertXml(text) {
|
|
41041
|
+
return text.includes("<eventType>") || text.includes("EventNotificationAlert");
|
|
41042
|
+
}
|
|
41043
|
+
function parseMultipartBlock(block) {
|
|
41044
|
+
const headerEnd = findHeaderEnd(block, 0);
|
|
41045
|
+
let headerBlob = "";
|
|
41046
|
+
let bodySlice;
|
|
41047
|
+
if (headerEnd >= 0) {
|
|
41048
|
+
headerBlob = Buffer.from(block.subarray(0, headerEnd)).toString("utf8");
|
|
41049
|
+
bodySlice = block.subarray(headerEnd + 4);
|
|
41050
|
+
} else {
|
|
41051
|
+
const raw = Buffer.from(block).toString("utf8");
|
|
41052
|
+
if (!looksLikeAlertXml(raw)) return {
|
|
41053
|
+
ok: false,
|
|
41054
|
+
reason: "no-header-separator-and-not-alert-xml"
|
|
41055
|
+
};
|
|
41056
|
+
return {
|
|
41057
|
+
ok: true,
|
|
41058
|
+
block: {
|
|
41059
|
+
contentType: "",
|
|
41060
|
+
bodyText: raw.replace(/\r?\n--$/m, "").trim()
|
|
41061
|
+
}
|
|
41062
|
+
};
|
|
41063
|
+
}
|
|
40741
41064
|
const headers = {};
|
|
40742
41065
|
for (const line of headerBlob.split(/\r?\n/)) {
|
|
40743
41066
|
const idx = line.indexOf(":");
|
|
@@ -40745,14 +41068,39 @@ function parseMultipartBlock(block) {
|
|
|
40745
41068
|
headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
|
|
40746
41069
|
}
|
|
40747
41070
|
return {
|
|
40748
|
-
|
|
40749
|
-
|
|
41071
|
+
ok: true,
|
|
41072
|
+
block: {
|
|
41073
|
+
contentType: headers["content-type"] ?? "",
|
|
41074
|
+
bodyText: Buffer.from(bodySlice).toString("utf8").replace(/\r?\n--$/m, "").trim()
|
|
41075
|
+
}
|
|
40750
41076
|
};
|
|
40751
41077
|
}
|
|
40752
41078
|
function findHeaderEnd(buf, from) {
|
|
40753
41079
|
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
41080
|
return -1;
|
|
40755
41081
|
}
|
|
41082
|
+
/**
|
|
41083
|
+
* Event types that imply onboard motion. Compared after `eventType`
|
|
41084
|
+
* is lowercased. Some firmwares emit `regionExit`, others
|
|
41085
|
+
* `regionexiting` — accept both.
|
|
41086
|
+
*/
|
|
41087
|
+
function isHikvisionMotionAlarmType(type) {
|
|
41088
|
+
switch (type) {
|
|
41089
|
+
case "vmd":
|
|
41090
|
+
case "motiondetection":
|
|
41091
|
+
case "linedetection":
|
|
41092
|
+
case "fielddetection":
|
|
41093
|
+
case "regionentrance":
|
|
41094
|
+
case "regionexit":
|
|
41095
|
+
case "regionexiting":
|
|
41096
|
+
case "attendedbaggage":
|
|
41097
|
+
case "unattendedbaggage": return true;
|
|
41098
|
+
default: return false;
|
|
41099
|
+
}
|
|
41100
|
+
}
|
|
41101
|
+
function isHikvisionSmartAlarmType(type) {
|
|
41102
|
+
return type !== "vmd" && type !== "motiondetection" && isHikvisionMotionAlarmType(type);
|
|
41103
|
+
}
|
|
40756
41104
|
function parseAlertXml(xml) {
|
|
40757
41105
|
const type = extractTag(xml, "eventType");
|
|
40758
41106
|
if (!type) return null;
|
|
@@ -44399,6 +44747,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44399
44747
|
this.alarmController = null;
|
|
44400
44748
|
this.scheduleAlarmReconnect();
|
|
44401
44749
|
}
|
|
44750
|
+
}, {
|
|
44751
|
+
logger: this.ctx.logger,
|
|
44752
|
+
deviceId: this.id
|
|
44402
44753
|
});
|
|
44403
44754
|
}
|
|
44404
44755
|
scheduleAlarmReconnect() {
|
|
@@ -44413,7 +44764,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44413
44764
|
handleAlarmEvent(ev) {
|
|
44414
44765
|
const camStreamId = this.resolveAlarmCamStreamId(ev.channelId);
|
|
44415
44766
|
const isMotion = ev.type === "vmd" || ev.type === "motiondetection";
|
|
44416
|
-
const isSmart = ev.type
|
|
44767
|
+
const isSmart = isHikvisionSmartAlarmType(ev.type);
|
|
44417
44768
|
if (isMotion) {
|
|
44418
44769
|
this.handleMotionEvent(ev, camStreamId);
|
|
44419
44770
|
return;
|
|
@@ -44423,7 +44774,17 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44423
44774
|
this.handleMotionEvent(ev, camStreamId);
|
|
44424
44775
|
return;
|
|
44425
44776
|
}
|
|
44426
|
-
|
|
44777
|
+
if (ev.type === "videoloss") {
|
|
44778
|
+
this.ctx.logger.debug("hikvision: unhandled alarm event", {
|
|
44779
|
+
tags: { deviceId: this.id },
|
|
44780
|
+
meta: {
|
|
44781
|
+
type: ev.type,
|
|
44782
|
+
state: ev.state
|
|
44783
|
+
}
|
|
44784
|
+
});
|
|
44785
|
+
return;
|
|
44786
|
+
}
|
|
44787
|
+
this.ctx.logger.info("hikvision: unhandled alarm event", {
|
|
44427
44788
|
tags: { deviceId: this.id },
|
|
44428
44789
|
meta: {
|
|
44429
44790
|
type: ev.type,
|