@camstack/core 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +220 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +9 -0
- package/dist/builtins/addon-pages-aggregator/index.js +222 -0
- package/dist/builtins/addon-pages-aggregator/index.js.map +1 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs +9 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +200 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +9 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.js +202 -0
- package/dist/builtins/addon-widgets-aggregator/index.js.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs +9 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs.map +1 -0
- package/dist/builtins/alerts/alerts.addon.js +443 -0
- package/dist/builtins/alerts/alerts.addon.js.map +1 -0
- package/dist/builtins/alerts/alerts.addon.mjs +9 -0
- package/dist/builtins/alerts/alerts.addon.mjs.map +1 -0
- package/dist/builtins/alerts/index.js +443 -0
- package/dist/builtins/alerts/index.js.map +1 -0
- package/dist/builtins/alerts/index.mjs +8 -0
- package/dist/builtins/alerts/index.mjs.map +1 -0
- package/dist/builtins/console-logging/index.js +242 -0
- package/dist/builtins/console-logging/index.js.map +1 -0
- package/dist/builtins/console-logging/index.mjs +11 -0
- package/dist/builtins/console-logging/index.mjs.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.js +2155 -0
- package/dist/builtins/device-manager/device-manager.addon.js.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs +9 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -0
- package/dist/builtins/device-manager/index.js +2157 -0
- package/dist/builtins/device-manager/index.js.map +1 -0
- package/dist/builtins/device-manager/index.mjs +10 -0
- package/dist/builtins/device-manager/index.mjs.map +1 -0
- package/dist/builtins/hub-forwarder/index.js +297 -0
- package/dist/builtins/hub-forwarder/index.js.map +1 -0
- package/dist/builtins/hub-forwarder/index.mjs +11 -0
- package/dist/builtins/hub-forwarder/index.mjs.map +1 -0
- package/dist/builtins/local-auth/index.js +623 -0
- package/dist/builtins/local-auth/index.js.map +1 -0
- package/dist/builtins/local-auth/index.mjs +8 -0
- package/dist/builtins/local-auth/index.mjs.map +1 -0
- package/dist/builtins/local-auth/local-auth.addon.js +623 -0
- package/dist/builtins/local-auth/local-auth.addon.js.map +1 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs +9 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -0
- package/dist/builtins/local-backup/index.js +53 -68
- package/dist/builtins/local-backup/index.js.map +1 -1
- package/dist/builtins/local-backup/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +898 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +7 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -0
- package/dist/builtins/snapshot/index.js +504 -0
- package/dist/builtins/snapshot/index.js.map +1 -0
- package/dist/builtins/snapshot/index.mjs +477 -0
- package/dist/builtins/snapshot/index.mjs.map +1 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +16 -166
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +554 -621
- package/dist/builtins/sqlite-storage/index.js.map +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +9 -11
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +368 -130
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/system-config/index.js +189 -0
- package/dist/builtins/system-config/index.js.map +1 -0
- package/dist/builtins/system-config/index.mjs +10 -0
- package/dist/builtins/system-config/index.mjs.map +1 -0
- package/dist/builtins/system-config/system-config.addon.js +187 -0
- package/dist/builtins/system-config/system-config.addon.js.map +1 -0
- package/dist/builtins/system-config/system-config.addon.mjs +9 -0
- package/dist/builtins/system-config/system-config.addon.mjs.map +1 -0
- package/dist/builtins/winston-logging/index.js +185 -65
- package/dist/builtins/winston-logging/index.js.map +1 -1
- package/dist/builtins/winston-logging/index.mjs +2 -1
- package/dist/chunk-2CIYKDRN.mjs +1 -0
- package/dist/chunk-2CIYKDRN.mjs.map +1 -0
- package/dist/chunk-2F76X6NL.mjs +136 -0
- package/dist/chunk-2F76X6NL.mjs.map +1 -0
- package/dist/chunk-2QUFBZ7M.mjs +1 -0
- package/dist/chunk-2QUFBZ7M.mjs.map +1 -0
- package/dist/chunk-3BK2Y7GY.mjs +593 -0
- package/dist/chunk-3BK2Y7GY.mjs.map +1 -0
- package/dist/chunk-4OOHFJHT.mjs +421 -0
- package/dist/chunk-4OOHFJHT.mjs.map +1 -0
- package/dist/chunk-4XHB7IHT.mjs +809 -0
- package/dist/chunk-4XHB7IHT.mjs.map +1 -0
- package/dist/{chunk-2F3XZYRW.mjs → chunk-6M2HSSTQ.mjs} +16 -7
- package/dist/chunk-6M2HSSTQ.mjs.map +1 -0
- package/dist/{chunk-SO4LROOT.mjs → chunk-7FI7SQS7.mjs} +54 -69
- package/dist/chunk-7FI7SQS7.mjs.map +1 -0
- package/dist/chunk-ED57RCQE.mjs +171 -0
- package/dist/chunk-ED57RCQE.mjs.map +1 -0
- package/dist/chunk-FZN56HGQ.mjs +626 -0
- package/dist/chunk-FZN56HGQ.mjs.map +1 -0
- package/dist/chunk-GL4OOB25.mjs +51 -0
- package/dist/chunk-GL4OOB25.mjs.map +1 -0
- package/dist/chunk-KDG2NTDB.mjs +137 -0
- package/dist/chunk-KDG2NTDB.mjs.map +1 -0
- package/dist/chunk-NRBQWBDM.mjs +191 -0
- package/dist/chunk-NRBQWBDM.mjs.map +1 -0
- package/dist/chunk-O4V246GG.mjs +2137 -0
- package/dist/chunk-O4V246GG.mjs.map +1 -0
- package/dist/chunk-QT57H266.mjs +163 -0
- package/dist/chunk-QT57H266.mjs.map +1 -0
- package/dist/chunk-QX4RH25I.mjs +141 -0
- package/dist/chunk-QX4RH25I.mjs.map +1 -0
- package/dist/chunk-TB562PZX.mjs +86 -0
- package/dist/chunk-TB562PZX.mjs.map +1 -0
- package/dist/chunk-TDYPZXK5.mjs +1 -0
- package/dist/chunk-TDYPZXK5.mjs.map +1 -0
- package/dist/chunk-UJI4LN5P.mjs +36 -0
- package/dist/chunk-UJI4LN5P.mjs.map +1 -0
- package/dist/chunk-W6RTHQGP.mjs +1 -0
- package/dist/chunk-W6RTHQGP.mjs.map +1 -0
- package/dist/chunk-ZELBCPDC.mjs +369 -0
- package/dist/chunk-ZELBCPDC.mjs.map +1 -0
- package/dist/index.d.mts +1103 -544
- package/dist/index.d.ts +1103 -544
- package/dist/index.js +7032 -6033
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +568 -2226
- package/dist/index.mjs.map +1 -1
- package/dist/resource-monitor-UZUGPIAU.mjs +9 -0
- package/dist/resource-monitor-UZUGPIAU.mjs.map +1 -0
- package/dist/storage-location-manager-HFNB3PCS.mjs +7 -0
- package/dist/storage-location-manager-HFNB3PCS.mjs.map +1 -0
- package/package.json +123 -2
- package/dist/builtins/local-backup/index.d.mts +0 -42
- package/dist/builtins/local-backup/index.d.ts +0 -42
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.mts +0 -2
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +0 -2
- package/dist/builtins/sqlite-storage/index.d.mts +0 -4
- package/dist/builtins/sqlite-storage/index.d.ts +0 -4
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.mts +0 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +0 -2
- package/dist/builtins/winston-logging/index.d.mts +0 -30
- package/dist/builtins/winston-logging/index.d.ts +0 -30
- package/dist/chunk-2F3XZYRW.mjs.map +0 -1
- package/dist/chunk-LQFPAEQF.mjs +0 -147
- package/dist/chunk-LQFPAEQF.mjs.map +0 -1
- package/dist/chunk-R3DIIBBX.mjs +0 -532
- package/dist/chunk-R3DIIBBX.mjs.map +0 -1
- package/dist/chunk-SMNR44VG.mjs +0 -386
- package/dist/chunk-SMNR44VG.mjs.map +0 -1
- package/dist/chunk-SO4LROOT.mjs.map +0 -1
- package/dist/chunk-SPA4JBKN.mjs +0 -175
- package/dist/chunk-SPA4JBKN.mjs.map +0 -1
- package/dist/dist-3BY63UQ5.mjs +0 -2151
- package/dist/dist-3BY63UQ5.mjs.map +0 -1
- package/dist/filesystem-storage.addon-C42r589X.d.mts +0 -57
- package/dist/filesystem-storage.addon-C42r589X.d.ts +0 -57
- package/dist/sql-schema-CKz78rId.d.mts +0 -97
- package/dist/sql-schema-CKz78rId.d.ts +0 -97
- package/dist/sqlite-settings.addon-KwG-uKMP.d.mts +0 -79
- package/dist/sqlite-settings.addon-KwG-uKMP.d.ts +0 -79
- package/dist/storage-location-manager-KKDQNAKA.mjs +0 -7
- /package/dist/{storage-location-manager-KKDQNAKA.mjs.map → builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs.map} +0 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/builtins/snapshot/index.ts
|
|
21
|
+
var snapshot_exports = {};
|
|
22
|
+
__export(snapshot_exports, {
|
|
23
|
+
SnapshotAddon: () => SnapshotAddon,
|
|
24
|
+
default: () => SnapshotAddon
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(snapshot_exports);
|
|
27
|
+
|
|
28
|
+
// src/builtins/snapshot/snapshot.addon.ts
|
|
29
|
+
var import_node_child_process = require("child_process");
|
|
30
|
+
var import_types = require("@camstack/types");
|
|
31
|
+
var import_types2 = require("@camstack/types");
|
|
32
|
+
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
33
|
+
var BATTERY_DEFAULT_MAX_AGE_S = 3600;
|
|
34
|
+
var SnapshotAddon = class extends import_types2.BaseAddon {
|
|
35
|
+
cache = /* @__PURE__ */ new Map();
|
|
36
|
+
constructor() {
|
|
37
|
+
super({
|
|
38
|
+
staleTtlMs: 6e4
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async onInitialize() {
|
|
42
|
+
this.ctx.logger.info("Snapshot wrapper initialized");
|
|
43
|
+
const provider = {
|
|
44
|
+
getSnapshot: (input) => this.getSnapshot(input),
|
|
45
|
+
invalidateCache: (input) => this.invalidateCache(input),
|
|
46
|
+
// DeviceSettingsContribution surface — numeric deviceId end-to-end.
|
|
47
|
+
getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
|
|
48
|
+
getDeviceLiveContribution: async () => null,
|
|
49
|
+
applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),
|
|
50
|
+
// Status surface — derives a diagnostic snapshot from the cache
|
|
51
|
+
// bookkeeping (lastCapturedAt, cacheAgeMs, lastBytes, lastStreamId).
|
|
52
|
+
// Returns null when the device has never been captured.
|
|
53
|
+
getStatus: async (input) => this.getStatus(input.deviceId)
|
|
54
|
+
};
|
|
55
|
+
return [{
|
|
56
|
+
capability: import_types.snapshotCapability,
|
|
57
|
+
provider,
|
|
58
|
+
kind: "wrapper",
|
|
59
|
+
defaultActive: true
|
|
60
|
+
}];
|
|
61
|
+
}
|
|
62
|
+
async onShutdown() {
|
|
63
|
+
this.cache.clear();
|
|
64
|
+
}
|
|
65
|
+
globalSettingsSchema() {
|
|
66
|
+
return this.schema({
|
|
67
|
+
sections: [{
|
|
68
|
+
id: "snapshot-cache",
|
|
69
|
+
title: "Snapshot Cache",
|
|
70
|
+
description: "Stale fallback when live capture fails. Per-device freshness is configured under Device \u2192 Snapshot \u2192 Max cache age.",
|
|
71
|
+
columns: 1,
|
|
72
|
+
fields: [
|
|
73
|
+
this.field({
|
|
74
|
+
type: "number",
|
|
75
|
+
key: "staleTtlMs",
|
|
76
|
+
label: "Stale fallback TTL (ms)",
|
|
77
|
+
description: "If live capture fails, cached snapshot younger than this is still returned.",
|
|
78
|
+
min: 0,
|
|
79
|
+
max: 36e5,
|
|
80
|
+
step: 1e3,
|
|
81
|
+
default: 6e4
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
}]
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// ── Capability methods ────────────────────────────────────────────────
|
|
88
|
+
async getSnapshot(input) {
|
|
89
|
+
const { deviceId, force } = input;
|
|
90
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
91
|
+
const deviceName = meta?.name;
|
|
92
|
+
const isBatteryDevice = meta?.isBattery ?? false;
|
|
93
|
+
const log = this.ctx.logger.withTags({ deviceId, ...deviceName ? { deviceName } : {} });
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const hit = this.cache.get(deviceId);
|
|
96
|
+
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
97
|
+
const rawPref = prefs.snapshotStreamId;
|
|
98
|
+
const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
|
|
99
|
+
if (prefs.snapshotDebug) {
|
|
100
|
+
log.info("debug on", { tags: { deviceId }, meta: { stream: effectiveStreamId ?? "auto" } });
|
|
101
|
+
}
|
|
102
|
+
const defaultMaxAgeS = isBatteryDevice ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S;
|
|
103
|
+
const effectiveMaxAgeMs = (typeof prefs.snapshotMaxAgeS === "number" && prefs.snapshotMaxAgeS >= 0 ? prefs.snapshotMaxAgeS : defaultMaxAgeS) * 1e3;
|
|
104
|
+
if (!force && hit && now - hit.ts < effectiveMaxAgeMs) {
|
|
105
|
+
if (prefs.snapshotDebug) {
|
|
106
|
+
log.debug("snapshot: cache hit", {
|
|
107
|
+
tags: { deviceId },
|
|
108
|
+
meta: { ageMs: now - hit.ts, maxAgeMs: effectiveMaxAgeMs, isBattery: isBatteryDevice }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return hit.data;
|
|
112
|
+
}
|
|
113
|
+
let nativeError = null;
|
|
114
|
+
let nativeAbsent = false;
|
|
115
|
+
try {
|
|
116
|
+
const native = this.ctx.getNativeProvider(import_types.snapshotCapability, deviceId);
|
|
117
|
+
if (native) {
|
|
118
|
+
const result = await native.getSnapshot(input);
|
|
119
|
+
if (result) {
|
|
120
|
+
this.cache.set(deviceId, { data: result, ts: now, streamId: effectiveStreamId ?? null });
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
nativeAbsent = true;
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const msg = (0, import_types2.errMsg)(err);
|
|
128
|
+
if (isAbsentNativeError(msg)) {
|
|
129
|
+
nativeAbsent = true;
|
|
130
|
+
log.debug("native snapshot absent", { tags: { deviceId }, meta: { error: msg } });
|
|
131
|
+
} else {
|
|
132
|
+
nativeError = err;
|
|
133
|
+
log.warn("native snapshot failed", { tags: { deviceId }, meta: { error: msg } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const skipBrokerForBattery = isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId);
|
|
137
|
+
if (!skipBrokerForBattery) {
|
|
138
|
+
try {
|
|
139
|
+
const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
|
|
140
|
+
if (fallback) {
|
|
141
|
+
this.cache.set(deviceId, { data: fallback, ts: now, streamId: effectiveStreamId ?? null });
|
|
142
|
+
return fallback;
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log.warn("stream-broker snapshot fallback failed", { tags: { deviceId }, meta: { error: (0, import_types2.errMsg)(err) } });
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
log.debug("snapshot: skipping broker fallback \u2014 battery device with absent native and no streaming broker", {
|
|
149
|
+
tags: { deviceId }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (hit) {
|
|
153
|
+
const ageMs = now - hit.ts;
|
|
154
|
+
if (ageMs > this.config.staleTtlMs) {
|
|
155
|
+
log.warn("snapshot: all live paths failed \u2014 serving stale cache", { tags: { deviceId }, meta: { ageMs } });
|
|
156
|
+
}
|
|
157
|
+
return hit.data;
|
|
158
|
+
}
|
|
159
|
+
if (nativeError) throw nativeError;
|
|
160
|
+
if (nativeAbsent) return null;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Tell apart "native provider isn't registered for this device" from
|
|
165
|
+
* "native provider ran and threw a real error". The former is the steady
|
|
166
|
+
* state for cameras without a vendor snapshot endpoint and should not
|
|
167
|
+
* propagate as a 500; the latter should.
|
|
168
|
+
*/
|
|
169
|
+
/**
|
|
170
|
+
* Pull one JPEG from the device's stream-broker RTSP restream using
|
|
171
|
+
* a short-lived ffmpeg invocation.
|
|
172
|
+
*
|
|
173
|
+
* Stream selection strategy (picks the broker that won't stall):
|
|
174
|
+
* 1. Explicit `preferredStreamId` (user set in per-device settings)
|
|
175
|
+
* — always honoured, even if currently idle. Operator choice
|
|
176
|
+
* wins.
|
|
177
|
+
* 2. Auto: highest-quality broker currently in `streaming` state.
|
|
178
|
+
* This is the whole point — prefer the stream that has active
|
|
179
|
+
* subscribers (usually `low` for detection, but `mid`/`high`
|
|
180
|
+
* if WebRTC is watching) so ffmpeg hits a warm pipe and
|
|
181
|
+
* doesn't race the broker's resume.
|
|
182
|
+
* 3. Fallback: highest-quality enabled entry regardless of status
|
|
183
|
+
* (will wake a suspended broker — retry-guarded against the
|
|
184
|
+
* cold-start error).
|
|
185
|
+
*
|
|
186
|
+
* The stream-broker auto-suspends idle streams on the "no demand"
|
|
187
|
+
* signal; snapshots used to default to `high` which was often the
|
|
188
|
+
* first to go idle, racing every snapshot with a broker resume.
|
|
189
|
+
* Now we ask the orchestrator of streams which one is warm and grab
|
|
190
|
+
* from there.
|
|
191
|
+
*/
|
|
192
|
+
async grabFrameFromBroker(deviceId, preferredStreamId) {
|
|
193
|
+
const dev = await this.ctx.fetchDevice(deviceId);
|
|
194
|
+
const prefix = `${deviceId}/`;
|
|
195
|
+
const [deviceEntries, profileSlots] = await Promise.all([
|
|
196
|
+
dev.cameraStreams?.getRtspEntries({}) ?? [],
|
|
197
|
+
dev.cameraStreams?.getBrokerStreams({}) ?? []
|
|
198
|
+
]);
|
|
199
|
+
const usable = deviceEntries.filter((e) => e.enabled && !!e.url);
|
|
200
|
+
if (usable.length === 0) return null;
|
|
201
|
+
if (preferredStreamId && preferredStreamId !== "auto") {
|
|
202
|
+
const explicit = usable.find((e) => e.brokerId === `${prefix}${preferredStreamId}`);
|
|
203
|
+
if (explicit) {
|
|
204
|
+
const grabbed = await this.runGrabWithResumeRetry(explicit.url, deviceId);
|
|
205
|
+
if (grabbed) return grabbed;
|
|
206
|
+
this.ctx.logger.debug("grabFrame: explicit stream failed \u2014 falling back to auto", { meta: { preferredStreamId } });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const ranked = [...usable].sort(
|
|
210
|
+
(a, b) => qualityRank(b.brokerId, prefix) - qualityRank(a.brokerId, prefix)
|
|
211
|
+
);
|
|
212
|
+
const statusByBrokerId = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const slot of profileSlots) statusByBrokerId.set(slot.brokerId, slot.status);
|
|
214
|
+
const statuses = ranked.map((e) => ({ entry: e, status: statusByBrokerId.get(e.brokerId) ?? "idle" }));
|
|
215
|
+
const warm = statuses.find((s) => s.status === "streaming");
|
|
216
|
+
if (warm) {
|
|
217
|
+
const grabbed = await this.runGrabWithResumeRetry(warm.entry.url, deviceId);
|
|
218
|
+
if (grabbed) return grabbed;
|
|
219
|
+
}
|
|
220
|
+
for (const { entry } of statuses) {
|
|
221
|
+
const grabbed = await this.runGrabWithResumeRetry(entry.url, deviceId).catch(() => null);
|
|
222
|
+
if (grabbed) return grabbed;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Ffmpeg grab with one retry on the broker-cold-start error
|
|
228
|
+
* signature. Covers the window between "client connected" and
|
|
229
|
+
* "first keyframe" when a suspended broker resumes.
|
|
230
|
+
*/
|
|
231
|
+
async runGrabWithResumeRetry(url, deviceId) {
|
|
232
|
+
let buf;
|
|
233
|
+
try {
|
|
234
|
+
buf = await runFfmpegFrameGrab(url, 15e3);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const msg = (0, import_types2.errMsg)(err);
|
|
237
|
+
if (isBrokerColdError(msg)) {
|
|
238
|
+
this.ctx.logger.debug("grabFrame: broker-resume race \u2014 retrying in 1500ms", { tags: { deviceId } });
|
|
239
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
240
|
+
buf = await runFfmpegFrameGrab(url, 15e3);
|
|
241
|
+
} else {
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (buf.length === 0) return null;
|
|
246
|
+
return { base64: buf.toString("base64"), contentType: "image/jpeg" };
|
|
247
|
+
}
|
|
248
|
+
async invalidateCache(input) {
|
|
249
|
+
this.cache.delete(input.deviceId);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Non-throwing probe of the device's battery cap. Returns true only
|
|
253
|
+
* when a battery native is registered AND its current status says
|
|
254
|
+
* `sleeping: true`. Any error (no provider, native absent, getStatus
|
|
255
|
+
* missing, RPC timeout) is swallowed and treated as "awake" — we'd
|
|
256
|
+
* rather pay a wake-up than strand the caller on a cache that's
|
|
257
|
+
* semantically stale. Debug-logged for observability.
|
|
258
|
+
*/
|
|
259
|
+
/**
|
|
260
|
+
* True when at least one of the device's brokers is actively
|
|
261
|
+
* streaming (status === 'streaming'). Used by the battery-cam guard
|
|
262
|
+
* around `grabFrameFromBroker` to allow the fallback ONLY when
|
|
263
|
+
* grabbing a frame is free (a consumer is already keeping the
|
|
264
|
+
* stream warm). When everything is suspended, the fallback would
|
|
265
|
+
* dial the camera and wake it — defeats the sleeping cache.
|
|
266
|
+
*/
|
|
267
|
+
async hasStreamingBrokerForDevice(deviceId) {
|
|
268
|
+
try {
|
|
269
|
+
const dev = await this.ctx.fetchDevice(deviceId);
|
|
270
|
+
const slots = await dev.cameraStreams?.getBrokerStreams({}) ?? [];
|
|
271
|
+
return slots.some((s) => s.status === "streaming");
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Diagnostic status for the `status` auto-injected cap method. Reports
|
|
278
|
+
* the cache bookkeeping for this device — when the last snapshot was
|
|
279
|
+
* captured, how stale the cached image is, its size, and which stream
|
|
280
|
+
* was used. Returns null when the device has never been captured
|
|
281
|
+
* (cache miss) since the addon started.
|
|
282
|
+
*/
|
|
283
|
+
async getStatus(deviceId) {
|
|
284
|
+
const hit = this.cache.get(deviceId);
|
|
285
|
+
if (!hit) return null;
|
|
286
|
+
return {
|
|
287
|
+
lastCapturedAt: hit.ts,
|
|
288
|
+
cacheAgeMs: Date.now() - hit.ts,
|
|
289
|
+
lastBytes: hit.data.base64.length,
|
|
290
|
+
// approx bytes = base64 length * 3/4, but length is stable enough
|
|
291
|
+
lastStreamId: hit.streamId
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// ── DeviceSettingsContribution ────────────────────────────────────────
|
|
295
|
+
//
|
|
296
|
+
// Snapshot carries two per-device knobs:
|
|
297
|
+
// - `snapshotStreamId` — which stream to prefer when grabbing frames
|
|
298
|
+
// (empty = auto / default "main")
|
|
299
|
+
// - `snapshotDebug` — extra logging for troubleshooting snapshot paths
|
|
300
|
+
//
|
|
301
|
+
// Storage uses the addon's own per-device store (`ctx.settings
|
|
302
|
+
// .writeDeviceStore`), not the device's config. The schema isn't
|
|
303
|
+
// captured in a Zod object: the keys are optional and the UI layer
|
|
304
|
+
// validates shapes.
|
|
305
|
+
async readDeviceSettings(deviceId) {
|
|
306
|
+
if (!this.ctx.settings) return {};
|
|
307
|
+
const raw = await this.ctx.settings.readDeviceStore(deviceId);
|
|
308
|
+
const rawAge = raw["snapshotMaxAgeS"];
|
|
309
|
+
const maxAgeS = typeof rawAge === "number" && rawAge >= 0 && Number.isFinite(rawAge) ? rawAge : void 0;
|
|
310
|
+
return {
|
|
311
|
+
snapshotStreamId: typeof raw["snapshotStreamId"] === "string" ? raw["snapshotStreamId"] : void 0,
|
|
312
|
+
snapshotDebug: raw["snapshotDebug"] === true,
|
|
313
|
+
...maxAgeS !== void 0 ? { snapshotMaxAgeS: maxAgeS } : {}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async buildDeviceSettingsContribution(deviceId) {
|
|
317
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
318
|
+
if (meta && meta.type !== import_types.DeviceType.Camera) return null;
|
|
319
|
+
const current = await this.readDeviceSettings(deviceId);
|
|
320
|
+
const streamOptions = await this.getStreamOptions(deviceId);
|
|
321
|
+
const isBattery = await this.isDeviceBattery(deviceId);
|
|
322
|
+
const defaultMaxAgeS = isBattery ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S;
|
|
323
|
+
return {
|
|
324
|
+
sections: [{
|
|
325
|
+
id: "snapshot-preferences",
|
|
326
|
+
title: "Snapshot",
|
|
327
|
+
tab: "snapshot",
|
|
328
|
+
order: 60,
|
|
329
|
+
fields: [
|
|
330
|
+
{
|
|
331
|
+
type: "select",
|
|
332
|
+
key: "snapshotStreamId",
|
|
333
|
+
label: "Preferred stream",
|
|
334
|
+
description: "Stream used when grabbing a snapshot",
|
|
335
|
+
options: streamOptions,
|
|
336
|
+
required: true,
|
|
337
|
+
value: current.snapshotStreamId || "auto"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
type: "number",
|
|
341
|
+
key: "snapshotMaxAgeS",
|
|
342
|
+
label: "Max cache age (s)",
|
|
343
|
+
description: `Serve cached snapshot up to this age before re-capturing. Default ${defaultMaxAgeS}s for ${isBattery ? "battery cams (avoids gratuitous wake-ups)" : "non-battery cams (live-feel)"}. The UI refresh button always forces a fresh capture regardless of this value.`,
|
|
344
|
+
min: 0,
|
|
345
|
+
max: 24 * 3600,
|
|
346
|
+
step: 1,
|
|
347
|
+
value: current.snapshotMaxAgeS ?? defaultMaxAgeS
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: "boolean",
|
|
351
|
+
key: "snapshotDebug",
|
|
352
|
+
label: "Debug logging",
|
|
353
|
+
description: "Log stream selection and timing details for this device.",
|
|
354
|
+
value: current.snapshotDebug ?? false
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
}]
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Single-trip device lookup against device-manager. Returns the
|
|
362
|
+
* fields the wrapper actually consults — name (logging) + battery
|
|
363
|
+
* flag (cache window + broker-fallback gate). Sourced from the
|
|
364
|
+
* device-manager registry rather than the battery cap so the answer
|
|
365
|
+
* survives a momentarily-unreachable provider (the very condition
|
|
366
|
+
* we're trying to be resilient to).
|
|
367
|
+
*
|
|
368
|
+
* Logged at debug + null return on failure: every call site already
|
|
369
|
+
* has a sensible fallback path (cache hit, conservative default, …),
|
|
370
|
+
* so we don't want a transient device-manager hiccup to throw.
|
|
371
|
+
*/
|
|
372
|
+
async lookupDeviceMeta(deviceId) {
|
|
373
|
+
const api = this.ctx.api;
|
|
374
|
+
if (!api) return null;
|
|
375
|
+
try {
|
|
376
|
+
const found = await api.deviceManager.getDevice.query({ deviceId });
|
|
377
|
+
if (!found) return null;
|
|
378
|
+
const features = found.features ?? [];
|
|
379
|
+
const rawType = found.type;
|
|
380
|
+
return {
|
|
381
|
+
...found.name ? { name: found.name } : {},
|
|
382
|
+
isBattery: features.includes(import_types.DeviceFeature.BatteryOperated),
|
|
383
|
+
...rawType ? { type: rawType } : {}
|
|
384
|
+
};
|
|
385
|
+
} catch (err) {
|
|
386
|
+
this.ctx.logger.debug("deviceManager.getDevice failed during snapshot", {
|
|
387
|
+
tags: { deviceId },
|
|
388
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
389
|
+
});
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** Settings-UI helper — battery flag drives the default max-age in the field description. */
|
|
394
|
+
async isDeviceBattery(deviceId) {
|
|
395
|
+
return (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
|
|
396
|
+
}
|
|
397
|
+
async getStreamOptions(deviceId) {
|
|
398
|
+
const prefix = `${deviceId}/`;
|
|
399
|
+
try {
|
|
400
|
+
const dev = await this.ctx.fetchDevice(deviceId);
|
|
401
|
+
const entries = await dev.cameraStreams?.getRtspEntries({}) ?? [];
|
|
402
|
+
const streamIds = entries.filter((e) => e.enabled).map((e) => e.brokerId.slice(prefix.length));
|
|
403
|
+
return [
|
|
404
|
+
{ value: "auto", label: "Auto" },
|
|
405
|
+
...streamIds.map((id) => ({ value: id, label: (0, import_types2.streamQualityLabel)(id) }))
|
|
406
|
+
];
|
|
407
|
+
} catch (err) {
|
|
408
|
+
this.ctx.logger.error("getStreamOptions failed", { tags: { deviceId }, meta: { error: (0, import_types2.errMsg)(err) } });
|
|
409
|
+
return [{ value: "auto", label: "Auto" }];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async saveDeviceSettingsPatch(deviceId, patch) {
|
|
413
|
+
if (!this.ctx.settings) {
|
|
414
|
+
throw new Error("[snapshot] settings store unavailable \u2014 cannot persist per-device settings");
|
|
415
|
+
}
|
|
416
|
+
const current = await this.ctx.settings.readDeviceStore(deviceId);
|
|
417
|
+
const next = { ...current };
|
|
418
|
+
if ("snapshotStreamId" in patch) {
|
|
419
|
+
const v = patch["snapshotStreamId"];
|
|
420
|
+
next["snapshotStreamId"] = typeof v === "string" && v.trim().length > 0 ? v.trim() : "";
|
|
421
|
+
}
|
|
422
|
+
if ("snapshotDebug" in patch) {
|
|
423
|
+
next["snapshotDebug"] = patch["snapshotDebug"] === true;
|
|
424
|
+
}
|
|
425
|
+
if ("snapshotMaxAgeS" in patch) {
|
|
426
|
+
const v = patch["snapshotMaxAgeS"];
|
|
427
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
|
|
428
|
+
next["snapshotMaxAgeS"] = v;
|
|
429
|
+
} else {
|
|
430
|
+
delete next["snapshotMaxAgeS"];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
await this.ctx.settings.writeDeviceStore(deviceId, next);
|
|
434
|
+
this.cache.delete(deviceId);
|
|
435
|
+
return { success: true };
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
function isAbsentNativeError(msg) {
|
|
439
|
+
return msg.includes("no provider for") || msg.includes("no native provider for capability");
|
|
440
|
+
}
|
|
441
|
+
function qualityRank(brokerId, prefix) {
|
|
442
|
+
const streamId = brokerId.startsWith(prefix) ? brokerId.slice(prefix.length) : brokerId;
|
|
443
|
+
const normalized = streamId.toLowerCase();
|
|
444
|
+
if (normalized.includes("high") || normalized === "main" || normalized === "hd") return 3;
|
|
445
|
+
if (normalized.includes("mid") || normalized === "medium") return 2;
|
|
446
|
+
if (normalized.includes("low") || normalized === "sub" || normalized === "sd") return 1;
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
function isBrokerColdError(msg) {
|
|
450
|
+
return msg.includes("Invalid data found when processing input") || msg.includes("Error opening input") || msg.includes("Connection refused") || msg.includes("No route to host");
|
|
451
|
+
}
|
|
452
|
+
function runFfmpegFrameGrab(url, timeoutMs) {
|
|
453
|
+
return new Promise((resolve, reject) => {
|
|
454
|
+
const child = (0, import_node_child_process.execFile)(
|
|
455
|
+
"ffmpeg",
|
|
456
|
+
[
|
|
457
|
+
"-loglevel",
|
|
458
|
+
"error",
|
|
459
|
+
"-rtsp_transport",
|
|
460
|
+
"tcp",
|
|
461
|
+
"-fflags",
|
|
462
|
+
"+discardcorrupt",
|
|
463
|
+
"-skip_frame",
|
|
464
|
+
"nokey",
|
|
465
|
+
"-i",
|
|
466
|
+
url,
|
|
467
|
+
"-vf",
|
|
468
|
+
"select=eq(pict_type\\,I)",
|
|
469
|
+
"-vsync",
|
|
470
|
+
"vfr",
|
|
471
|
+
"-frames:v",
|
|
472
|
+
"1",
|
|
473
|
+
"-q:v",
|
|
474
|
+
"3",
|
|
475
|
+
"-f",
|
|
476
|
+
"image2pipe",
|
|
477
|
+
"-vcodec",
|
|
478
|
+
"mjpeg",
|
|
479
|
+
"pipe:1"
|
|
480
|
+
],
|
|
481
|
+
{ encoding: "buffer", maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs },
|
|
482
|
+
(err, stdout, stderr) => {
|
|
483
|
+
if (err) {
|
|
484
|
+
const errWithMeta = err;
|
|
485
|
+
const stderrText = Buffer.isBuffer(stderr) ? stderr.toString("utf8").trim() : String(stderr ?? "").trim();
|
|
486
|
+
const parts = [err instanceof Error ? err.message : String(err)];
|
|
487
|
+
if (errWithMeta.killed) parts.push("killed=true");
|
|
488
|
+
if (errWithMeta.code !== void 0) parts.push(`code=${String(errWithMeta.code)}`);
|
|
489
|
+
if (errWithMeta.signal) parts.push(`signal=${errWithMeta.signal}`);
|
|
490
|
+
if (stderrText) parts.push(`stderr: ${stderrText.slice(0, 500)}`);
|
|
491
|
+
reject(new Error(parts.join(" \u2014 ")));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
child.on("error", (e) => reject(e));
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
501
|
+
0 && (module.exports = {
|
|
502
|
+
SnapshotAddon
|
|
503
|
+
});
|
|
504
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/builtins/snapshot/index.ts","../../../src/builtins/snapshot/snapshot.addon.ts"],"sourcesContent":["export { SnapshotAddon } from './snapshot.addon.js'\nexport { SnapshotAddon as default } from './snapshot.addon.js'\n","import { execFile } from 'node:child_process'\nimport { DeviceFeature, DeviceType, snapshotCapability } from '@camstack/types'\nimport type { ConfigUISchemaWithValues, InferProvider, ProfileSlot, ProviderRegistration } from '@camstack/types'\nimport { BaseAddon, errMsg, streamQualityLabel } from '@camstack/types'\n\ntype ISnapshotProvider = InferProvider<typeof snapshotCapability>\ntype SnapshotImage = { base64: string; contentType: string }\n\n/**\n * Per-device snapshot settings stored via `ctx.settings.writeDeviceStore`.\n * Kept as a plain interface (not Zod) because the value set is tiny and\n * `snapshot` isn't worth the extra schema plumbing — runtime coercion\n * happens inline in `getDeviceStoreTyped` below.\n */\ninterface SnapshotDeviceSettings {\n /** Quality profile to prefer when grabbing a snapshot (e.g. `high`, `mid`). Empty = auto. */\n readonly snapshotStreamId?: string\n /** If true, log extra detail (timing, selected stream, error paths). */\n readonly snapshotDebug?: boolean\n /**\n * Maximum acceptable cache age in seconds. Below it the wrapper\n * serves the cached image; above it (or on `force`) it falls\n * through to the native (which on battery cams wakes the\n * firmware). When unset the per-device default applies:\n * `BATTERY_DEFAULT_MAX_AGE_S` for battery cams and\n * `NON_BATTERY_DEFAULT_MAX_AGE_S` for everything else.\n */\n readonly snapshotMaxAgeS?: number\n}\n\n/** Default cache window for non-battery cams (seconds). 10s feels live. */\nconst NON_BATTERY_DEFAULT_MAX_AGE_S = 10\n/** Default cache window for battery cams (seconds). 1h ≈ \"don't wake the cam unless asked\". */\nconst BATTERY_DEFAULT_MAX_AGE_S = 3600\n\ninterface CacheEntry {\n readonly data: SnapshotImage\n readonly ts: number\n /** Stream id resolved at capture time — 'high' | 'mid' | 'low' | custom | null. */\n readonly streamId: string | null\n}\n\ninterface SnapshotAddonConfig {\n /**\n * Last-resort cache age (ms): if every live capture path fails\n * (native + stream-broker fallback) AND a stale entry is older\n * than this, the wrapper still returns the stale image rather\n * than `null`. Keeps the UI from going blank during transient\n * camera unreachability. Per-device freshness is governed by\n * `snapshotMaxAgeS` instead.\n */\n readonly staleTtlMs: number\n}\n\n/**\n * SnapshotAddon — wrapper over the `snapshot` capability.\n *\n * Activated per-device (toggleable by user; default active). When active,\n * caches fresh snapshots in memory. On cache miss, delegates to the native\n * provider for this device via `ctx.getNativeProvider(snapshotCapability, id)`.\n *\n * No silent fallback — if the native fails, propagate the error up. Stale\n * cache is returned ONLY when the native throws AND a stale entry exists,\n * and the failure is logged so the fallback is never silent.\n *\n * Frame-grab fallback: when the native provider is absent (or returns null /\n * throws), we ask the `stream-broker` capability for the device's RTSP\n * restream URL and pipe one JPEG out of ffmpeg. stream-broker intentionally\n * does NOT expose a `grabFrame` method — the orchestration (native-first,\n * ffmpeg fallback, caching) is this addon's concern; stream-broker only\n * publishes stream endpoints.\n */\nexport class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {\n private readonly cache = new Map<number, CacheEntry>()\n\n constructor() {\n super({\n staleTtlMs: 60_000,\n })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('Snapshot wrapper initialized')\n const provider: ISnapshotProvider = {\n getSnapshot: (input) => this.getSnapshot(input),\n invalidateCache: (input) => this.invalidateCache(input),\n // DeviceSettingsContribution surface — numeric deviceId end-to-end.\n getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),\n getDeviceLiveContribution: async () => null,\n applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),\n // Status surface — derives a diagnostic snapshot from the cache\n // bookkeeping (lastCapturedAt, cacheAgeMs, lastBytes, lastStreamId).\n // Returns null when the device has never been captured.\n getStatus: async (input) => this.getStatus(input.deviceId),\n }\n return [{\n capability: snapshotCapability,\n provider,\n kind: 'wrapper',\n defaultActive: true,\n }]\n }\n\n protected async onShutdown(): Promise<void> {\n this.cache.clear()\n }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'snapshot-cache',\n title: 'Snapshot Cache',\n description: 'Stale fallback when live capture fails. Per-device freshness is configured under Device → Snapshot → Max cache age.',\n columns: 1,\n fields: [\n this.field({\n type: 'number', key: 'staleTtlMs', label: 'Stale fallback TTL (ms)',\n description: 'If live capture fails, cached snapshot younger than this is still returned.',\n min: 0, max: 3_600_000, step: 1_000, default: 60_000,\n }),\n ],\n }],\n })\n }\n\n // ── Capability methods ────────────────────────────────────────────────\n\n private async getSnapshot(input: { deviceId: number; streamId?: string; force?: boolean }): Promise<SnapshotImage | null> {\n const { deviceId, force } = input\n // Pull device name + battery feature with a single device-scoped\n // query. `getDevice` is cheaper than `listAll().find(...)` (one row\n // returned, no client-side filter) and surfaces the same fields.\n const meta = await this.lookupDeviceMeta(deviceId)\n const deviceName = meta?.name\n const isBatteryDevice = meta?.isBattery ?? false\n const log = this.ctx.logger.withTags({ deviceId, ...(deviceName ? { deviceName } : {}) })\n const now = Date.now()\n const hit = this.cache.get(deviceId)\n\n // Honour the per-device stored preference unless the caller specified\n // an explicit streamId. `snapshotDebug` just flips log verbosity —\n // read once per call so the flag can be toggled without restart.\n const prefs: SnapshotDeviceSettings = await this.readDeviceSettings(deviceId).catch(() => ({}))\n const rawPref = prefs.snapshotStreamId\n const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== 'auto' ? rawPref : undefined)\n if (prefs.snapshotDebug) {\n log.info('debug on', { tags: { deviceId }, meta: { stream: effectiveStreamId ?? 'auto' } })\n }\n\n // 1. Effective max-age check. A single per-device knob replaces\n // the legacy fresh/battery/sleeping cascade — defaults are 10s\n // for non-battery cams (matches the live-feel UI poll cadence)\n // and 1h for battery cams (avoids gratuitous wake-ups). The\n // operator can override via the per-device `snapshotMaxAgeS`\n // setting; a `force=true` call (UI refresh button) bypasses\n // the gate entirely. Behaviour matrix:\n //\n // hit + ageMs < maxAgeMs + !force → serve cache\n // hit + sleeping + ageMs < maxAgeMs + !force → serve cache (no wake)\n // hit + sleeping + ageMs >= maxAgeMs + !force → cache is stale,\n // let the native\n // decide whether\n // to wake (the\n // native cap layer\n // handles the\n // \"are we awake?\"\n // question)\n // no hit → fall through (will wake)\n // force === true → fall through (will wake)\n const defaultMaxAgeS = isBatteryDevice ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S\n const effectiveMaxAgeMs = (typeof prefs.snapshotMaxAgeS === 'number' && prefs.snapshotMaxAgeS >= 0\n ? prefs.snapshotMaxAgeS\n : defaultMaxAgeS) * 1_000\n if (!force && hit && now - hit.ts < effectiveMaxAgeMs) {\n if (prefs.snapshotDebug) {\n log.debug('snapshot: cache hit', {\n tags: { deviceId },\n meta: { ageMs: now - hit.ts, maxAgeMs: effectiveMaxAgeMs, isBattery: isBatteryDevice },\n })\n }\n return hit.data\n }\n\n // 2. Native provider (per-device, registered by the device-class addon).\n // Missing native is expected for cameras without a vendor snapshot\n // endpoint (e.g. a plain RTSP camera with no `snapshotUrl` configured).\n // We catch and fall through to the stream-broker ffmpeg path.\n let nativeError: unknown = null\n let nativeAbsent = false\n try {\n const native = this.ctx.getNativeProvider(snapshotCapability, deviceId)\n if (native) {\n const result = await native.getSnapshot(input)\n if (result) {\n this.cache.set(deviceId, { data: result, ts: now, streamId: effectiveStreamId ?? null })\n return result\n }\n } else {\n nativeAbsent = true\n }\n } catch (err) {\n const msg = errMsg(err)\n if (isAbsentNativeError(msg)) {\n nativeAbsent = true\n log.debug('native snapshot absent', { tags: { deviceId }, meta: { error: msg } })\n } else {\n nativeError = err\n log.warn('native snapshot failed', { tags: { deviceId }, meta: { error: msg } })\n }\n }\n\n // 3. Frame-grab fallback via stream-broker's RTSP restream. Passes\n // the effective streamId (explicit caller or stored preference) so\n // the fallback hits the same stream the user asked for instead of\n // always grabbing `main`.\n //\n // Battery-cam guard: skip the broker fallback only when the native\n // was ABSENT for a battery device — that's the case where invoking\n // the broker would be the first wake-up signal. If the native was\n // present (whether it succeeded with null, threw, or returned a\n // value), the camera is already being talked to (and likely awake);\n // the additional broker dial costs nothing extra. When a streaming\n // broker is already active, the fallback is unconditionally free\n // (someone is watching), so allow it regardless.\n const skipBrokerForBattery = isBatteryDevice\n && nativeAbsent\n && !(await this.hasStreamingBrokerForDevice(deviceId))\n if (!skipBrokerForBattery) {\n try {\n const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId)\n if (fallback) {\n this.cache.set(deviceId, { data: fallback, ts: now, streamId: effectiveStreamId ?? null })\n return fallback\n }\n } catch (err) {\n log.warn('stream-broker snapshot fallback failed', { tags: { deviceId }, meta: { error: errMsg(err) } })\n }\n } else {\n log.debug('snapshot: skipping broker fallback — battery device with absent native and no streaming broker', {\n tags: { deviceId },\n })\n }\n\n // 4. Stale cache — last-resort \"don't go blank\" path.\n //\n // If ALL live paths failed and we have any cached entry, return\n // it and log the age. The UI keeps showing the last known frame\n // rather than a broken-image icon. The freshness gate at step 1\n // (per-device `snapshotMaxAgeS`) is the cap that decides whether\n // the cache is \"fresh enough\" in normal flow; this branch only\n // runs when every live source refused.\n if (hit) {\n const ageMs = now - hit.ts\n if (ageMs > this.config.staleTtlMs) {\n log.warn('snapshot: all live paths failed — serving stale cache', { tags: { deviceId }, meta: { ageMs } })\n }\n return hit.data\n }\n\n // Nothing produced a frame and nothing in cache. If the native provider\n // threw a real error (not just \"no provider registered for this\n // device\"), surface it so callers see the cause. If the native was\n // simply absent (camera has no snapshotUrl + no wrapper active),\n // return null so the UI renders \"no image yet\" instead of a 500.\n if (nativeError) throw nativeError\n if (nativeAbsent) return null\n return null\n }\n\n /**\n * Tell apart \"native provider isn't registered for this device\" from\n * \"native provider ran and threw a real error\". The former is the steady\n * state for cameras without a vendor snapshot endpoint and should not\n * propagate as a 500; the latter should.\n */\n\n /**\n * Pull one JPEG from the device's stream-broker RTSP restream using\n * a short-lived ffmpeg invocation.\n *\n * Stream selection strategy (picks the broker that won't stall):\n * 1. Explicit `preferredStreamId` (user set in per-device settings)\n * — always honoured, even if currently idle. Operator choice\n * wins.\n * 2. Auto: highest-quality broker currently in `streaming` state.\n * This is the whole point — prefer the stream that has active\n * subscribers (usually `low` for detection, but `mid`/`high`\n * if WebRTC is watching) so ffmpeg hits a warm pipe and\n * doesn't race the broker's resume.\n * 3. Fallback: highest-quality enabled entry regardless of status\n * (will wake a suspended broker — retry-guarded against the\n * cold-start error).\n *\n * The stream-broker auto-suspends idle streams on the \"no demand\"\n * signal; snapshots used to default to `high` which was often the\n * first to go idle, racing every snapshot with a broker resume.\n * Now we ask the orchestrator of streams which one is warm and grab\n * from there.\n */\n private async grabFrameFromBroker(\n deviceId: number,\n preferredStreamId?: string,\n ): Promise<SnapshotImage | null> {\n const dev = await this.ctx.fetchDevice(deviceId)\n const prefix = `${deviceId}/`\n\n // Device-scoped fetch: broker filters by deviceId server-side, no\n // cluster-wide list + client-side filter needed. Profile slot\n // statuses come from the same per-device facade for the warmth\n // ranking below.\n const [deviceEntries, profileSlots] = await Promise.all([\n dev.cameraStreams?.getRtspEntries({}) ?? [],\n dev.cameraStreams?.getBrokerStreams({}) ?? [],\n ])\n const usable = deviceEntries.filter(e => e.enabled && !!e.url)\n if (usable.length === 0) return null\n\n // 1. Explicit preference — try it first without status gating.\n if (preferredStreamId && preferredStreamId !== 'auto') {\n const explicit = usable.find(e => e.brokerId === `${prefix}${preferredStreamId}`)\n if (explicit) {\n const grabbed = await this.runGrabWithResumeRetry(explicit.url, deviceId)\n if (grabbed) return grabbed\n // Explicit pick failed even after retry — fall through to auto\n // rather than give up. Keeps the snapshot useful when the\n // operator's preferred stream is momentarily unreachable.\n this.ctx.logger.debug('grabFrame: explicit stream failed — falling back to auto', { meta: { preferredStreamId } })\n }\n }\n\n // Rank entries by quality (high → mid → low → other).\n const ranked = [...usable].sort(\n (a, b) => qualityRank(b.brokerId, prefix) - qualityRank(a.brokerId, prefix),\n )\n\n // Per-device slot status lookup keyed by brokerId, sourced from\n // the same `cameraStreams` facade — `status: 'streaming'` means the\n // broker is warm and an ffmpeg grab is free of dial overhead.\n const statusByBrokerId = new Map<string, ProfileSlot['status']>()\n for (const slot of profileSlots) statusByBrokerId.set(slot.brokerId, slot.status)\n const statuses = ranked.map(e => ({ entry: e, status: statusByBrokerId.get(e.brokerId) ?? 'idle' as const }))\n\n // 2. Highest-quality broker currently `streaming`.\n const warm = statuses.find(s => s.status === 'streaming')\n if (warm) {\n const grabbed = await this.runGrabWithResumeRetry(warm.entry.url, deviceId)\n if (grabbed) return grabbed\n }\n\n // 3. Final fallback: wake whichever broker has the highest quality.\n // The retry handles the resume race window.\n for (const { entry } of statuses) {\n const grabbed = await this.runGrabWithResumeRetry(entry.url, deviceId).catch(() => null)\n if (grabbed) return grabbed\n }\n return null\n }\n\n /**\n * Ffmpeg grab with one retry on the broker-cold-start error\n * signature. Covers the window between \"client connected\" and\n * \"first keyframe\" when a suspended broker resumes.\n */\n private async runGrabWithResumeRetry(url: string, deviceId: number): Promise<SnapshotImage | null> {\n let buf: Buffer\n try {\n buf = await runFfmpegFrameGrab(url, 15000)\n } catch (err) {\n const msg = errMsg(err)\n if (isBrokerColdError(msg)) {\n this.ctx.logger.debug('grabFrame: broker-resume race — retrying in 1500ms', { tags: { deviceId } })\n await new Promise((r) => setTimeout(r, 1500))\n buf = await runFfmpegFrameGrab(url, 15000)\n } else {\n throw err\n }\n }\n if (buf.length === 0) return null\n return { base64: buf.toString('base64'), contentType: 'image/jpeg' }\n }\n\n private async invalidateCache(input: { deviceId: number }): Promise<void> {\n this.cache.delete(input.deviceId)\n }\n\n /**\n * Non-throwing probe of the device's battery cap. Returns true only\n * when a battery native is registered AND its current status says\n * `sleeping: true`. Any error (no provider, native absent, getStatus\n * missing, RPC timeout) is swallowed and treated as \"awake\" — we'd\n * rather pay a wake-up than strand the caller on a cache that's\n * semantically stale. Debug-logged for observability.\n */\n /**\n * True when at least one of the device's brokers is actively\n * streaming (status === 'streaming'). Used by the battery-cam guard\n * around `grabFrameFromBroker` to allow the fallback ONLY when\n * grabbing a frame is free (a consumer is already keeping the\n * stream warm). When everything is suspended, the fallback would\n * dial the camera and wake it — defeats the sleeping cache.\n */\n private async hasStreamingBrokerForDevice(deviceId: number): Promise<boolean> {\n // `cameraStreams` is the device-scoped facade over the system\n // `stream-broker` cap — `getBrokerStreams` returns this device's\n // profile slots only (no cluster-wide scan + filter).\n try {\n const dev = await this.ctx.fetchDevice(deviceId)\n const slots = await dev.cameraStreams?.getBrokerStreams({}) ?? []\n return slots.some(s => s.status === 'streaming')\n } catch {\n return false\n }\n }\n\n /**\n * Diagnostic status for the `status` auto-injected cap method. Reports\n * the cache bookkeeping for this device — when the last snapshot was\n * captured, how stale the cached image is, its size, and which stream\n * was used. Returns null when the device has never been captured\n * (cache miss) since the addon started.\n */\n private async getStatus(deviceId: number): Promise<{\n lastCapturedAt: number | null\n cacheAgeMs: number | null\n lastBytes: number | null\n lastStreamId: string | null\n } | null> {\n const hit = this.cache.get(deviceId)\n if (!hit) return null\n return {\n lastCapturedAt: hit.ts,\n cacheAgeMs: Date.now() - hit.ts,\n lastBytes: hit.data.base64.length, // approx bytes = base64 length * 3/4, but length is stable enough\n lastStreamId: hit.streamId,\n }\n }\n\n // ── DeviceSettingsContribution ────────────────────────────────────────\n //\n // Snapshot carries two per-device knobs:\n // - `snapshotStreamId` — which stream to prefer when grabbing frames\n // (empty = auto / default \"main\")\n // - `snapshotDebug` — extra logging for troubleshooting snapshot paths\n //\n // Storage uses the addon's own per-device store (`ctx.settings\n // .writeDeviceStore`), not the device's config. The schema isn't\n // captured in a Zod object: the keys are optional and the UI layer\n // validates shapes.\n\n private async readDeviceSettings(deviceId: number): Promise<SnapshotDeviceSettings> {\n if (!this.ctx.settings) return {}\n const raw = await this.ctx.settings.readDeviceStore(deviceId)\n const rawAge = raw['snapshotMaxAgeS']\n const maxAgeS = typeof rawAge === 'number' && rawAge >= 0 && Number.isFinite(rawAge)\n ? rawAge\n : undefined\n return {\n snapshotStreamId: typeof raw['snapshotStreamId'] === 'string' ? raw['snapshotStreamId'] : undefined,\n snapshotDebug: raw['snapshotDebug'] === true,\n ...(maxAgeS !== undefined ? { snapshotMaxAgeS: maxAgeS } : {}),\n }\n }\n\n private async buildDeviceSettingsContribution(deviceId: number): Promise<ConfigUISchemaWithValues | null> {\n // Camera-only — snapshot settings are meaningless on Lights /\n // Switches / Sensors / Buttons, and surfacing the section there\n // produces ghost top-tabs (the device-detail UI builds the tab\n // list off the contribution sections). Source-side gate keeps the\n // policy with the addon that owns the cap; the admin-ui no longer\n // needs the symmetric `device-section-policy` allow-list.\n const meta = await this.lookupDeviceMeta(deviceId)\n if (meta && meta.type !== DeviceType.Camera) return null\n const current = await this.readDeviceSettings(deviceId)\n const streamOptions = await this.getStreamOptions(deviceId)\n const isBattery = await this.isDeviceBattery(deviceId)\n const defaultMaxAgeS = isBattery ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S\n return {\n sections: [{\n id: 'snapshot-preferences',\n title: 'Snapshot',\n tab: 'snapshot',\n order: 60,\n fields: [\n {\n type: 'select' as const,\n key: 'snapshotStreamId',\n label: 'Preferred stream',\n description: 'Stream used when grabbing a snapshot',\n options: streamOptions,\n required: true,\n value: current.snapshotStreamId || 'auto',\n },\n {\n type: 'number' as const,\n key: 'snapshotMaxAgeS',\n label: 'Max cache age (s)',\n description:\n `Serve cached snapshot up to this age before re-capturing. Default ${defaultMaxAgeS}s for ` +\n `${isBattery ? 'battery cams (avoids gratuitous wake-ups)' : 'non-battery cams (live-feel)'}. ` +\n 'The UI refresh button always forces a fresh capture regardless of this value.',\n min: 0,\n max: 24 * 3600,\n step: 1,\n value: current.snapshotMaxAgeS ?? defaultMaxAgeS,\n },\n {\n type: 'boolean' as const,\n key: 'snapshotDebug',\n label: 'Debug logging',\n description: 'Log stream selection and timing details for this device.',\n value: current.snapshotDebug ?? false,\n },\n ],\n }],\n }\n }\n\n /**\n * Single-trip device lookup against device-manager. Returns the\n * fields the wrapper actually consults — name (logging) + battery\n * flag (cache window + broker-fallback gate). Sourced from the\n * device-manager registry rather than the battery cap so the answer\n * survives a momentarily-unreachable provider (the very condition\n * we're trying to be resilient to).\n *\n * Logged at debug + null return on failure: every call site already\n * has a sensible fallback path (cache hit, conservative default, …),\n * so we don't want a transient device-manager hiccup to throw.\n */\n private async lookupDeviceMeta(\n deviceId: number,\n ): Promise<{ name?: string; isBattery: boolean; type?: DeviceType } | null> {\n const api = this.ctx.api\n if (!api) return null\n try {\n const found = await api.deviceManager.getDevice.query({ deviceId })\n if (!found) return null\n const features = (found.features as readonly string[] | undefined) ?? []\n const rawType = (found as { type?: string }).type\n return {\n ...(found.name ? { name: found.name } : {}),\n isBattery: features.includes(DeviceFeature.BatteryOperated),\n ...(rawType ? { type: rawType as DeviceType } : {}),\n }\n } catch (err) {\n this.ctx.logger.debug('deviceManager.getDevice failed during snapshot', {\n tags: { deviceId },\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n return null\n }\n }\n\n /** Settings-UI helper — battery flag drives the default max-age in the field description. */\n private async isDeviceBattery(deviceId: number): Promise<boolean> {\n return (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false\n }\n\n private async getStreamOptions(deviceId: number): Promise<Array<{ value: string; label: string }>> {\n const prefix = `${deviceId}/`\n try {\n const dev = await this.ctx.fetchDevice(deviceId)\n const entries = await dev.cameraStreams?.getRtspEntries({}) ?? []\n const streamIds = entries\n .filter(e => e.enabled)\n .map(e => e.brokerId.slice(prefix.length))\n return [\n { value: 'auto', label: 'Auto' },\n ...streamIds.map(id => ({ value: id, label: streamQualityLabel(id) })),\n ]\n } catch (err) {\n this.ctx.logger.error('getStreamOptions failed', { tags: { deviceId }, meta: { error: errMsg(err) } })\n return [{ value: 'auto', label: 'Auto' }]\n }\n }\n\n private async saveDeviceSettingsPatch(\n deviceId: number,\n patch: Record<string, unknown>,\n ): Promise<{ success: true }> {\n if (!this.ctx.settings) {\n throw new Error('[snapshot] settings store unavailable — cannot persist per-device settings')\n }\n const current = await this.ctx.settings.readDeviceStore(deviceId)\n const next: Record<string, unknown> = { ...current }\n if ('snapshotStreamId' in patch) {\n const v = patch['snapshotStreamId']\n next['snapshotStreamId'] = typeof v === 'string' && v.trim().length > 0 ? v.trim() : ''\n }\n if ('snapshotDebug' in patch) {\n next['snapshotDebug'] = patch['snapshotDebug'] === true\n }\n if ('snapshotMaxAgeS' in patch) {\n const v = patch['snapshotMaxAgeS']\n // Persist a finite non-negative number; anything else (null,\n // undefined, NaN, negative) falls back to the per-device\n // default at read time.\n if (typeof v === 'number' && Number.isFinite(v) && v >= 0) {\n next['snapshotMaxAgeS'] = v\n } else {\n delete next['snapshotMaxAgeS']\n }\n }\n await this.ctx.settings.writeDeviceStore(deviceId, next)\n // Drop the cache so the next `getSnapshot` applies the new stream/debug flags.\n this.cache.delete(deviceId)\n return { success: true as const }\n }\n}\n\nfunction isAbsentNativeError(msg: string): boolean {\n return msg.includes('no provider for') || msg.includes('no native provider for capability')\n}\n\n/**\n * Quality ordering for broker picker: `high` > `mid` > `low` > other.\n * The streamId is the suffix after `${deviceId}/` in the brokerId.\n * Unknown labels fall through to 0 so they land at the bottom of the\n * preference list without crashing.\n */\nfunction qualityRank(brokerId: string, prefix: string): number {\n const streamId = brokerId.startsWith(prefix) ? brokerId.slice(prefix.length) : brokerId\n const normalized = streamId.toLowerCase()\n if (normalized.includes('high') || normalized === 'main' || normalized === 'hd') return 3\n if (normalized.includes('mid') || normalized === 'medium') return 2\n if (normalized.includes('low') || normalized === 'sub' || normalized === 'sd') return 1\n return 0\n}\n\n/**\n * Detect ffmpeg failure signatures that indicate the broker wasn't\n * streaming yet when we tried to read its RTSP restream — a race\n * window that closes as soon as the broker lands its first keyframe.\n * Used by `grabFrameFromBroker` to retry once instead of giving up.\n */\nfunction isBrokerColdError(msg: string): boolean {\n return msg.includes('Invalid data found when processing input')\n || msg.includes('Error opening input')\n || msg.includes('Connection refused')\n || msg.includes('No route to host')\n}\n\nfunction runFfmpegFrameGrab(url: string, timeoutMs: number): Promise<Buffer> {\n return new Promise<Buffer>((resolve, reject) => {\n // ── IDR-first snapshot ────────────────────────────────────────────────\n //\n // `-skip_frame nokey` decodes only keyframes — the first frame handed\n // to the MJPEG encoder is guaranteed to be a complete IDR with all\n // references present, so the JPEG can't come out as a half-decoded\n // delta (the \"green stripe\" a non-keyframe decode produces).\n //\n // `-fflags +discardcorrupt` drops corrupt packets that slipped in\n // before sync — otherwise they propagate as green/gray smears into\n // the decoded output.\n //\n // No `-analyzeduration` / `-probesize` override: the old values\n // (5_000_000 µs = 5 s) forced ffmpeg to buffer 5 s of RTP before\n // decoding even when a keyframe was already in hand, which stacked\n // with a cold stream-broker's upstream handshake and pushed the\n // snapshot past the caller's ffmpeg timeout. Defaults are fine —\n // `-skip_frame nokey` + `-frames:v 1` already bound the work to a\n // single keyframe.\n const child = execFile(\n 'ffmpeg',\n [\n '-loglevel', 'error',\n '-rtsp_transport', 'tcp',\n '-fflags', '+discardcorrupt',\n '-skip_frame', 'nokey',\n '-i', url,\n '-vf', 'select=eq(pict_type\\\\,I)',\n '-vsync', 'vfr',\n '-frames:v', '1',\n '-q:v', '3',\n '-f', 'image2pipe',\n '-vcodec', 'mjpeg',\n 'pipe:1',\n ],\n { encoding: 'buffer', maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs },\n (err, stdout, stderr) => {\n if (err) {\n // `execFile`'s default Error carries \"Command failed: …\" plus a\n // `code`/`signal` that Node fills in. ffmpeg with `-loglevel\n // error` is silent on RTSP-level failures; when the parent\n // kills the child on timeout, `killed === true` and stderr is\n // empty. Surface both sides so the log tells the real story.\n const errWithMeta = err as (Error & { code?: string | number; signal?: string | null; killed?: boolean })\n const stderrText = Buffer.isBuffer(stderr) ? stderr.toString('utf8').trim() : String(stderr ?? '').trim()\n const parts: string[] = [err instanceof Error ? err.message : String(err)]\n if (errWithMeta.killed) parts.push('killed=true')\n if (errWithMeta.code !== undefined) parts.push(`code=${String(errWithMeta.code)}`)\n if (errWithMeta.signal) parts.push(`signal=${errWithMeta.signal}`)\n if (stderrText) parts.push(`stderr: ${stderrText.slice(0, 500)}`)\n reject(new Error(parts.join(' — ')))\n return\n }\n resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout))\n },\n )\n child.on('error', (e: Error) => reject(e))\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,gCAAyB;AACzB,mBAA8D;AAE9D,IAAAA,gBAAsD;AA4BtD,IAAM,gCAAgC;AAEtC,IAAM,4BAA4B;AAuC3B,IAAM,gBAAN,cAA4B,wBAA+B;AAAA,EAC/C,QAAQ,oBAAI,IAAwB;AAAA,EAErD,cAAc;AACZ,UAAM;AAAA,MACJ,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,MAAgB,eAAgD;AAC9D,SAAK,IAAI,OAAO,KAAK,8BAA8B;AACnD,UAAM,WAA8B;AAAA,MAClC,aAAa,CAAC,UAAU,KAAK,YAAY,KAAK;AAAA,MAC9C,iBAAiB,CAAC,UAAU,KAAK,gBAAgB,KAAK;AAAA;AAAA,MAEtD,+BAA+B,CAAC,UAAU,KAAK,gCAAgC,MAAM,QAAQ;AAAA,MAC7F,2BAA2B,YAAY;AAAA,MACvC,0BAA0B,CAAC,UAAU,KAAK,wBAAwB,MAAM,UAAU,MAAM,KAAK;AAAA;AAAA;AAAA;AAAA,MAI7F,WAAW,OAAO,UAAU,KAAK,UAAU,MAAM,QAAQ;AAAA,IAC3D;AACA,WAAO,CAAC;AAAA,MACN,YAAY;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,MACN,eAAe;AAAA,IACjB,CAAC;AAAA,EACH;AAAA,EAEA,MAAgB,aAA4B;AAC1C,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA,EAEU,uBAAuB;AAC/B,WAAO,KAAK,OAAO;AAAA,MACjB,UAAU,CAAC;AAAA,QACT,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,aAAa;AAAA,QACb,SAAS;AAAA,QACT,QAAQ;AAAA,UACN,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YAAU,KAAK;AAAA,YAAc,OAAO;AAAA,YAC1C,aAAa;AAAA,YACb,KAAK;AAAA,YAAG,KAAK;AAAA,YAAW,MAAM;AAAA,YAAO,SAAS;AAAA,UAChD,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAIA,MAAc,YAAY,OAAgG;AACxH,UAAM,EAAE,UAAU,MAAM,IAAI;AAI5B,UAAM,OAAO,MAAM,KAAK,iBAAiB,QAAQ;AACjD,UAAM,aAAa,MAAM;AACzB,UAAM,kBAAkB,MAAM,aAAa;AAC3C,UAAM,MAAM,KAAK,IAAI,OAAO,SAAS,EAAE,UAAU,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC,EAAG,CAAC;AACxF,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,MAAM,KAAK,MAAM,IAAI,QAAQ;AAKnC,UAAM,QAAgC,MAAM,KAAK,mBAAmB,QAAQ,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9F,UAAM,UAAU,MAAM;AACtB,UAAM,oBAAoB,MAAM,aAAa,WAAW,YAAY,SAAS,UAAU;AACvF,QAAI,MAAM,eAAe;AACvB,UAAI,KAAK,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,QAAQ,qBAAqB,OAAO,EAAE,CAAC;AAAA,IAC5F;AAsBA,UAAM,iBAAiB,kBAAkB,4BAA4B;AACrE,UAAM,qBAAqB,OAAO,MAAM,oBAAoB,YAAY,MAAM,mBAAmB,IAC7F,MAAM,kBACN,kBAAkB;AACtB,QAAI,CAAC,SAAS,OAAO,MAAM,IAAI,KAAK,mBAAmB;AACrD,UAAI,MAAM,eAAe;AACvB,YAAI,MAAM,uBAAuB;AAAA,UAC/B,MAAM,EAAE,SAAS;AAAA,UACjB,MAAM,EAAE,OAAO,MAAM,IAAI,IAAI,UAAU,mBAAmB,WAAW,gBAAgB;AAAA,QACvF,CAAC;AAAA,MACH;AACA,aAAO,IAAI;AAAA,IACb;AAMA,QAAI,cAAuB;AAC3B,QAAI,eAAe;AACnB,QAAI;AACF,YAAM,SAAS,KAAK,IAAI,kBAAkB,iCAAoB,QAAQ;AACtE,UAAI,QAAQ;AACV,cAAM,SAAS,MAAM,OAAO,YAAY,KAAK;AAC7C,YAAI,QAAQ;AACV,eAAK,MAAM,IAAI,UAAU,EAAE,MAAM,QAAQ,IAAI,KAAK,UAAU,qBAAqB,KAAK,CAAC;AACvF,iBAAO;AAAA,QACT;AAAA,MACF,OAAO;AACL,uBAAe;AAAA,MACjB;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAM,sBAAO,GAAG;AACtB,UAAI,oBAAoB,GAAG,GAAG;AAC5B,uBAAe;AACf,YAAI,MAAM,0BAA0B,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC;AAAA,MAClF,OAAO;AACL,sBAAc;AACd,YAAI,KAAK,0BAA0B,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC;AAAA,MACjF;AAAA,IACF;AAeA,UAAM,uBAAuB,mBACxB,gBACA,CAAE,MAAM,KAAK,4BAA4B,QAAQ;AACtD,QAAI,CAAC,sBAAsB;AACzB,UAAI;AACF,cAAM,WAAW,MAAM,KAAK,oBAAoB,UAAU,iBAAiB;AAC3E,YAAI,UAAU;AACZ,eAAK,MAAM,IAAI,UAAU,EAAE,MAAM,UAAU,IAAI,KAAK,UAAU,qBAAqB,KAAK,CAAC;AACzF,iBAAO;AAAA,QACT;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,KAAK,0CAA0C,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,WAAO,sBAAO,GAAG,EAAE,EAAE,CAAC;AAAA,MACzG;AAAA,IACF,OAAO;AACL,UAAI,MAAM,uGAAkG;AAAA,QAC1G,MAAM,EAAE,SAAS;AAAA,MACnB,CAAC;AAAA,IACH;AAUA,QAAI,KAAK;AACP,YAAM,QAAQ,MAAM,IAAI;AACxB,UAAI,QAAQ,KAAK,OAAO,YAAY;AAClC,YAAI,KAAK,8DAAyD,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC;AAAA,MAC3G;AACA,aAAO,IAAI;AAAA,IACb;AAOA,QAAI,YAAa,OAAM;AACvB,QAAI,aAAc,QAAO;AACzB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgCA,MAAc,oBACZ,UACA,mBAC+B;AAC/B,UAAM,MAAM,MAAM,KAAK,IAAI,YAAY,QAAQ;AAC/C,UAAM,SAAS,GAAG,QAAQ;AAM1B,UAAM,CAAC,eAAe,YAAY,IAAI,MAAM,QAAQ,IAAI;AAAA,MACtD,IAAI,eAAe,eAAe,CAAC,CAAC,KAAK,CAAC;AAAA,MAC1C,IAAI,eAAe,iBAAiB,CAAC,CAAC,KAAK,CAAC;AAAA,IAC9C,CAAC;AACD,UAAM,SAAS,cAAc,OAAO,OAAK,EAAE,WAAW,CAAC,CAAC,EAAE,GAAG;AAC7D,QAAI,OAAO,WAAW,EAAG,QAAO;AAGhC,QAAI,qBAAqB,sBAAsB,QAAQ;AACrD,YAAM,WAAW,OAAO,KAAK,OAAK,EAAE,aAAa,GAAG,MAAM,GAAG,iBAAiB,EAAE;AAChF,UAAI,UAAU;AACZ,cAAM,UAAU,MAAM,KAAK,uBAAuB,SAAS,KAAK,QAAQ;AACxE,YAAI,QAAS,QAAO;AAIpB,aAAK,IAAI,OAAO,MAAM,iEAA4D,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;AAAA,MACnH;AAAA,IACF;AAGA,UAAM,SAAS,CAAC,GAAG,MAAM,EAAE;AAAA,MACzB,CAAC,GAAG,MAAM,YAAY,EAAE,UAAU,MAAM,IAAI,YAAY,EAAE,UAAU,MAAM;AAAA,IAC5E;AAKA,UAAM,mBAAmB,oBAAI,IAAmC;AAChE,eAAW,QAAQ,aAAc,kBAAiB,IAAI,KAAK,UAAU,KAAK,MAAM;AAChF,UAAM,WAAW,OAAO,IAAI,QAAM,EAAE,OAAO,GAAG,QAAQ,iBAAiB,IAAI,EAAE,QAAQ,KAAK,OAAgB,EAAE;AAG5G,UAAM,OAAO,SAAS,KAAK,OAAK,EAAE,WAAW,WAAW;AACxD,QAAI,MAAM;AACR,YAAM,UAAU,MAAM,KAAK,uBAAuB,KAAK,MAAM,KAAK,QAAQ;AAC1E,UAAI,QAAS,QAAO;AAAA,IACtB;AAIA,eAAW,EAAE,MAAM,KAAK,UAAU;AAChC,YAAM,UAAU,MAAM,KAAK,uBAAuB,MAAM,KAAK,QAAQ,EAAE,MAAM,MAAM,IAAI;AACvF,UAAI,QAAS,QAAO;AAAA,IACtB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,uBAAuB,KAAa,UAAiD;AACjG,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,mBAAmB,KAAK,IAAK;AAAA,IAC3C,SAAS,KAAK;AACZ,YAAM,UAAM,sBAAO,GAAG;AACtB,UAAI,kBAAkB,GAAG,GAAG;AAC1B,aAAK,IAAI,OAAO,MAAM,2DAAsD,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAClG,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC;AAC5C,cAAM,MAAM,mBAAmB,KAAK,IAAK;AAAA,MAC3C,OAAO;AACL,cAAM;AAAA,MACR;AAAA,IACF;AACA,QAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,WAAO,EAAE,QAAQ,IAAI,SAAS,QAAQ,GAAG,aAAa,aAAa;AAAA,EACrE;AAAA,EAEA,MAAc,gBAAgB,OAA4C;AACxE,SAAK,MAAM,OAAO,MAAM,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAc,4BAA4B,UAAoC;AAI5E,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,IAAI,YAAY,QAAQ;AAC/C,YAAM,QAAQ,MAAM,IAAI,eAAe,iBAAiB,CAAC,CAAC,KAAK,CAAC;AAChE,aAAO,MAAM,KAAK,OAAK,EAAE,WAAW,WAAW;AAAA,IACjD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,UAAU,UAKd;AACR,UAAM,MAAM,KAAK,MAAM,IAAI,QAAQ;AACnC,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO;AAAA,MACL,gBAAgB,IAAI;AAAA,MACpB,YAAY,KAAK,IAAI,IAAI,IAAI;AAAA,MAC7B,WAAW,IAAI,KAAK,OAAO;AAAA;AAAA,MAC3B,cAAc,IAAI;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAc,mBAAmB,UAAmD;AAClF,QAAI,CAAC,KAAK,IAAI,SAAU,QAAO,CAAC;AAChC,UAAM,MAAM,MAAM,KAAK,IAAI,SAAS,gBAAgB,QAAQ;AAC5D,UAAM,SAAS,IAAI,iBAAiB;AACpC,UAAM,UAAU,OAAO,WAAW,YAAY,UAAU,KAAK,OAAO,SAAS,MAAM,IAC/E,SACA;AACJ,WAAO;AAAA,MACL,kBAAkB,OAAO,IAAI,kBAAkB,MAAM,WAAW,IAAI,kBAAkB,IAAI;AAAA,MAC1F,eAAe,IAAI,eAAe,MAAM;AAAA,MACxC,GAAI,YAAY,SAAY,EAAE,iBAAiB,QAAQ,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAc,gCAAgC,UAA4D;AAOxG,UAAM,OAAO,MAAM,KAAK,iBAAiB,QAAQ;AACjD,QAAI,QAAQ,KAAK,SAAS,wBAAW,OAAQ,QAAO;AACpD,UAAM,UAAU,MAAM,KAAK,mBAAmB,QAAQ;AACtD,UAAM,gBAAgB,MAAM,KAAK,iBAAiB,QAAQ;AAC1D,UAAM,YAAY,MAAM,KAAK,gBAAgB,QAAQ;AACrD,UAAM,iBAAiB,YAAY,4BAA4B;AAC/D,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,QACT,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,KAAK;AAAA,QACL,OAAO;AAAA,QACP,QAAQ;AAAA,UACN;AAAA,YACE,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aAAa;AAAA,YACb,SAAS;AAAA,YACT,UAAU;AAAA,YACV,OAAO,QAAQ,oBAAoB;AAAA,UACrC;AAAA,UACA;AAAA,YACE,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aACE,qEAAqE,cAAc,SAChF,YAAY,8CAA8C,8BAA8B;AAAA,YAE7F,KAAK;AAAA,YACL,KAAK,KAAK;AAAA,YACV,MAAM;AAAA,YACN,OAAO,QAAQ,mBAAmB;AAAA,UACpC;AAAA,UACA;AAAA,YACE,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aAAa;AAAA,YACb,OAAO,QAAQ,iBAAiB;AAAA,UAClC;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAc,iBACZ,UAC0E;AAC1E,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI;AACF,YAAM,QAAQ,MAAM,IAAI,cAAc,UAAU,MAAM,EAAE,SAAS,CAAC;AAClE,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,WAAY,MAAM,YAA8C,CAAC;AACvE,YAAM,UAAW,MAA4B;AAC7C,aAAO;AAAA,QACL,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,QACzC,WAAW,SAAS,SAAS,2BAAc,eAAe;AAAA,QAC1D,GAAI,UAAU,EAAE,MAAM,QAAsB,IAAI,CAAC;AAAA,MACnD;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,IAAI,OAAO,MAAM,kDAAkD;AAAA,QACtE,MAAM,EAAE,SAAS;AAAA,QACjB,MAAM,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,MAClE,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,gBAAgB,UAAoC;AAChE,YAAQ,MAAM,KAAK,iBAAiB,QAAQ,IAAI,aAAa;AAAA,EAC/D;AAAA,EAEA,MAAc,iBAAiB,UAAoE;AACjG,UAAM,SAAS,GAAG,QAAQ;AAC1B,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,IAAI,YAAY,QAAQ;AAC/C,YAAM,UAAU,MAAM,IAAI,eAAe,eAAe,CAAC,CAAC,KAAK,CAAC;AAChE,YAAM,YAAY,QACf,OAAO,OAAK,EAAE,OAAO,EACrB,IAAI,OAAK,EAAE,SAAS,MAAM,OAAO,MAAM,CAAC;AAC3C,aAAO;AAAA,QACL,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,QAC/B,GAAG,UAAU,IAAI,SAAO,EAAE,OAAO,IAAI,WAAO,kCAAmB,EAAE,EAAE,EAAE;AAAA,MACvE;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,IAAI,OAAO,MAAM,2BAA2B,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,WAAO,sBAAO,GAAG,EAAE,EAAE,CAAC;AACrG,aAAO,CAAC,EAAE,OAAO,QAAQ,OAAO,OAAO,CAAC;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,MAAc,wBACZ,UACA,OAC4B;AAC5B,QAAI,CAAC,KAAK,IAAI,UAAU;AACtB,YAAM,IAAI,MAAM,iFAA4E;AAAA,IAC9F;AACA,UAAM,UAAU,MAAM,KAAK,IAAI,SAAS,gBAAgB,QAAQ;AAChE,UAAM,OAAgC,EAAE,GAAG,QAAQ;AACnD,QAAI,sBAAsB,OAAO;AAC/B,YAAM,IAAI,MAAM,kBAAkB;AAClC,WAAK,kBAAkB,IAAI,OAAO,MAAM,YAAY,EAAE,KAAK,EAAE,SAAS,IAAI,EAAE,KAAK,IAAI;AAAA,IACvF;AACA,QAAI,mBAAmB,OAAO;AAC5B,WAAK,eAAe,IAAI,MAAM,eAAe,MAAM;AAAA,IACrD;AACA,QAAI,qBAAqB,OAAO;AAC9B,YAAM,IAAI,MAAM,iBAAiB;AAIjC,UAAI,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,KAAK,KAAK,GAAG;AACzD,aAAK,iBAAiB,IAAI;AAAA,MAC5B,OAAO;AACL,eAAO,KAAK,iBAAiB;AAAA,MAC/B;AAAA,IACF;AACA,UAAM,KAAK,IAAI,SAAS,iBAAiB,UAAU,IAAI;AAEvD,SAAK,MAAM,OAAO,QAAQ;AAC1B,WAAO,EAAE,SAAS,KAAc;AAAA,EAClC;AACF;AAEA,SAAS,oBAAoB,KAAsB;AACjD,SAAO,IAAI,SAAS,iBAAiB,KAAK,IAAI,SAAS,mCAAmC;AAC5F;AAQA,SAAS,YAAY,UAAkB,QAAwB;AAC7D,QAAM,WAAW,SAAS,WAAW,MAAM,IAAI,SAAS,MAAM,OAAO,MAAM,IAAI;AAC/E,QAAM,aAAa,SAAS,YAAY;AACxC,MAAI,WAAW,SAAS,MAAM,KAAK,eAAe,UAAU,eAAe,KAAM,QAAO;AACxF,MAAI,WAAW,SAAS,KAAK,KAAK,eAAe,SAAU,QAAO;AAClE,MAAI,WAAW,SAAS,KAAK,KAAK,eAAe,SAAS,eAAe,KAAM,QAAO;AACtF,SAAO;AACT;AAQA,SAAS,kBAAkB,KAAsB;AAC/C,SAAO,IAAI,SAAS,0CAA0C,KACzD,IAAI,SAAS,qBAAqB,KAClC,IAAI,SAAS,oBAAoB,KACjC,IAAI,SAAS,kBAAkB;AACtC;AAEA,SAAS,mBAAmB,KAAa,WAAoC;AAC3E,SAAO,IAAI,QAAgB,CAAC,SAAS,WAAW;AAmB9C,UAAM,YAAQ;AAAA,MACZ;AAAA,MACA;AAAA,QACE;AAAA,QAAa;AAAA,QACb;AAAA,QAAmB;AAAA,QACnB;AAAA,QAAW;AAAA,QACX;AAAA,QAAe;AAAA,QACf;AAAA,QAAM;AAAA,QACN;AAAA,QAAO;AAAA,QACP;AAAA,QAAU;AAAA,QACV;AAAA,QAAa;AAAA,QACb;AAAA,QAAQ;AAAA,QACR;AAAA,QAAM;AAAA,QACN;AAAA,QAAW;AAAA,QACX;AAAA,MACF;AAAA,MACA,EAAE,UAAU,UAAU,WAAW,KAAK,OAAO,MAAM,SAAS,UAAU;AAAA,MACtE,CAAC,KAAK,QAAQ,WAAW;AACvB,YAAI,KAAK;AAMP,gBAAM,cAAc;AACpB,gBAAM,aAAa,OAAO,SAAS,MAAM,IAAI,OAAO,SAAS,MAAM,EAAE,KAAK,IAAI,OAAO,UAAU,EAAE,EAAE,KAAK;AACxG,gBAAM,QAAkB,CAAC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AACzE,cAAI,YAAY,OAAQ,OAAM,KAAK,aAAa;AAChD,cAAI,YAAY,SAAS,OAAW,OAAM,KAAK,QAAQ,OAAO,YAAY,IAAI,CAAC,EAAE;AACjF,cAAI,YAAY,OAAQ,OAAM,KAAK,UAAU,YAAY,MAAM,EAAE;AACjE,cAAI,WAAY,OAAM,KAAK,WAAW,WAAW,MAAM,GAAG,GAAG,CAAC,EAAE;AAChE,iBAAO,IAAI,MAAM,MAAM,KAAK,UAAK,CAAC,CAAC;AACnC;AAAA,QACF;AACA,gBAAQ,OAAO,SAAS,MAAM,IAAI,SAAS,OAAO,KAAK,MAAM,CAAC;AAAA,MAChE;AAAA,IACF;AACA,UAAM,GAAG,SAAS,CAAC,MAAa,OAAO,CAAC,CAAC;AAAA,EAC3C,CAAC;AACH;","names":["import_types"]}
|