@camstack/addon-provider-amcrest 0.1.1 → 0.1.2
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/assets/icon.svg +2 -5
- package/dist/addon.js +437 -34
- package/dist/addon.mjs +437 -35
- package/dist/index.js +1 -0
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/assets/icon.svg
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"
|
|
2
|
-
<rect width="48" height="48" rx="10" fill="#16a34a"/>
|
|
3
|
-
<path d="M14 18h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H14a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3Z" fill="#fff"/>
|
|
4
|
-
<path d="M30 22l6-3v11l-6-3v-5Z" fill="#fff"/>
|
|
5
|
-
<circle cx="20.5" cy="25.5" r="3.5" fill="#16a34a"/>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
|
2
|
+
<image href="data:image/jpeg;base64,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" x="0" y="0" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>
|
|
6
3
|
</svg>
|
package/dist/addon.js
CHANGED
|
@@ -26423,6 +26423,54 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26423
26423
|
const body = await this.getText(`/cgi-bin/configManager.cgi?action=setConfig&${query}`);
|
|
26424
26424
|
if (!/\bOK\b/i.test(body)) throw new Error(`setConfig rejected: ${body.trim().slice(0, 200)}`);
|
|
26425
26425
|
}
|
|
26426
|
+
/**
|
|
26427
|
+
* Open the Dahua CGI event-manager attach stream — a long-lived
|
|
26428
|
+
* `multipart/x-mixed-replace` GET that pushes camera events
|
|
26429
|
+
* (`VideoMotion` Start/Stop, heartbeats, …) until the connection drops
|
|
26430
|
+
* or the caller aborts. Performs the same digest handshake as `send`
|
|
26431
|
+
* (absorbing the initial 401) but — critically — installs NO
|
|
26432
|
+
* per-request timeout: the long-poll must never self-cancel, so
|
|
26433
|
+
* teardown is driven solely by `signal.abort()`. `codes` is passed
|
|
26434
|
+
* through unescaped (Dahua requires the literal `[...]` brackets, same
|
|
26435
|
+
* as `setConfig`), e.g. `'[VideoMotion]'` or `'[All]'`.
|
|
26436
|
+
*/
|
|
26437
|
+
async openEventStream(codes, signal) {
|
|
26438
|
+
const path = `/cgi-bin/eventManager.cgi?action=attach&codes=${codes}`;
|
|
26439
|
+
const url = `${this.scheme}://${this.host}:${this.port}${path}`;
|
|
26440
|
+
const firstHeader = buildAuthHeader({
|
|
26441
|
+
state: this.digestState,
|
|
26442
|
+
method: "GET",
|
|
26443
|
+
uri: path,
|
|
26444
|
+
username: this.username,
|
|
26445
|
+
password: this.password
|
|
26446
|
+
});
|
|
26447
|
+
let res = await fetch(url, {
|
|
26448
|
+
method: "GET",
|
|
26449
|
+
headers: firstHeader ? { authorization: firstHeader } : {},
|
|
26450
|
+
signal
|
|
26451
|
+
});
|
|
26452
|
+
if (res.status === 401) {
|
|
26453
|
+
await res.body?.cancel().catch(() => {});
|
|
26454
|
+
const challenge = parseChallenge(res.headers.get("www-authenticate"));
|
|
26455
|
+
if (challenge) {
|
|
26456
|
+
this.digestState.challenge = challenge;
|
|
26457
|
+
this.digestState.nc = 0;
|
|
26458
|
+
const retryHeader = buildAuthHeader({
|
|
26459
|
+
state: this.digestState,
|
|
26460
|
+
method: "GET",
|
|
26461
|
+
uri: path,
|
|
26462
|
+
username: this.username,
|
|
26463
|
+
password: this.password
|
|
26464
|
+
});
|
|
26465
|
+
res = await fetch(url, {
|
|
26466
|
+
method: "GET",
|
|
26467
|
+
headers: retryHeader ? { authorization: retryHeader } : {},
|
|
26468
|
+
signal
|
|
26469
|
+
});
|
|
26470
|
+
}
|
|
26471
|
+
}
|
|
26472
|
+
return res;
|
|
26473
|
+
}
|
|
26426
26474
|
async getEncodeConfig() {
|
|
26427
26475
|
return this.getConfig("Encode");
|
|
26428
26476
|
}
|
|
@@ -26449,21 +26497,26 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26449
26497
|
};
|
|
26450
26498
|
}
|
|
26451
26499
|
/** Begin a continuous PTZ move. `code` is a Dahua direction token
|
|
26452
|
-
* (`Left`/`Right`/`Up`/`Down`). `speed` maps to
|
|
26453
|
-
|
|
26454
|
-
|
|
26500
|
+
* (`Left`/`Right`/`Up`/`Down`/`ZoomTele`/`ZoomWide`). `speed` maps to
|
|
26501
|
+
* arg2 (1..8 typical). `timeoutMs` (optional) lets the caller pass a
|
|
26502
|
+
* short per-tick deadline so a slow/blocked camera round-trip fails
|
|
26503
|
+
* fast rather than hanging the whole cap call. The Dahua `start`
|
|
26504
|
+
* action is fire-and-return — the head keeps moving until a matching
|
|
26505
|
+
* `stop` is sent — so this must resolve quickly. */
|
|
26506
|
+
async ptzStart(channel, code, speed, timeoutMs) {
|
|
26507
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=${code}&arg1=0&arg2=${speed}&arg3=0`, timeoutMs);
|
|
26455
26508
|
}
|
|
26456
|
-
async ptzStop(channel, code) {
|
|
26457
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0
|
|
26509
|
+
async ptzStop(channel, code, timeoutMs) {
|
|
26510
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0`, timeoutMs);
|
|
26458
26511
|
}
|
|
26459
|
-
async ptzGotoPreset(channel, presetId) {
|
|
26460
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26512
|
+
async ptzGotoPreset(channel, presetId, timeoutMs) {
|
|
26513
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26461
26514
|
}
|
|
26462
|
-
async ptzSetPreset(channel, presetId) {
|
|
26463
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26515
|
+
async ptzSetPreset(channel, presetId, timeoutMs) {
|
|
26516
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26464
26517
|
}
|
|
26465
|
-
async ptzClearPreset(channel, presetId) {
|
|
26466
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26518
|
+
async ptzClearPreset(channel, presetId, timeoutMs) {
|
|
26519
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26467
26520
|
}
|
|
26468
26521
|
/** Read stored PTZ presets from the `PtzPreset` config block. */
|
|
26469
26522
|
async getPtzPresets(channel) {
|
|
@@ -26487,6 +26540,131 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26487
26540
|
}));
|
|
26488
26541
|
}
|
|
26489
26542
|
};
|
|
26543
|
+
/**
|
|
26544
|
+
* Subscribe to a camera's Dahua CGI event-manager attach stream. Returns
|
|
26545
|
+
* an `AbortController` — `controller.abort()` tears the subscription
|
|
26546
|
+
* down. Reconnect logic is the caller's responsibility (the parser stays
|
|
26547
|
+
* simple; the device class owns lifecycle / backoff timing). Mirrors the
|
|
26548
|
+
* Hikvision provider's `subscribeAlarms` shape so both HTTP-stream
|
|
26549
|
+
* providers read the same way.
|
|
26550
|
+
*
|
|
26551
|
+
* Unlike Hikvision's XML alert stream, Dahua parts are `text/plain`
|
|
26552
|
+
* key=value blocks — `parseAmcrestEventBody` handles the projection.
|
|
26553
|
+
*/
|
|
26554
|
+
function subscribeAmcrestEvents(client, codes, handlers) {
|
|
26555
|
+
const controller = new AbortController();
|
|
26556
|
+
(async () => {
|
|
26557
|
+
try {
|
|
26558
|
+
const res = await client.openEventStream(codes, controller.signal);
|
|
26559
|
+
if (!res.ok || !res.body) {
|
|
26560
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream HTTP ${res.status} ${res.statusText}`));
|
|
26561
|
+
return;
|
|
26562
|
+
}
|
|
26563
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
26564
|
+
const boundary = parseBoundary(ct);
|
|
26565
|
+
if (!boundary) {
|
|
26566
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream missing multipart boundary in Content-Type: "${ct}"`));
|
|
26567
|
+
return;
|
|
26568
|
+
}
|
|
26569
|
+
handlers.onConnected?.();
|
|
26570
|
+
await pumpEventStream(res.body, boundary, handlers);
|
|
26571
|
+
if (!controller.signal.aborted) handlers.onError(/* @__PURE__ */ new Error("event stream ended"));
|
|
26572
|
+
} catch (err) {
|
|
26573
|
+
if (controller.signal.aborted) return;
|
|
26574
|
+
handlers.onError(err);
|
|
26575
|
+
}
|
|
26576
|
+
})();
|
|
26577
|
+
return controller;
|
|
26578
|
+
}
|
|
26579
|
+
function parseBoundary(contentType) {
|
|
26580
|
+
const m = /boundary\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
|
|
26581
|
+
return m ? m[1].trim() : null;
|
|
26582
|
+
}
|
|
26583
|
+
async function pumpEventStream(stream, boundary, handlers) {
|
|
26584
|
+
const reader = stream.getReader();
|
|
26585
|
+
const dashBoundary = `--${boundary}`;
|
|
26586
|
+
let bufferedBytes = new Uint8Array(0);
|
|
26587
|
+
try {
|
|
26588
|
+
while (true) {
|
|
26589
|
+
const { value, done } = await reader.read();
|
|
26590
|
+
if (done) break;
|
|
26591
|
+
if (!value || value.byteLength === 0) continue;
|
|
26592
|
+
bufferedBytes = concatBytes(bufferedBytes, value);
|
|
26593
|
+
const parts = splitOnBoundary(bufferedBytes, dashBoundary);
|
|
26594
|
+
bufferedBytes = parts.tail;
|
|
26595
|
+
for (const block of parts.blocks) {
|
|
26596
|
+
const parsed = parseMultipartBlock(block);
|
|
26597
|
+
if (!parsed) continue;
|
|
26598
|
+
const ev = parseAmcrestEventBody(parsed.bodyText);
|
|
26599
|
+
if (ev) handlers.onEvent(ev);
|
|
26600
|
+
}
|
|
26601
|
+
}
|
|
26602
|
+
} finally {
|
|
26603
|
+
try {
|
|
26604
|
+
reader.releaseLock();
|
|
26605
|
+
} catch {}
|
|
26606
|
+
}
|
|
26607
|
+
}
|
|
26608
|
+
function concatBytes(a, b) {
|
|
26609
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
26610
|
+
out.set(a, 0);
|
|
26611
|
+
out.set(b, a.byteLength);
|
|
26612
|
+
return out;
|
|
26613
|
+
}
|
|
26614
|
+
function splitOnBoundary(buf, dashBoundary) {
|
|
26615
|
+
const parts = Buffer.from(buf).toString("binary").split(dashBoundary);
|
|
26616
|
+
if (parts.length <= 1) return {
|
|
26617
|
+
blocks: [],
|
|
26618
|
+
tail: buf
|
|
26619
|
+
};
|
|
26620
|
+
const tail = Buffer.from(parts.pop(), "binary");
|
|
26621
|
+
parts.shift();
|
|
26622
|
+
return {
|
|
26623
|
+
blocks: parts.filter((p) => p.length > 0 && !p.startsWith("--")).map((p) => Buffer.from(p, "binary")),
|
|
26624
|
+
tail
|
|
26625
|
+
};
|
|
26626
|
+
}
|
|
26627
|
+
function parseMultipartBlock(block) {
|
|
26628
|
+
let start = 0;
|
|
26629
|
+
while (start < block.length && (block[start] === 13 || block[start] === 10)) start++;
|
|
26630
|
+
const headerEnd = findHeaderEnd(block, start);
|
|
26631
|
+
const bodyStart = headerEnd < 0 ? start : headerEnd + 4;
|
|
26632
|
+
const bodySlice = block.subarray(bodyStart);
|
|
26633
|
+
return { bodyText: Buffer.from(bodySlice).toString("utf8").replace(/\r?\n--$/m, "").trim() };
|
|
26634
|
+
}
|
|
26635
|
+
function findHeaderEnd(buf, from) {
|
|
26636
|
+
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;
|
|
26637
|
+
return -1;
|
|
26638
|
+
}
|
|
26639
|
+
/**
|
|
26640
|
+
* Parse a Dahua event block body. The salient line is a `;`-delimited
|
|
26641
|
+
* key=value list, e.g. `Code=VideoMotion;action=Start;index=0`. A
|
|
26642
|
+
* trailing multi-line `data={…}` JSON blob (present on some firmwares) is
|
|
26643
|
+
* ignored — motion needs only code/action/index. Returns null when no
|
|
26644
|
+
* `Code=` line is present (e.g. an empty heartbeat frame).
|
|
26645
|
+
*/
|
|
26646
|
+
function parseAmcrestEventBody(body) {
|
|
26647
|
+
const codeLine = body.split(/\r?\n/).find((l) => l.includes("Code=")) ?? "";
|
|
26648
|
+
if (!codeLine) return null;
|
|
26649
|
+
const fields = /* @__PURE__ */ new Map();
|
|
26650
|
+
for (const pair of codeLine.split(";")) {
|
|
26651
|
+
const eq = pair.indexOf("=");
|
|
26652
|
+
if (eq < 0) continue;
|
|
26653
|
+
fields.set(pair.slice(0, eq).trim().toLowerCase(), pair.slice(eq + 1).trim());
|
|
26654
|
+
}
|
|
26655
|
+
const code = fields.get("code");
|
|
26656
|
+
const action = fields.get("action");
|
|
26657
|
+
if (!code || !action) return null;
|
|
26658
|
+
const indexRaw = fields.get("index");
|
|
26659
|
+
const index = indexRaw !== void 0 && /^\d+$/.test(indexRaw) ? Number.parseInt(indexRaw, 10) : null;
|
|
26660
|
+
return {
|
|
26661
|
+
observedAt: Date.now(),
|
|
26662
|
+
code,
|
|
26663
|
+
action,
|
|
26664
|
+
index,
|
|
26665
|
+
raw: body
|
|
26666
|
+
};
|
|
26667
|
+
}
|
|
26490
26668
|
//#endregion
|
|
26491
26669
|
//#region src/amcrest-camera.ts
|
|
26492
26670
|
/**
|
|
@@ -26538,11 +26716,34 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26538
26716
|
this.registerPtzCap();
|
|
26539
26717
|
this.registerStreamParamsCap();
|
|
26540
26718
|
this.registerMotionZonesCap();
|
|
26719
|
+
this.registerMotionCap();
|
|
26541
26720
|
this.registerPrivacyMaskCap();
|
|
26542
26721
|
this.registerOsdCap();
|
|
26543
26722
|
this.registerDayNightCap();
|
|
26544
26723
|
this.registerImageSettingsCap();
|
|
26545
26724
|
}
|
|
26725
|
+
/** Dahua CGI `eventManager` attach codes — VideoMotion is the base
|
|
26726
|
+
* onboard-motion signal (Start/Stop). Smart sub-codes
|
|
26727
|
+
* (SmartMotionHuman/Vehicle) are camera-AI detections handled by a
|
|
26728
|
+
* separate cap and intentionally out of scope here. */
|
|
26729
|
+
static MOTION_EVENT_CODES = "[VideoMotion]";
|
|
26730
|
+
/** Re-emit interval (ms) for a sustained motion window. Dahua fires a
|
|
26731
|
+
* single `VideoMotion action=Start` for the whole window (no per-tick
|
|
26732
|
+
* repeats), but the pipeline-runner re-arms its `motionCooldownMs` on
|
|
26733
|
+
* every `detected:true`. Without a heartbeat a >cooldown motion window
|
|
26734
|
+
* would lapse the runner back to `watching`; a bounded ~0.1 Hz re-emit
|
|
26735
|
+
* holds it `active` until the `Stop`. */
|
|
26736
|
+
static MOTION_KEEPALIVE_MS = 1e4;
|
|
26737
|
+
/** Active event-stream subscription; aborted on dispose / conn change. */
|
|
26738
|
+
eventStreamController = null;
|
|
26739
|
+
/** Reconnect attempt counter — drives exponential backoff. */
|
|
26740
|
+
eventStreamReconnectAttempts = 0;
|
|
26741
|
+
/** Pending reconnect timer. */
|
|
26742
|
+
eventStreamReconnectTimer = null;
|
|
26743
|
+
/** True between a `VideoMotion Start` and its `Stop`. */
|
|
26744
|
+
motionActive = false;
|
|
26745
|
+
/** Keepalive re-emit timer for a sustained motion window. */
|
|
26746
|
+
motionKeepaliveTimer = null;
|
|
26546
26747
|
/** Single-flight guard for the `stream-params` camera refresh. */
|
|
26547
26748
|
streamParamsRefreshInFlight = null;
|
|
26548
26749
|
/** Single-flight guard for the `motion-zones` camera refresh. */
|
|
@@ -26573,7 +26774,155 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26573
26774
|
* (possibly just-edited) credentials/host. */
|
|
26574
26775
|
async applySettingsPatch(patch) {
|
|
26575
26776
|
await super.applySettingsPatch(patch);
|
|
26576
|
-
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch)
|
|
26777
|
+
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch) {
|
|
26778
|
+
this.clearPtzAutoStop();
|
|
26779
|
+
this.client = null;
|
|
26780
|
+
const hadStream = this.eventStreamController !== null || this.eventStreamReconnectTimer !== null;
|
|
26781
|
+
this.teardownEventSubscription();
|
|
26782
|
+
if (hadStream) this.ensureEventSubscription();
|
|
26783
|
+
}
|
|
26784
|
+
}
|
|
26785
|
+
/** Register the native `motion` cap. Its mere presence (a `kind:'native'`
|
|
26786
|
+
* `motion` binding) is what the orchestrator reads to default a
|
|
26787
|
+
* freshly-adopted camera to `motionSources:['onboard']`. */
|
|
26788
|
+
registerMotionCap() {
|
|
26789
|
+
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
26790
|
+
if (deviceId !== this.id) throw new Error(`AmcrestCamera: motion deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
26791
|
+
return this.getCapSlice(motionCapability)?.detected ?? false;
|
|
26792
|
+
} });
|
|
26793
|
+
this.setCapSlice(motionCapability, {
|
|
26794
|
+
detected: false,
|
|
26795
|
+
lastDetectedAt: null,
|
|
26796
|
+
autoClearAfterMs: null
|
|
26797
|
+
});
|
|
26798
|
+
this.ctx.logger.info("amcrest: motion cap registered", { tags: { deviceId: this.id } });
|
|
26799
|
+
}
|
|
26800
|
+
/** Reusable event-source identity for every emit on this device. */
|
|
26801
|
+
eventSource() {
|
|
26802
|
+
return {
|
|
26803
|
+
type: "device",
|
|
26804
|
+
id: this.id,
|
|
26805
|
+
addonId: AMCREST_ADDON_ID,
|
|
26806
|
+
deviceId: this.id
|
|
26807
|
+
};
|
|
26808
|
+
}
|
|
26809
|
+
/** Emit an onboard motion transition on the bus. The runner subscribes
|
|
26810
|
+
* and drives its phase machine via `reportMotion`. */
|
|
26811
|
+
emitMotion(detected, timestamp) {
|
|
26812
|
+
this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, this.eventSource(), {
|
|
26813
|
+
deviceId: this.id,
|
|
26814
|
+
detected,
|
|
26815
|
+
timestamp,
|
|
26816
|
+
source: "onboard"
|
|
26817
|
+
}));
|
|
26818
|
+
}
|
|
26819
|
+
/** Idempotently open the eventManager attach stream. */
|
|
26820
|
+
ensureEventSubscription() {
|
|
26821
|
+
if (this.eventStreamController) return;
|
|
26822
|
+
const client = this.ensureClient();
|
|
26823
|
+
this.eventStreamController = subscribeAmcrestEvents(client, AmcrestCamera.MOTION_EVENT_CODES, {
|
|
26824
|
+
onConnected: () => {
|
|
26825
|
+
this.eventStreamReconnectAttempts = 0;
|
|
26826
|
+
if (this.motionActive) this.onMotionStop(Date.now());
|
|
26827
|
+
this.ctx.logger.info("amcrest: event stream connected", { tags: { deviceId: this.id } });
|
|
26828
|
+
},
|
|
26829
|
+
onEvent: (ev) => this.handleAmcrestEvent(ev),
|
|
26830
|
+
onError: (err) => {
|
|
26831
|
+
this.ctx.logger.warn("amcrest: event stream error — will reconnect", {
|
|
26832
|
+
tags: { deviceId: this.id },
|
|
26833
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26834
|
+
});
|
|
26835
|
+
this.eventStreamController = null;
|
|
26836
|
+
this.scheduleEventReconnect();
|
|
26837
|
+
}
|
|
26838
|
+
});
|
|
26839
|
+
}
|
|
26840
|
+
/** Single pending reconnect timer with capped exponential backoff
|
|
26841
|
+
* (1s → 60s) — same shape as the Hikvision alarm stream. */
|
|
26842
|
+
scheduleEventReconnect() {
|
|
26843
|
+
if (this.eventStreamReconnectTimer) return;
|
|
26844
|
+
const attempts = ++this.eventStreamReconnectAttempts;
|
|
26845
|
+
const delayMs = Math.min(6e4, 1e3 * 2 ** Math.min(attempts, 6));
|
|
26846
|
+
this.eventStreamReconnectTimer = setTimeout(() => {
|
|
26847
|
+
this.eventStreamReconnectTimer = null;
|
|
26848
|
+
this.ensureEventSubscription();
|
|
26849
|
+
}, delayMs);
|
|
26850
|
+
}
|
|
26851
|
+
handleAmcrestEvent(ev) {
|
|
26852
|
+
if (ev.code !== "VideoMotion") {
|
|
26853
|
+
this.ctx.logger.debug("amcrest: unhandled event", {
|
|
26854
|
+
tags: { deviceId: this.id },
|
|
26855
|
+
meta: {
|
|
26856
|
+
code: ev.code,
|
|
26857
|
+
action: ev.action
|
|
26858
|
+
}
|
|
26859
|
+
});
|
|
26860
|
+
return;
|
|
26861
|
+
}
|
|
26862
|
+
const action = ev.action.toLowerCase();
|
|
26863
|
+
if (action === "start" || action === "pulse") this.onMotionStart(ev.observedAt);
|
|
26864
|
+
else if (action === "stop") this.onMotionStop(ev.observedAt);
|
|
26865
|
+
}
|
|
26866
|
+
onMotionStart(now) {
|
|
26867
|
+
const wasActive = this.motionActive;
|
|
26868
|
+
this.motionActive = true;
|
|
26869
|
+
this.emitMotion(true, now);
|
|
26870
|
+
if (!wasActive) {
|
|
26871
|
+
this.setCapSlice(motionCapability, {
|
|
26872
|
+
detected: true,
|
|
26873
|
+
lastDetectedAt: now,
|
|
26874
|
+
autoClearAfterMs: null
|
|
26875
|
+
});
|
|
26876
|
+
this.ctx.logger.info("amcrest: onboard motion started", { tags: { deviceId: this.id } });
|
|
26877
|
+
}
|
|
26878
|
+
if (this.motionKeepaliveTimer === null) this.motionKeepaliveTimer = setInterval(() => {
|
|
26879
|
+
this.emitMotion(true, Date.now());
|
|
26880
|
+
}, AmcrestCamera.MOTION_KEEPALIVE_MS);
|
|
26881
|
+
}
|
|
26882
|
+
onMotionStop(now) {
|
|
26883
|
+
this.clearMotionKeepalive();
|
|
26884
|
+
if (!this.motionActive) return;
|
|
26885
|
+
this.motionActive = false;
|
|
26886
|
+
this.emitMotion(false, now);
|
|
26887
|
+
const lastDetectedAt = this.getCapSlice(motionCapability)?.lastDetectedAt ?? now;
|
|
26888
|
+
this.setCapSlice(motionCapability, {
|
|
26889
|
+
detected: false,
|
|
26890
|
+
lastDetectedAt,
|
|
26891
|
+
autoClearAfterMs: null
|
|
26892
|
+
});
|
|
26893
|
+
this.ctx.logger.info("amcrest: onboard motion stopped", { tags: { deviceId: this.id } });
|
|
26894
|
+
}
|
|
26895
|
+
clearMotionKeepalive() {
|
|
26896
|
+
if (this.motionKeepaliveTimer !== null) {
|
|
26897
|
+
clearInterval(this.motionKeepaliveTimer);
|
|
26898
|
+
this.motionKeepaliveTimer = null;
|
|
26899
|
+
}
|
|
26900
|
+
}
|
|
26901
|
+
/** Abort the stream + clear all motion timers. Idempotent. */
|
|
26902
|
+
teardownEventSubscription() {
|
|
26903
|
+
if (this.eventStreamReconnectTimer) {
|
|
26904
|
+
clearTimeout(this.eventStreamReconnectTimer);
|
|
26905
|
+
this.eventStreamReconnectTimer = null;
|
|
26906
|
+
}
|
|
26907
|
+
if (this.eventStreamController) {
|
|
26908
|
+
try {
|
|
26909
|
+
this.eventStreamController.abort();
|
|
26910
|
+
} catch {}
|
|
26911
|
+
this.eventStreamController = null;
|
|
26912
|
+
}
|
|
26913
|
+
this.clearMotionKeepalive();
|
|
26914
|
+
this.motionActive = false;
|
|
26915
|
+
}
|
|
26916
|
+
/** Phase 5 — device is live: open the onboard-motion event stream. */
|
|
26917
|
+
async onActivate() {
|
|
26918
|
+
this.ensureEventSubscription();
|
|
26919
|
+
}
|
|
26920
|
+
/** Teardown — stop the stream + motion timers, drop the client. */
|
|
26921
|
+
async removeDevice() {
|
|
26922
|
+
this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
|
|
26923
|
+
this.teardownEventSubscription();
|
|
26924
|
+
this.clearPtzAutoStop();
|
|
26925
|
+
this.client = null;
|
|
26577
26926
|
}
|
|
26578
26927
|
registerStreamCatalogProvider() {
|
|
26579
26928
|
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
@@ -26680,16 +27029,41 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26680
27029
|
invalidateCache: async () => {}
|
|
26681
27030
|
});
|
|
26682
27031
|
}
|
|
27032
|
+
/** Duration (ms) a one-shot `move` pulse runs before its safety
|
|
27033
|
+
* auto-stop fires. The `usePTZ` UI hook already issues its own paired
|
|
27034
|
+
* `stop` after a client-side pulse window; this timer is a backstop so
|
|
27035
|
+
* a lone `move` (no following `stop`) can't leave the head gliding. */
|
|
27036
|
+
static PTZ_PULSE_MS = 350;
|
|
27037
|
+
/** Short per-call deadline (ms) for the PTZ start/stop CGI so a slow or
|
|
27038
|
+
* blocked camera round-trip fails fast instead of exceeding the
|
|
27039
|
+
* hub-local-uds transport's abort window — the failure mode that
|
|
27040
|
+
* surfaced live as
|
|
27041
|
+
* `ptz.move ... hub-local-uds=This operation was aborted`. */
|
|
27042
|
+
static PTZ_CGI_TIMEOUT_MS = 2500;
|
|
26683
27043
|
/** Dahua `ptz.cgi` direction/zoom codes currently started — tracked so
|
|
26684
27044
|
* `stop()` / the next `continuousMove()` can cleanly halt exactly the
|
|
26685
27045
|
* codes that are actually moving instead of guessing. */
|
|
26686
27046
|
activePtzCodes = /* @__PURE__ */ new Set();
|
|
27047
|
+
/** Pending safety auto-stop for the last one-shot `move` pulse.
|
|
27048
|
+
* Cleared by any subsequent move/continuousMove/stop so pulses and
|
|
27049
|
+
* gestures never race a stale timer. */
|
|
27050
|
+
ptzAutoStopTimer = null;
|
|
27051
|
+
/** Cancel the pending one-shot `move` auto-stop, if any. */
|
|
27052
|
+
clearPtzAutoStop() {
|
|
27053
|
+
if (this.ptzAutoStopTimer !== null) {
|
|
27054
|
+
clearTimeout(this.ptzAutoStopTimer);
|
|
27055
|
+
this.ptzAutoStopTimer = null;
|
|
27056
|
+
}
|
|
27057
|
+
}
|
|
26687
27058
|
/** Resolve a cap `PtzMoveCommand` into 0-2 Dahua CGI move commands
|
|
26688
27059
|
* (one pan/tilt code — possibly diagonal — plus one zoom code).
|
|
26689
|
-
* `pan`/`tilt`/`zoom` are normalized -1..1
|
|
26690
|
-
*
|
|
26691
|
-
|
|
26692
|
-
|
|
27060
|
+
* `pan`/`tilt`/`zoom` are normalized -1..1. Speed maps to Dahua's 1-8
|
|
27061
|
+
* scale: the explicit cap-level `speed` (0..1, from the UI speed
|
|
27062
|
+
* slider) wins when supplied; otherwise the vector magnitude is used. */
|
|
27063
|
+
resolvePtzCodes(pan, tilt, zoom, speed) {
|
|
27064
|
+
const speedFor = (magnitude) => {
|
|
27065
|
+
return Math.max(1, Math.min(8, Math.round((speed !== void 0 ? Math.abs(speed) : Math.abs(magnitude)) * 8)));
|
|
27066
|
+
};
|
|
26693
27067
|
const out = [];
|
|
26694
27068
|
const p = pan ?? 0;
|
|
26695
27069
|
const t = tilt ?? 0;
|
|
@@ -26715,7 +27089,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26715
27089
|
const channel = this.channel;
|
|
26716
27090
|
const codes = [...this.activePtzCodes];
|
|
26717
27091
|
this.activePtzCodes.clear();
|
|
26718
|
-
await Promise.all(codes.map((code) => client.ptzStop(channel, code).catch((err) => {
|
|
27092
|
+
await Promise.all(codes.map((code) => client.ptzStop(channel, code, AmcrestCamera.PTZ_CGI_TIMEOUT_MS).catch((err) => {
|
|
26719
27093
|
this.ctx.logger.debug("amcrest: ptzStop failed (continuing)", {
|
|
26720
27094
|
tags: { deviceId: this.id },
|
|
26721
27095
|
meta: {
|
|
@@ -26728,31 +27102,57 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26728
27102
|
async startPtzCodes(codes) {
|
|
26729
27103
|
const client = this.ensureClient();
|
|
26730
27104
|
const channel = this.channel;
|
|
26731
|
-
|
|
27105
|
+
await Promise.all(codes.map(({ code, speed }) => {
|
|
26732
27106
|
this.activePtzCodes.add(code);
|
|
26733
|
-
|
|
26734
|
-
}
|
|
27107
|
+
return client.ptzStart(channel, code, speed, AmcrestCamera.PTZ_CGI_TIMEOUT_MS);
|
|
27108
|
+
}));
|
|
27109
|
+
}
|
|
27110
|
+
/** Serialized tail of PTZ CGI operations for this camera. */
|
|
27111
|
+
ptzChain = Promise.resolve();
|
|
27112
|
+
/** Serialize a PTZ CGI op onto the per-camera chain and return AT ONCE.
|
|
27113
|
+
* The UI models a short tap as `move`+`stop` and a long press as
|
|
27114
|
+
* `continuousMove`+`stop`. Two problems the chain solves together:
|
|
27115
|
+
* (1) awaiting the camera round-trip (digest handshake + CGI) in the cap
|
|
27116
|
+
* call makes the hub-local-uds transport abort it → `This operation was
|
|
27117
|
+
* aborted`, head never moves; (2) issuing start/stop as independent
|
|
27118
|
+
* fire-and-forget fetches RACES them — a `stop` fetch can land before its
|
|
27119
|
+
* `start`, leaving the head gliding or never moving (why tap "only worked
|
|
27120
|
+
* on long press"). Chaining runs each op to completion IN ORDER (start
|
|
27121
|
+
* before its paired stop) while the cap returns immediately. */
|
|
27122
|
+
enqueuePtz(label, op) {
|
|
27123
|
+
this.ptzChain = this.ptzChain.then(op).catch((err) => {
|
|
27124
|
+
this.ctx.logger.debug(`amcrest: ptz ${label} failed`, {
|
|
27125
|
+
tags: { deviceId: this.id },
|
|
27126
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
27127
|
+
});
|
|
27128
|
+
});
|
|
26735
27129
|
}
|
|
26736
27130
|
registerPtzCap() {
|
|
26737
27131
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
26738
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27132
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26739
27133
|
if (deviceId !== this.id) return;
|
|
26740
|
-
|
|
26741
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
27134
|
+
this.clearPtzAutoStop();
|
|
27135
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
26742
27136
|
if (codes.length === 0) return;
|
|
26743
|
-
|
|
26744
|
-
|
|
26745
|
-
|
|
27137
|
+
this.enqueuePtz("move", () => this.startPtzCodes(codes));
|
|
27138
|
+
this.ptzAutoStopTimer = setTimeout(() => {
|
|
27139
|
+
this.ptzAutoStopTimer = null;
|
|
27140
|
+
this.enqueuePtz("auto-stop", () => this.stopActivePtz());
|
|
27141
|
+
}, AmcrestCamera.PTZ_PULSE_MS);
|
|
26746
27142
|
},
|
|
26747
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27143
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26748
27144
|
if (deviceId !== this.id) return;
|
|
26749
|
-
|
|
26750
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
26751
|
-
|
|
27145
|
+
this.clearPtzAutoStop();
|
|
27146
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
27147
|
+
this.enqueuePtz("continuousMove", async () => {
|
|
27148
|
+
await this.stopActivePtz();
|
|
27149
|
+
if (codes.length > 0) await this.startPtzCodes(codes);
|
|
27150
|
+
});
|
|
26752
27151
|
},
|
|
26753
27152
|
stop: async ({ deviceId }) => {
|
|
26754
27153
|
if (deviceId !== this.id) return;
|
|
26755
|
-
|
|
27154
|
+
this.clearPtzAutoStop();
|
|
27155
|
+
this.enqueuePtz("stop", () => this.stopActivePtz());
|
|
26756
27156
|
},
|
|
26757
27157
|
getPresets: async ({ deviceId }) => {
|
|
26758
27158
|
if (deviceId !== this.id) return [];
|
|
@@ -26764,8 +27164,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26764
27164
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
26765
27165
|
if (deviceId !== this.id) return;
|
|
26766
27166
|
const id = Number.parseInt(presetId, 10);
|
|
26767
|
-
if (!Number.isFinite(id)) return;
|
|
26768
|
-
|
|
27167
|
+
if (!Number.isFinite(id) || id < 1) return;
|
|
27168
|
+
const client = this.ensureClient();
|
|
27169
|
+
this.enqueuePtz("goToPreset", () => client.ptzGotoPreset(this.channel, id, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26769
27170
|
},
|
|
26770
27171
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
26771
27172
|
if (deviceId !== this.id) return;
|
|
@@ -26801,7 +27202,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26801
27202
|
},
|
|
26802
27203
|
goHome: async ({ deviceId }) => {
|
|
26803
27204
|
if (deviceId !== this.id) return;
|
|
26804
|
-
|
|
27205
|
+
const client = this.ensureClient();
|
|
27206
|
+
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel, 1, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26805
27207
|
},
|
|
26806
27208
|
getPosition: async () => {
|
|
26807
27209
|
return {
|
|
@@ -36728,3 +37130,4 @@ exports.AmcrestCamera = AmcrestCamera;
|
|
|
36728
37130
|
exports.AmcrestCgiClient = AmcrestCgiClient;
|
|
36729
37131
|
exports.AmcrestProviderAddon = AmcrestProviderAddon;
|
|
36730
37132
|
exports.amcrestCameraSchema = amcrestCameraSchema;
|
|
37133
|
+
exports.subscribeAmcrestEvents = subscribeAmcrestEvents;
|
package/dist/addon.mjs
CHANGED
|
@@ -26424,6 +26424,54 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26424
26424
|
const body = await this.getText(`/cgi-bin/configManager.cgi?action=setConfig&${query}`);
|
|
26425
26425
|
if (!/\bOK\b/i.test(body)) throw new Error(`setConfig rejected: ${body.trim().slice(0, 200)}`);
|
|
26426
26426
|
}
|
|
26427
|
+
/**
|
|
26428
|
+
* Open the Dahua CGI event-manager attach stream — a long-lived
|
|
26429
|
+
* `multipart/x-mixed-replace` GET that pushes camera events
|
|
26430
|
+
* (`VideoMotion` Start/Stop, heartbeats, …) until the connection drops
|
|
26431
|
+
* or the caller aborts. Performs the same digest handshake as `send`
|
|
26432
|
+
* (absorbing the initial 401) but — critically — installs NO
|
|
26433
|
+
* per-request timeout: the long-poll must never self-cancel, so
|
|
26434
|
+
* teardown is driven solely by `signal.abort()`. `codes` is passed
|
|
26435
|
+
* through unescaped (Dahua requires the literal `[...]` brackets, same
|
|
26436
|
+
* as `setConfig`), e.g. `'[VideoMotion]'` or `'[All]'`.
|
|
26437
|
+
*/
|
|
26438
|
+
async openEventStream(codes, signal) {
|
|
26439
|
+
const path = `/cgi-bin/eventManager.cgi?action=attach&codes=${codes}`;
|
|
26440
|
+
const url = `${this.scheme}://${this.host}:${this.port}${path}`;
|
|
26441
|
+
const firstHeader = buildAuthHeader({
|
|
26442
|
+
state: this.digestState,
|
|
26443
|
+
method: "GET",
|
|
26444
|
+
uri: path,
|
|
26445
|
+
username: this.username,
|
|
26446
|
+
password: this.password
|
|
26447
|
+
});
|
|
26448
|
+
let res = await fetch(url, {
|
|
26449
|
+
method: "GET",
|
|
26450
|
+
headers: firstHeader ? { authorization: firstHeader } : {},
|
|
26451
|
+
signal
|
|
26452
|
+
});
|
|
26453
|
+
if (res.status === 401) {
|
|
26454
|
+
await res.body?.cancel().catch(() => {});
|
|
26455
|
+
const challenge = parseChallenge(res.headers.get("www-authenticate"));
|
|
26456
|
+
if (challenge) {
|
|
26457
|
+
this.digestState.challenge = challenge;
|
|
26458
|
+
this.digestState.nc = 0;
|
|
26459
|
+
const retryHeader = buildAuthHeader({
|
|
26460
|
+
state: this.digestState,
|
|
26461
|
+
method: "GET",
|
|
26462
|
+
uri: path,
|
|
26463
|
+
username: this.username,
|
|
26464
|
+
password: this.password
|
|
26465
|
+
});
|
|
26466
|
+
res = await fetch(url, {
|
|
26467
|
+
method: "GET",
|
|
26468
|
+
headers: retryHeader ? { authorization: retryHeader } : {},
|
|
26469
|
+
signal
|
|
26470
|
+
});
|
|
26471
|
+
}
|
|
26472
|
+
}
|
|
26473
|
+
return res;
|
|
26474
|
+
}
|
|
26427
26475
|
async getEncodeConfig() {
|
|
26428
26476
|
return this.getConfig("Encode");
|
|
26429
26477
|
}
|
|
@@ -26450,21 +26498,26 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26450
26498
|
};
|
|
26451
26499
|
}
|
|
26452
26500
|
/** Begin a continuous PTZ move. `code` is a Dahua direction token
|
|
26453
|
-
* (`Left`/`Right`/`Up`/`Down`). `speed` maps to
|
|
26454
|
-
|
|
26455
|
-
|
|
26501
|
+
* (`Left`/`Right`/`Up`/`Down`/`ZoomTele`/`ZoomWide`). `speed` maps to
|
|
26502
|
+
* arg2 (1..8 typical). `timeoutMs` (optional) lets the caller pass a
|
|
26503
|
+
* short per-tick deadline so a slow/blocked camera round-trip fails
|
|
26504
|
+
* fast rather than hanging the whole cap call. The Dahua `start`
|
|
26505
|
+
* action is fire-and-return — the head keeps moving until a matching
|
|
26506
|
+
* `stop` is sent — so this must resolve quickly. */
|
|
26507
|
+
async ptzStart(channel, code, speed, timeoutMs) {
|
|
26508
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=${code}&arg1=0&arg2=${speed}&arg3=0`, timeoutMs);
|
|
26456
26509
|
}
|
|
26457
|
-
async ptzStop(channel, code) {
|
|
26458
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0
|
|
26510
|
+
async ptzStop(channel, code, timeoutMs) {
|
|
26511
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0`, timeoutMs);
|
|
26459
26512
|
}
|
|
26460
|
-
async ptzGotoPreset(channel, presetId) {
|
|
26461
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26513
|
+
async ptzGotoPreset(channel, presetId, timeoutMs) {
|
|
26514
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26462
26515
|
}
|
|
26463
|
-
async ptzSetPreset(channel, presetId) {
|
|
26464
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26516
|
+
async ptzSetPreset(channel, presetId, timeoutMs) {
|
|
26517
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26465
26518
|
}
|
|
26466
|
-
async ptzClearPreset(channel, presetId) {
|
|
26467
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26519
|
+
async ptzClearPreset(channel, presetId, timeoutMs) {
|
|
26520
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26468
26521
|
}
|
|
26469
26522
|
/** Read stored PTZ presets from the `PtzPreset` config block. */
|
|
26470
26523
|
async getPtzPresets(channel) {
|
|
@@ -26488,6 +26541,131 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26488
26541
|
}));
|
|
26489
26542
|
}
|
|
26490
26543
|
};
|
|
26544
|
+
/**
|
|
26545
|
+
* Subscribe to a camera's Dahua CGI event-manager attach stream. Returns
|
|
26546
|
+
* an `AbortController` — `controller.abort()` tears the subscription
|
|
26547
|
+
* down. Reconnect logic is the caller's responsibility (the parser stays
|
|
26548
|
+
* simple; the device class owns lifecycle / backoff timing). Mirrors the
|
|
26549
|
+
* Hikvision provider's `subscribeAlarms` shape so both HTTP-stream
|
|
26550
|
+
* providers read the same way.
|
|
26551
|
+
*
|
|
26552
|
+
* Unlike Hikvision's XML alert stream, Dahua parts are `text/plain`
|
|
26553
|
+
* key=value blocks — `parseAmcrestEventBody` handles the projection.
|
|
26554
|
+
*/
|
|
26555
|
+
function subscribeAmcrestEvents(client, codes, handlers) {
|
|
26556
|
+
const controller = new AbortController();
|
|
26557
|
+
(async () => {
|
|
26558
|
+
try {
|
|
26559
|
+
const res = await client.openEventStream(codes, controller.signal);
|
|
26560
|
+
if (!res.ok || !res.body) {
|
|
26561
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream HTTP ${res.status} ${res.statusText}`));
|
|
26562
|
+
return;
|
|
26563
|
+
}
|
|
26564
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
26565
|
+
const boundary = parseBoundary(ct);
|
|
26566
|
+
if (!boundary) {
|
|
26567
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream missing multipart boundary in Content-Type: "${ct}"`));
|
|
26568
|
+
return;
|
|
26569
|
+
}
|
|
26570
|
+
handlers.onConnected?.();
|
|
26571
|
+
await pumpEventStream(res.body, boundary, handlers);
|
|
26572
|
+
if (!controller.signal.aborted) handlers.onError(/* @__PURE__ */ new Error("event stream ended"));
|
|
26573
|
+
} catch (err) {
|
|
26574
|
+
if (controller.signal.aborted) return;
|
|
26575
|
+
handlers.onError(err);
|
|
26576
|
+
}
|
|
26577
|
+
})();
|
|
26578
|
+
return controller;
|
|
26579
|
+
}
|
|
26580
|
+
function parseBoundary(contentType) {
|
|
26581
|
+
const m = /boundary\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
|
|
26582
|
+
return m ? m[1].trim() : null;
|
|
26583
|
+
}
|
|
26584
|
+
async function pumpEventStream(stream, boundary, handlers) {
|
|
26585
|
+
const reader = stream.getReader();
|
|
26586
|
+
const dashBoundary = `--${boundary}`;
|
|
26587
|
+
let bufferedBytes = new Uint8Array(0);
|
|
26588
|
+
try {
|
|
26589
|
+
while (true) {
|
|
26590
|
+
const { value, done } = await reader.read();
|
|
26591
|
+
if (done) break;
|
|
26592
|
+
if (!value || value.byteLength === 0) continue;
|
|
26593
|
+
bufferedBytes = concatBytes(bufferedBytes, value);
|
|
26594
|
+
const parts = splitOnBoundary(bufferedBytes, dashBoundary);
|
|
26595
|
+
bufferedBytes = parts.tail;
|
|
26596
|
+
for (const block of parts.blocks) {
|
|
26597
|
+
const parsed = parseMultipartBlock(block);
|
|
26598
|
+
if (!parsed) continue;
|
|
26599
|
+
const ev = parseAmcrestEventBody(parsed.bodyText);
|
|
26600
|
+
if (ev) handlers.onEvent(ev);
|
|
26601
|
+
}
|
|
26602
|
+
}
|
|
26603
|
+
} finally {
|
|
26604
|
+
try {
|
|
26605
|
+
reader.releaseLock();
|
|
26606
|
+
} catch {}
|
|
26607
|
+
}
|
|
26608
|
+
}
|
|
26609
|
+
function concatBytes(a, b) {
|
|
26610
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
26611
|
+
out.set(a, 0);
|
|
26612
|
+
out.set(b, a.byteLength);
|
|
26613
|
+
return out;
|
|
26614
|
+
}
|
|
26615
|
+
function splitOnBoundary(buf, dashBoundary) {
|
|
26616
|
+
const parts = Buffer.from(buf).toString("binary").split(dashBoundary);
|
|
26617
|
+
if (parts.length <= 1) return {
|
|
26618
|
+
blocks: [],
|
|
26619
|
+
tail: buf
|
|
26620
|
+
};
|
|
26621
|
+
const tail = Buffer.from(parts.pop(), "binary");
|
|
26622
|
+
parts.shift();
|
|
26623
|
+
return {
|
|
26624
|
+
blocks: parts.filter((p) => p.length > 0 && !p.startsWith("--")).map((p) => Buffer.from(p, "binary")),
|
|
26625
|
+
tail
|
|
26626
|
+
};
|
|
26627
|
+
}
|
|
26628
|
+
function parseMultipartBlock(block) {
|
|
26629
|
+
let start = 0;
|
|
26630
|
+
while (start < block.length && (block[start] === 13 || block[start] === 10)) start++;
|
|
26631
|
+
const headerEnd = findHeaderEnd(block, start);
|
|
26632
|
+
const bodyStart = headerEnd < 0 ? start : headerEnd + 4;
|
|
26633
|
+
const bodySlice = block.subarray(bodyStart);
|
|
26634
|
+
return { bodyText: Buffer.from(bodySlice).toString("utf8").replace(/\r?\n--$/m, "").trim() };
|
|
26635
|
+
}
|
|
26636
|
+
function findHeaderEnd(buf, from) {
|
|
26637
|
+
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;
|
|
26638
|
+
return -1;
|
|
26639
|
+
}
|
|
26640
|
+
/**
|
|
26641
|
+
* Parse a Dahua event block body. The salient line is a `;`-delimited
|
|
26642
|
+
* key=value list, e.g. `Code=VideoMotion;action=Start;index=0`. A
|
|
26643
|
+
* trailing multi-line `data={…}` JSON blob (present on some firmwares) is
|
|
26644
|
+
* ignored — motion needs only code/action/index. Returns null when no
|
|
26645
|
+
* `Code=` line is present (e.g. an empty heartbeat frame).
|
|
26646
|
+
*/
|
|
26647
|
+
function parseAmcrestEventBody(body) {
|
|
26648
|
+
const codeLine = body.split(/\r?\n/).find((l) => l.includes("Code=")) ?? "";
|
|
26649
|
+
if (!codeLine) return null;
|
|
26650
|
+
const fields = /* @__PURE__ */ new Map();
|
|
26651
|
+
for (const pair of codeLine.split(";")) {
|
|
26652
|
+
const eq = pair.indexOf("=");
|
|
26653
|
+
if (eq < 0) continue;
|
|
26654
|
+
fields.set(pair.slice(0, eq).trim().toLowerCase(), pair.slice(eq + 1).trim());
|
|
26655
|
+
}
|
|
26656
|
+
const code = fields.get("code");
|
|
26657
|
+
const action = fields.get("action");
|
|
26658
|
+
if (!code || !action) return null;
|
|
26659
|
+
const indexRaw = fields.get("index");
|
|
26660
|
+
const index = indexRaw !== void 0 && /^\d+$/.test(indexRaw) ? Number.parseInt(indexRaw, 10) : null;
|
|
26661
|
+
return {
|
|
26662
|
+
observedAt: Date.now(),
|
|
26663
|
+
code,
|
|
26664
|
+
action,
|
|
26665
|
+
index,
|
|
26666
|
+
raw: body
|
|
26667
|
+
};
|
|
26668
|
+
}
|
|
26491
26669
|
//#endregion
|
|
26492
26670
|
//#region src/amcrest-camera.ts
|
|
26493
26671
|
/**
|
|
@@ -26539,11 +26717,34 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26539
26717
|
this.registerPtzCap();
|
|
26540
26718
|
this.registerStreamParamsCap();
|
|
26541
26719
|
this.registerMotionZonesCap();
|
|
26720
|
+
this.registerMotionCap();
|
|
26542
26721
|
this.registerPrivacyMaskCap();
|
|
26543
26722
|
this.registerOsdCap();
|
|
26544
26723
|
this.registerDayNightCap();
|
|
26545
26724
|
this.registerImageSettingsCap();
|
|
26546
26725
|
}
|
|
26726
|
+
/** Dahua CGI `eventManager` attach codes — VideoMotion is the base
|
|
26727
|
+
* onboard-motion signal (Start/Stop). Smart sub-codes
|
|
26728
|
+
* (SmartMotionHuman/Vehicle) are camera-AI detections handled by a
|
|
26729
|
+
* separate cap and intentionally out of scope here. */
|
|
26730
|
+
static MOTION_EVENT_CODES = "[VideoMotion]";
|
|
26731
|
+
/** Re-emit interval (ms) for a sustained motion window. Dahua fires a
|
|
26732
|
+
* single `VideoMotion action=Start` for the whole window (no per-tick
|
|
26733
|
+
* repeats), but the pipeline-runner re-arms its `motionCooldownMs` on
|
|
26734
|
+
* every `detected:true`. Without a heartbeat a >cooldown motion window
|
|
26735
|
+
* would lapse the runner back to `watching`; a bounded ~0.1 Hz re-emit
|
|
26736
|
+
* holds it `active` until the `Stop`. */
|
|
26737
|
+
static MOTION_KEEPALIVE_MS = 1e4;
|
|
26738
|
+
/** Active event-stream subscription; aborted on dispose / conn change. */
|
|
26739
|
+
eventStreamController = null;
|
|
26740
|
+
/** Reconnect attempt counter — drives exponential backoff. */
|
|
26741
|
+
eventStreamReconnectAttempts = 0;
|
|
26742
|
+
/** Pending reconnect timer. */
|
|
26743
|
+
eventStreamReconnectTimer = null;
|
|
26744
|
+
/** True between a `VideoMotion Start` and its `Stop`. */
|
|
26745
|
+
motionActive = false;
|
|
26746
|
+
/** Keepalive re-emit timer for a sustained motion window. */
|
|
26747
|
+
motionKeepaliveTimer = null;
|
|
26547
26748
|
/** Single-flight guard for the `stream-params` camera refresh. */
|
|
26548
26749
|
streamParamsRefreshInFlight = null;
|
|
26549
26750
|
/** Single-flight guard for the `motion-zones` camera refresh. */
|
|
@@ -26574,7 +26775,155 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26574
26775
|
* (possibly just-edited) credentials/host. */
|
|
26575
26776
|
async applySettingsPatch(patch) {
|
|
26576
26777
|
await super.applySettingsPatch(patch);
|
|
26577
|
-
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch)
|
|
26778
|
+
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch) {
|
|
26779
|
+
this.clearPtzAutoStop();
|
|
26780
|
+
this.client = null;
|
|
26781
|
+
const hadStream = this.eventStreamController !== null || this.eventStreamReconnectTimer !== null;
|
|
26782
|
+
this.teardownEventSubscription();
|
|
26783
|
+
if (hadStream) this.ensureEventSubscription();
|
|
26784
|
+
}
|
|
26785
|
+
}
|
|
26786
|
+
/** Register the native `motion` cap. Its mere presence (a `kind:'native'`
|
|
26787
|
+
* `motion` binding) is what the orchestrator reads to default a
|
|
26788
|
+
* freshly-adopted camera to `motionSources:['onboard']`. */
|
|
26789
|
+
registerMotionCap() {
|
|
26790
|
+
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
26791
|
+
if (deviceId !== this.id) throw new Error(`AmcrestCamera: motion deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
26792
|
+
return this.getCapSlice(motionCapability)?.detected ?? false;
|
|
26793
|
+
} });
|
|
26794
|
+
this.setCapSlice(motionCapability, {
|
|
26795
|
+
detected: false,
|
|
26796
|
+
lastDetectedAt: null,
|
|
26797
|
+
autoClearAfterMs: null
|
|
26798
|
+
});
|
|
26799
|
+
this.ctx.logger.info("amcrest: motion cap registered", { tags: { deviceId: this.id } });
|
|
26800
|
+
}
|
|
26801
|
+
/** Reusable event-source identity for every emit on this device. */
|
|
26802
|
+
eventSource() {
|
|
26803
|
+
return {
|
|
26804
|
+
type: "device",
|
|
26805
|
+
id: this.id,
|
|
26806
|
+
addonId: AMCREST_ADDON_ID,
|
|
26807
|
+
deviceId: this.id
|
|
26808
|
+
};
|
|
26809
|
+
}
|
|
26810
|
+
/** Emit an onboard motion transition on the bus. The runner subscribes
|
|
26811
|
+
* and drives its phase machine via `reportMotion`. */
|
|
26812
|
+
emitMotion(detected, timestamp) {
|
|
26813
|
+
this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, this.eventSource(), {
|
|
26814
|
+
deviceId: this.id,
|
|
26815
|
+
detected,
|
|
26816
|
+
timestamp,
|
|
26817
|
+
source: "onboard"
|
|
26818
|
+
}));
|
|
26819
|
+
}
|
|
26820
|
+
/** Idempotently open the eventManager attach stream. */
|
|
26821
|
+
ensureEventSubscription() {
|
|
26822
|
+
if (this.eventStreamController) return;
|
|
26823
|
+
const client = this.ensureClient();
|
|
26824
|
+
this.eventStreamController = subscribeAmcrestEvents(client, AmcrestCamera.MOTION_EVENT_CODES, {
|
|
26825
|
+
onConnected: () => {
|
|
26826
|
+
this.eventStreamReconnectAttempts = 0;
|
|
26827
|
+
if (this.motionActive) this.onMotionStop(Date.now());
|
|
26828
|
+
this.ctx.logger.info("amcrest: event stream connected", { tags: { deviceId: this.id } });
|
|
26829
|
+
},
|
|
26830
|
+
onEvent: (ev) => this.handleAmcrestEvent(ev),
|
|
26831
|
+
onError: (err) => {
|
|
26832
|
+
this.ctx.logger.warn("amcrest: event stream error — will reconnect", {
|
|
26833
|
+
tags: { deviceId: this.id },
|
|
26834
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26835
|
+
});
|
|
26836
|
+
this.eventStreamController = null;
|
|
26837
|
+
this.scheduleEventReconnect();
|
|
26838
|
+
}
|
|
26839
|
+
});
|
|
26840
|
+
}
|
|
26841
|
+
/** Single pending reconnect timer with capped exponential backoff
|
|
26842
|
+
* (1s → 60s) — same shape as the Hikvision alarm stream. */
|
|
26843
|
+
scheduleEventReconnect() {
|
|
26844
|
+
if (this.eventStreamReconnectTimer) return;
|
|
26845
|
+
const attempts = ++this.eventStreamReconnectAttempts;
|
|
26846
|
+
const delayMs = Math.min(6e4, 1e3 * 2 ** Math.min(attempts, 6));
|
|
26847
|
+
this.eventStreamReconnectTimer = setTimeout(() => {
|
|
26848
|
+
this.eventStreamReconnectTimer = null;
|
|
26849
|
+
this.ensureEventSubscription();
|
|
26850
|
+
}, delayMs);
|
|
26851
|
+
}
|
|
26852
|
+
handleAmcrestEvent(ev) {
|
|
26853
|
+
if (ev.code !== "VideoMotion") {
|
|
26854
|
+
this.ctx.logger.debug("amcrest: unhandled event", {
|
|
26855
|
+
tags: { deviceId: this.id },
|
|
26856
|
+
meta: {
|
|
26857
|
+
code: ev.code,
|
|
26858
|
+
action: ev.action
|
|
26859
|
+
}
|
|
26860
|
+
});
|
|
26861
|
+
return;
|
|
26862
|
+
}
|
|
26863
|
+
const action = ev.action.toLowerCase();
|
|
26864
|
+
if (action === "start" || action === "pulse") this.onMotionStart(ev.observedAt);
|
|
26865
|
+
else if (action === "stop") this.onMotionStop(ev.observedAt);
|
|
26866
|
+
}
|
|
26867
|
+
onMotionStart(now) {
|
|
26868
|
+
const wasActive = this.motionActive;
|
|
26869
|
+
this.motionActive = true;
|
|
26870
|
+
this.emitMotion(true, now);
|
|
26871
|
+
if (!wasActive) {
|
|
26872
|
+
this.setCapSlice(motionCapability, {
|
|
26873
|
+
detected: true,
|
|
26874
|
+
lastDetectedAt: now,
|
|
26875
|
+
autoClearAfterMs: null
|
|
26876
|
+
});
|
|
26877
|
+
this.ctx.logger.info("amcrest: onboard motion started", { tags: { deviceId: this.id } });
|
|
26878
|
+
}
|
|
26879
|
+
if (this.motionKeepaliveTimer === null) this.motionKeepaliveTimer = setInterval(() => {
|
|
26880
|
+
this.emitMotion(true, Date.now());
|
|
26881
|
+
}, AmcrestCamera.MOTION_KEEPALIVE_MS);
|
|
26882
|
+
}
|
|
26883
|
+
onMotionStop(now) {
|
|
26884
|
+
this.clearMotionKeepalive();
|
|
26885
|
+
if (!this.motionActive) return;
|
|
26886
|
+
this.motionActive = false;
|
|
26887
|
+
this.emitMotion(false, now);
|
|
26888
|
+
const lastDetectedAt = this.getCapSlice(motionCapability)?.lastDetectedAt ?? now;
|
|
26889
|
+
this.setCapSlice(motionCapability, {
|
|
26890
|
+
detected: false,
|
|
26891
|
+
lastDetectedAt,
|
|
26892
|
+
autoClearAfterMs: null
|
|
26893
|
+
});
|
|
26894
|
+
this.ctx.logger.info("amcrest: onboard motion stopped", { tags: { deviceId: this.id } });
|
|
26895
|
+
}
|
|
26896
|
+
clearMotionKeepalive() {
|
|
26897
|
+
if (this.motionKeepaliveTimer !== null) {
|
|
26898
|
+
clearInterval(this.motionKeepaliveTimer);
|
|
26899
|
+
this.motionKeepaliveTimer = null;
|
|
26900
|
+
}
|
|
26901
|
+
}
|
|
26902
|
+
/** Abort the stream + clear all motion timers. Idempotent. */
|
|
26903
|
+
teardownEventSubscription() {
|
|
26904
|
+
if (this.eventStreamReconnectTimer) {
|
|
26905
|
+
clearTimeout(this.eventStreamReconnectTimer);
|
|
26906
|
+
this.eventStreamReconnectTimer = null;
|
|
26907
|
+
}
|
|
26908
|
+
if (this.eventStreamController) {
|
|
26909
|
+
try {
|
|
26910
|
+
this.eventStreamController.abort();
|
|
26911
|
+
} catch {}
|
|
26912
|
+
this.eventStreamController = null;
|
|
26913
|
+
}
|
|
26914
|
+
this.clearMotionKeepalive();
|
|
26915
|
+
this.motionActive = false;
|
|
26916
|
+
}
|
|
26917
|
+
/** Phase 5 — device is live: open the onboard-motion event stream. */
|
|
26918
|
+
async onActivate() {
|
|
26919
|
+
this.ensureEventSubscription();
|
|
26920
|
+
}
|
|
26921
|
+
/** Teardown — stop the stream + motion timers, drop the client. */
|
|
26922
|
+
async removeDevice() {
|
|
26923
|
+
this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
|
|
26924
|
+
this.teardownEventSubscription();
|
|
26925
|
+
this.clearPtzAutoStop();
|
|
26926
|
+
this.client = null;
|
|
26578
26927
|
}
|
|
26579
26928
|
registerStreamCatalogProvider() {
|
|
26580
26929
|
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
@@ -26681,16 +27030,41 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26681
27030
|
invalidateCache: async () => {}
|
|
26682
27031
|
});
|
|
26683
27032
|
}
|
|
27033
|
+
/** Duration (ms) a one-shot `move` pulse runs before its safety
|
|
27034
|
+
* auto-stop fires. The `usePTZ` UI hook already issues its own paired
|
|
27035
|
+
* `stop` after a client-side pulse window; this timer is a backstop so
|
|
27036
|
+
* a lone `move` (no following `stop`) can't leave the head gliding. */
|
|
27037
|
+
static PTZ_PULSE_MS = 350;
|
|
27038
|
+
/** Short per-call deadline (ms) for the PTZ start/stop CGI so a slow or
|
|
27039
|
+
* blocked camera round-trip fails fast instead of exceeding the
|
|
27040
|
+
* hub-local-uds transport's abort window — the failure mode that
|
|
27041
|
+
* surfaced live as
|
|
27042
|
+
* `ptz.move ... hub-local-uds=This operation was aborted`. */
|
|
27043
|
+
static PTZ_CGI_TIMEOUT_MS = 2500;
|
|
26684
27044
|
/** Dahua `ptz.cgi` direction/zoom codes currently started — tracked so
|
|
26685
27045
|
* `stop()` / the next `continuousMove()` can cleanly halt exactly the
|
|
26686
27046
|
* codes that are actually moving instead of guessing. */
|
|
26687
27047
|
activePtzCodes = /* @__PURE__ */ new Set();
|
|
27048
|
+
/** Pending safety auto-stop for the last one-shot `move` pulse.
|
|
27049
|
+
* Cleared by any subsequent move/continuousMove/stop so pulses and
|
|
27050
|
+
* gestures never race a stale timer. */
|
|
27051
|
+
ptzAutoStopTimer = null;
|
|
27052
|
+
/** Cancel the pending one-shot `move` auto-stop, if any. */
|
|
27053
|
+
clearPtzAutoStop() {
|
|
27054
|
+
if (this.ptzAutoStopTimer !== null) {
|
|
27055
|
+
clearTimeout(this.ptzAutoStopTimer);
|
|
27056
|
+
this.ptzAutoStopTimer = null;
|
|
27057
|
+
}
|
|
27058
|
+
}
|
|
26688
27059
|
/** Resolve a cap `PtzMoveCommand` into 0-2 Dahua CGI move commands
|
|
26689
27060
|
* (one pan/tilt code — possibly diagonal — plus one zoom code).
|
|
26690
|
-
* `pan`/`tilt`/`zoom` are normalized -1..1
|
|
26691
|
-
*
|
|
26692
|
-
|
|
26693
|
-
|
|
27061
|
+
* `pan`/`tilt`/`zoom` are normalized -1..1. Speed maps to Dahua's 1-8
|
|
27062
|
+
* scale: the explicit cap-level `speed` (0..1, from the UI speed
|
|
27063
|
+
* slider) wins when supplied; otherwise the vector magnitude is used. */
|
|
27064
|
+
resolvePtzCodes(pan, tilt, zoom, speed) {
|
|
27065
|
+
const speedFor = (magnitude) => {
|
|
27066
|
+
return Math.max(1, Math.min(8, Math.round((speed !== void 0 ? Math.abs(speed) : Math.abs(magnitude)) * 8)));
|
|
27067
|
+
};
|
|
26694
27068
|
const out = [];
|
|
26695
27069
|
const p = pan ?? 0;
|
|
26696
27070
|
const t = tilt ?? 0;
|
|
@@ -26716,7 +27090,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26716
27090
|
const channel = this.channel;
|
|
26717
27091
|
const codes = [...this.activePtzCodes];
|
|
26718
27092
|
this.activePtzCodes.clear();
|
|
26719
|
-
await Promise.all(codes.map((code) => client.ptzStop(channel, code).catch((err) => {
|
|
27093
|
+
await Promise.all(codes.map((code) => client.ptzStop(channel, code, AmcrestCamera.PTZ_CGI_TIMEOUT_MS).catch((err) => {
|
|
26720
27094
|
this.ctx.logger.debug("amcrest: ptzStop failed (continuing)", {
|
|
26721
27095
|
tags: { deviceId: this.id },
|
|
26722
27096
|
meta: {
|
|
@@ -26729,31 +27103,57 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26729
27103
|
async startPtzCodes(codes) {
|
|
26730
27104
|
const client = this.ensureClient();
|
|
26731
27105
|
const channel = this.channel;
|
|
26732
|
-
|
|
27106
|
+
await Promise.all(codes.map(({ code, speed }) => {
|
|
26733
27107
|
this.activePtzCodes.add(code);
|
|
26734
|
-
|
|
26735
|
-
}
|
|
27108
|
+
return client.ptzStart(channel, code, speed, AmcrestCamera.PTZ_CGI_TIMEOUT_MS);
|
|
27109
|
+
}));
|
|
27110
|
+
}
|
|
27111
|
+
/** Serialized tail of PTZ CGI operations for this camera. */
|
|
27112
|
+
ptzChain = Promise.resolve();
|
|
27113
|
+
/** Serialize a PTZ CGI op onto the per-camera chain and return AT ONCE.
|
|
27114
|
+
* The UI models a short tap as `move`+`stop` and a long press as
|
|
27115
|
+
* `continuousMove`+`stop`. Two problems the chain solves together:
|
|
27116
|
+
* (1) awaiting the camera round-trip (digest handshake + CGI) in the cap
|
|
27117
|
+
* call makes the hub-local-uds transport abort it → `This operation was
|
|
27118
|
+
* aborted`, head never moves; (2) issuing start/stop as independent
|
|
27119
|
+
* fire-and-forget fetches RACES them — a `stop` fetch can land before its
|
|
27120
|
+
* `start`, leaving the head gliding or never moving (why tap "only worked
|
|
27121
|
+
* on long press"). Chaining runs each op to completion IN ORDER (start
|
|
27122
|
+
* before its paired stop) while the cap returns immediately. */
|
|
27123
|
+
enqueuePtz(label, op) {
|
|
27124
|
+
this.ptzChain = this.ptzChain.then(op).catch((err) => {
|
|
27125
|
+
this.ctx.logger.debug(`amcrest: ptz ${label} failed`, {
|
|
27126
|
+
tags: { deviceId: this.id },
|
|
27127
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
27128
|
+
});
|
|
27129
|
+
});
|
|
26736
27130
|
}
|
|
26737
27131
|
registerPtzCap() {
|
|
26738
27132
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
26739
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27133
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26740
27134
|
if (deviceId !== this.id) return;
|
|
26741
|
-
|
|
26742
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
27135
|
+
this.clearPtzAutoStop();
|
|
27136
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
26743
27137
|
if (codes.length === 0) return;
|
|
26744
|
-
|
|
26745
|
-
|
|
26746
|
-
|
|
27138
|
+
this.enqueuePtz("move", () => this.startPtzCodes(codes));
|
|
27139
|
+
this.ptzAutoStopTimer = setTimeout(() => {
|
|
27140
|
+
this.ptzAutoStopTimer = null;
|
|
27141
|
+
this.enqueuePtz("auto-stop", () => this.stopActivePtz());
|
|
27142
|
+
}, AmcrestCamera.PTZ_PULSE_MS);
|
|
26747
27143
|
},
|
|
26748
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27144
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26749
27145
|
if (deviceId !== this.id) return;
|
|
26750
|
-
|
|
26751
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
26752
|
-
|
|
27146
|
+
this.clearPtzAutoStop();
|
|
27147
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
27148
|
+
this.enqueuePtz("continuousMove", async () => {
|
|
27149
|
+
await this.stopActivePtz();
|
|
27150
|
+
if (codes.length > 0) await this.startPtzCodes(codes);
|
|
27151
|
+
});
|
|
26753
27152
|
},
|
|
26754
27153
|
stop: async ({ deviceId }) => {
|
|
26755
27154
|
if (deviceId !== this.id) return;
|
|
26756
|
-
|
|
27155
|
+
this.clearPtzAutoStop();
|
|
27156
|
+
this.enqueuePtz("stop", () => this.stopActivePtz());
|
|
26757
27157
|
},
|
|
26758
27158
|
getPresets: async ({ deviceId }) => {
|
|
26759
27159
|
if (deviceId !== this.id) return [];
|
|
@@ -26765,8 +27165,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26765
27165
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
26766
27166
|
if (deviceId !== this.id) return;
|
|
26767
27167
|
const id = Number.parseInt(presetId, 10);
|
|
26768
|
-
if (!Number.isFinite(id)) return;
|
|
26769
|
-
|
|
27168
|
+
if (!Number.isFinite(id) || id < 1) return;
|
|
27169
|
+
const client = this.ensureClient();
|
|
27170
|
+
this.enqueuePtz("goToPreset", () => client.ptzGotoPreset(this.channel, id, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26770
27171
|
},
|
|
26771
27172
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
26772
27173
|
if (deviceId !== this.id) return;
|
|
@@ -26802,7 +27203,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26802
27203
|
},
|
|
26803
27204
|
goHome: async ({ deviceId }) => {
|
|
26804
27205
|
if (deviceId !== this.id) return;
|
|
26805
|
-
|
|
27206
|
+
const client = this.ensureClient();
|
|
27207
|
+
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel, 1, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26806
27208
|
},
|
|
26807
27209
|
getPosition: async () => {
|
|
26808
27210
|
return {
|
|
@@ -36724,4 +37126,4 @@ var AmcrestProviderAddon = class extends BaseDeviceProvider {
|
|
|
36724
37126
|
}
|
|
36725
37127
|
};
|
|
36726
37128
|
//#endregion
|
|
36727
|
-
export { AmcrestProviderAddon, amcrestCameraSchema as i, AmcrestCgiClient as n,
|
|
37129
|
+
export { AmcrestProviderAddon, amcrestCameraSchema as a, AMCREST_ADDON_ID as i, AmcrestCgiClient as n, subscribeAmcrestEvents as r, AmcrestCamera as t };
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,4 @@ exports.AmcrestCamera = require_addon.AmcrestCamera;
|
|
|
5
5
|
exports.AmcrestCgiClient = require_addon.AmcrestCgiClient;
|
|
6
6
|
exports.AmcrestProviderAddon = require_addon.AmcrestProviderAddon;
|
|
7
7
|
exports.amcrestCameraSchema = require_addon.amcrestCameraSchema;
|
|
8
|
+
exports.subscribeAmcrestEvents = require_addon.subscribeAmcrestEvents;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AmcrestProviderAddon,
|
|
2
|
-
export { AMCREST_ADDON_ID, AmcrestCamera, AmcrestCgiClient, AmcrestProviderAddon, amcrestCameraSchema };
|
|
1
|
+
import { AmcrestProviderAddon, a as amcrestCameraSchema, i as AMCREST_ADDON_ID, n as AmcrestCgiClient, r as subscribeAmcrestEvents, t as AmcrestCamera } from "./addon.mjs";
|
|
2
|
+
export { AMCREST_ADDON_ID, AmcrestCamera, AmcrestCgiClient, AmcrestProviderAddon, amcrestCameraSchema, subscribeAmcrestEvents };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-amcrest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|