@camstack/system 1.0.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.
Files changed (262) hide show
  1. package/dist/addon/addon-api-factory.d.ts +35 -0
  2. package/dist/addon-routes/addon-route-registry.d.ts +37 -0
  3. package/dist/addon-runner.js +599 -0
  4. package/dist/addon-runner.mjs +597 -0
  5. package/dist/auth/api-key-manager.d.ts +26 -0
  6. package/dist/auth/auth-manager.d.ts +109 -0
  7. package/dist/auth/parse-record.d.ts +18 -0
  8. package/dist/auth/scope-matcher.d.ts +7 -0
  9. package/dist/auth/scoped-token-manager.d.ts +40 -0
  10. package/dist/auth/totp-manager.d.ts +51 -0
  11. package/dist/auth/user-manager.d.ts +34 -0
  12. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +53 -0
  13. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +259 -0
  14. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +251 -0
  15. package/dist/builtins/addon-pages-aggregator/dedupe-pages.d.ts +6 -0
  16. package/dist/builtins/addon-pages-aggregator/index.d.ts +1 -0
  17. package/dist/builtins/addon-pages-aggregator/index.js +8 -0
  18. package/dist/builtins/addon-pages-aggregator/index.mjs +2 -0
  19. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +47 -0
  20. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +228 -0
  21. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +220 -0
  22. package/dist/builtins/addon-widgets-aggregator/index.d.ts +1 -0
  23. package/dist/builtins/addon-widgets-aggregator/index.js +8 -0
  24. package/dist/builtins/addon-widgets-aggregator/index.mjs +2 -0
  25. package/dist/builtins/alerts/alerts.addon.d.ts +81 -0
  26. package/dist/builtins/alerts/alerts.addon.js +601 -0
  27. package/dist/builtins/alerts/alerts.addon.mjs +595 -0
  28. package/dist/builtins/alerts/index.d.ts +1 -0
  29. package/dist/builtins/alerts/index.js +4 -0
  30. package/dist/builtins/alerts/index.mjs +2 -0
  31. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +147 -0
  32. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2229 -0
  33. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2220 -0
  34. package/dist/builtins/backup-orchestrator/cron-helpers.d.ts +23 -0
  35. package/dist/builtins/backup-orchestrator/destination-policy.d.ts +72 -0
  36. package/dist/builtins/backup-orchestrator/download-helpers.d.ts +12 -0
  37. package/dist/builtins/backup-orchestrator/index.d.ts +2 -0
  38. package/dist/builtins/backup-orchestrator/index.js +8 -0
  39. package/dist/builtins/backup-orchestrator/index.mjs +2 -0
  40. package/dist/builtins/backup-orchestrator/manifest-store.d.ts +77 -0
  41. package/dist/builtins/console-logging/console-destination.d.ts +13 -0
  42. package/dist/builtins/console-logging/console-logging.addon.d.ts +25 -0
  43. package/dist/builtins/console-logging/index.d.ts +3 -0
  44. package/dist/builtins/console-logging/index.js +104 -0
  45. package/dist/builtins/console-logging/index.mjs +95 -0
  46. package/dist/builtins/device-manager/device-config-contribution.d.ts +32 -0
  47. package/dist/builtins/device-manager/device-event-propagator.d.ts +26 -0
  48. package/dist/builtins/device-manager/device-link-overlay.d.ts +23 -0
  49. package/dist/builtins/device-manager/device-link-resolver.d.ts +15 -0
  50. package/dist/builtins/device-manager/device-manager.addon.d.ts +452 -0
  51. package/dist/builtins/device-manager/device-manager.addon.js +3299 -0
  52. package/dist/builtins/device-manager/device-manager.addon.mjs +3292 -0
  53. package/dist/builtins/device-manager/index.d.ts +2 -0
  54. package/dist/builtins/device-manager/index.js +8 -0
  55. package/dist/builtins/device-manager/index.mjs +2 -0
  56. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +44 -0
  57. package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts +15 -0
  58. package/dist/builtins/hub-forwarder/index.d.ts +3 -0
  59. package/dist/builtins/hub-forwarder/index.js +154 -0
  60. package/dist/builtins/hub-forwarder/index.mjs +145 -0
  61. package/dist/builtins/local-auth/auth-schema.d.ts +26 -0
  62. package/dist/builtins/local-auth/index.d.ts +1 -0
  63. package/dist/builtins/local-auth/index.js +4 -0
  64. package/dist/builtins/local-auth/index.mjs +2 -0
  65. package/dist/builtins/local-auth/local-auth.addon.d.ts +18 -0
  66. package/dist/builtins/local-auth/local-auth.addon.js +8094 -0
  67. package/dist/builtins/local-auth/local-auth.addon.mjs +8063 -0
  68. package/dist/builtins/local-auth/oauth-grants.d.ts +45 -0
  69. package/dist/builtins/local-auth/oauth-session-manager.d.ts +50 -0
  70. package/dist/builtins/local-network/index.d.ts +2 -0
  71. package/dist/builtins/local-network/index.js +10 -0
  72. package/dist/builtins/local-network/index.mjs +2 -0
  73. package/dist/builtins/local-network/local-network.addon.d.ts +150 -0
  74. package/dist/builtins/local-network/local-network.addon.js +489 -0
  75. package/dist/builtins/local-network/local-network.addon.mjs +477 -0
  76. package/dist/builtins/native-metrics/index.d.ts +2 -0
  77. package/dist/builtins/native-metrics/native-metrics-provider.d.ts +48 -0
  78. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +73 -0
  79. package/dist/builtins/native-metrics/native-metrics.addon.js +922 -0
  80. package/dist/builtins/native-metrics/native-metrics.addon.mjs +914 -0
  81. package/dist/builtins/platform-probe/hardware-decode-accel-probe.d.ts +37 -0
  82. package/dist/builtins/platform-probe/hardware-encoder-probe.d.ts +13 -0
  83. package/dist/builtins/platform-probe/index.d.ts +22 -0
  84. package/dist/builtins/platform-probe/index.js +834 -0
  85. package/dist/builtins/platform-probe/index.mjs +822 -0
  86. package/dist/builtins/platform-probe/inference-config-resolver.d.ts +29 -0
  87. package/dist/builtins/platform-probe/intel-accelerators.d.ts +11 -0
  88. package/dist/builtins/platform-probe/platform-scorer.d.ts +30 -0
  89. package/dist/builtins/platform-probe/runtime-packages.d.ts +6 -0
  90. package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +96 -0
  91. package/dist/builtins/remote-access-orchestrator/index.d.ts +1 -0
  92. package/dist/builtins/remote-access-orchestrator/index.js +8 -0
  93. package/dist/builtins/remote-access-orchestrator/index.mjs +2 -0
  94. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +40 -0
  95. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +214 -0
  96. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +208 -0
  97. package/dist/builtins/shared/settle-sources.d.ts +22 -0
  98. package/dist/builtins/snapshot/index.d.ts +2 -0
  99. package/dist/builtins/snapshot/index.js +494 -0
  100. package/dist/builtins/snapshot/index.mjs +488 -0
  101. package/dist/builtins/snapshot/snapshot.addon.d.ts +120 -0
  102. package/dist/builtins/sqlite-storage/config-store.d.ts +8 -0
  103. package/dist/builtins/sqlite-storage/device-store.d.ts +23 -0
  104. package/dist/builtins/sqlite-storage/filesystem-browse-provider.d.ts +25 -0
  105. package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts +83 -0
  106. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +32 -0
  107. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +396 -0
  108. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +388 -0
  109. package/dist/builtins/sqlite-storage/index.d.ts +8 -0
  110. package/dist/builtins/sqlite-storage/index.js +62 -0
  111. package/dist/builtins/sqlite-storage/index.mjs +49 -0
  112. package/dist/builtins/sqlite-storage/integration-registry.d.ts +27 -0
  113. package/dist/builtins/sqlite-storage/path-guard.d.ts +4 -0
  114. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +102 -0
  115. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +14 -0
  116. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +644 -0
  117. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +636 -0
  118. package/dist/builtins/storage-orchestrator/index.d.ts +6 -0
  119. package/dist/builtins/storage-orchestrator/index.js +10 -0
  120. package/dist/builtins/storage-orchestrator/index.mjs +2 -0
  121. package/dist/builtins/storage-orchestrator/location-store.d.ts +49 -0
  122. package/dist/builtins/storage-orchestrator/provider-discovery.d.ts +10 -0
  123. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +103 -0
  124. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1138 -0
  125. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1128 -0
  126. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +236 -0
  127. package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +38 -0
  128. package/dist/builtins/system-backup/system-backup.service.d.ts +137 -0
  129. package/dist/builtins/system-config/index.d.ts +1 -0
  130. package/dist/builtins/system-config/index.js +8 -0
  131. package/dist/builtins/system-config/index.mjs +2 -0
  132. package/dist/builtins/system-config/system-config.addon.d.ts +10 -0
  133. package/dist/builtins/system-config/system-config.addon.js +232 -0
  134. package/dist/builtins/system-config/system-config.addon.mjs +226 -0
  135. package/dist/builtins/winston-logging/index.d.ts +3 -0
  136. package/dist/builtins/winston-logging/index.js +156 -0
  137. package/dist/builtins/winston-logging/index.mjs +144 -0
  138. package/dist/builtins/winston-logging/winston-destination.d.ts +21 -0
  139. package/dist/builtins/winston-logging/winston-logging.addon.d.ts +19 -0
  140. package/dist/chunk-CNf5ZN-e.mjs +37 -0
  141. package/dist/chunk-Cek0wNdY.js +64 -0
  142. package/dist/download/model-download-service.d.ts +41 -0
  143. package/dist/download/model-downloader.d.ts +31 -0
  144. package/dist/events/event-bus.d.ts +10 -0
  145. package/dist/events/system-event-bus.d.ts +14 -0
  146. package/dist/feature/feature-manager.d.ts +11 -0
  147. package/dist/formatter-B7qW8bPJ.mjs +162 -0
  148. package/dist/formatter-DqAKDlvN.js +167 -0
  149. package/dist/http/authenticated-file-server.d.ts +53 -0
  150. package/dist/http/data-plane-registry.d.ts +23 -0
  151. package/dist/http/file-data-plane.d.ts +10 -0
  152. package/dist/http/reverse-proxy.d.ts +15 -0
  153. package/dist/index.d.ts +82 -0
  154. package/dist/index.js +93485 -0
  155. package/dist/index.mjs +93179 -0
  156. package/dist/intel-accelerators-Gg0P5mnl.js +20 -0
  157. package/dist/intel-accelerators-hGgpZ0pX.mjs +19 -0
  158. package/dist/kernel/addon-class-resolver.d.ts +4 -0
  159. package/dist/kernel/addon-engine-manager.d.ts +22 -0
  160. package/dist/kernel/addon-health-monitor.d.ts +154 -0
  161. package/dist/kernel/addon-installer.d.ts +208 -0
  162. package/dist/kernel/addon-loader.d.ts +106 -0
  163. package/dist/kernel/addon-manifest.d.ts +77 -0
  164. package/dist/kernel/capability-handle.d.ts +46 -0
  165. package/dist/kernel/capability-registry.d.ts +412 -0
  166. package/dist/kernel/config-manager.d.ts +212 -0
  167. package/dist/kernel/config-schema.d.ts +93 -0
  168. package/dist/kernel/custom-action-registry.d.ts +23 -0
  169. package/dist/kernel/deps/addon-deps-manager.d.ts +19 -0
  170. package/dist/kernel/deps/manifest-native-deps.d.ts +25 -0
  171. package/dist/kernel/deps/manifest-python-deps.d.ts +20 -0
  172. package/dist/kernel/device-registry.d.ts +29 -0
  173. package/dist/kernel/fs-utils.d.ts +41 -0
  174. package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +19 -0
  175. package/dist/kernel/hwaccel/hwaccel-service.d.ts +4 -0
  176. package/dist/kernel/index.d.ts +74 -0
  177. package/dist/kernel/infra-capabilities.d.ts +13 -0
  178. package/dist/kernel/moleculer/addon-context-factory.d.ts +91 -0
  179. package/dist/kernel/moleculer/addon-data-plane-facility.d.ts +19 -0
  180. package/dist/kernel/moleculer/addon-runner.d.ts +1 -0
  181. package/dist/kernel/moleculer/addon-service-factory.d.ts +50 -0
  182. package/dist/kernel/moleculer/broker-factory.d.ts +50 -0
  183. package/dist/kernel/moleculer/cap-usage-registry.d.ts +46 -0
  184. package/dist/kernel/moleculer/capabilities-access.d.ts +21 -0
  185. package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +46 -0
  186. package/dist/kernel/moleculer/child-cap-dispatch.d.ts +20 -0
  187. package/dist/kernel/moleculer/cluster-secret.d.ts +15 -0
  188. package/dist/kernel/moleculer/core-cap-service.d.ts +50 -0
  189. package/dist/kernel/moleculer/crash-supervisor.d.ts +50 -0
  190. package/dist/kernel/moleculer/device-cap-proxy.d.ts +79 -0
  191. package/dist/kernel/moleculer/event-bus-core.d.ts +53 -0
  192. package/dist/kernel/moleculer/event-bus.d.ts +53 -0
  193. package/dist/kernel/moleculer/hub-log-forwarder.d.ts +36 -0
  194. package/dist/kernel/moleculer/hub-service.d.ts +35 -0
  195. package/dist/kernel/moleculer/node-registry.d.ts +126 -0
  196. package/dist/kernel/moleculer/process-context.d.ts +4 -0
  197. package/dist/kernel/moleculer/process-service.d.ts +72 -0
  198. package/dist/kernel/moleculer/provider-registry.d.ts +28 -0
  199. package/dist/kernel/moleculer/readiness-context.d.ts +62 -0
  200. package/dist/kernel/moleculer/readiness-service.d.ts +7 -0
  201. package/dist/kernel/moleculer/register-node-client.d.ts +35 -0
  202. package/dist/kernel/moleculer/remote-logger.d.ts +43 -0
  203. package/dist/kernel/moleculer/resilient-cap-call.d.ts +28 -0
  204. package/dist/kernel/moleculer/stream-probe-service.d.ts +9 -0
  205. package/dist/kernel/moleculer/trpc-links.d.ts +189 -0
  206. package/dist/kernel/moleculer/typed-array-serde.d.ts +25 -0
  207. package/dist/kernel/moleculer/worker-device-restore.d.ts +10 -0
  208. package/dist/kernel/provider-kind-drift.d.ts +12 -0
  209. package/dist/kernel/restart-coordinator.d.ts +90 -0
  210. package/dist/kernel/storage-location-registry.d.ts +40 -0
  211. package/dist/kernel/transport/cap-action-name.d.ts +100 -0
  212. package/dist/kernel/transport/cap-route-resolver.d.ts +148 -0
  213. package/dist/kernel/transport/cap-route.d.ts +148 -0
  214. package/dist/kernel/transport/child-cap-protocol.d.ts +136 -0
  215. package/dist/kernel/transport/create-local-transport.d.ts +7 -0
  216. package/dist/kernel/transport/frame-codec.d.ts +7 -0
  217. package/dist/kernel/transport/index.d.ts +27 -0
  218. package/dist/kernel/transport/local-child-client.d.ts +136 -0
  219. package/dist/kernel/transport/local-child-registry.d.ts +179 -0
  220. package/dist/kernel/transport/local-endpoint-path.d.ts +6 -0
  221. package/dist/kernel/transport/local-transport.d.ts +46 -0
  222. package/dist/kernel/transport/parent-unowned-call.d.ts +75 -0
  223. package/dist/kernel/transport/socket-channel.d.ts +27 -0
  224. package/dist/kernel/transport/uds-event-bridge.d.ts +36 -0
  225. package/dist/kernel/transport/uds-event-bus.d.ts +22 -0
  226. package/dist/kernel/transport/uds-local-transport.d.ts +18 -0
  227. package/dist/kernel/transport/uds-log-ingest.d.ts +28 -0
  228. package/dist/kernel/transport/uds-logger.d.ts +44 -0
  229. package/dist/kernel/utils/ring-buffer.d.ts +15 -0
  230. package/dist/kernel/workspace-detect.d.ts +9 -0
  231. package/dist/lifecycle/lifecycle-state-machine.d.ts +28 -0
  232. package/dist/logging/formatter.d.ts +30 -0
  233. package/dist/logging/log-manager.d.ts +54 -0
  234. package/dist/logging/log-ring-buffer.d.ts +47 -0
  235. package/dist/logging/partitioned-log-buffer.d.ts +35 -0
  236. package/dist/logging/scoped-logger.d.ts +17 -0
  237. package/dist/main-DNnMW7Z2.js +9983 -0
  238. package/dist/main-rtjOwPBR.mjs +9976 -0
  239. package/dist/manifest-python-deps-D1DbAQEv.js +6724 -0
  240. package/dist/manifest-python-deps-DZsKTbs1.mjs +6315 -0
  241. package/dist/network/network-quality.d.ts +11 -0
  242. package/dist/notification/notification-service.d.ts +37 -0
  243. package/dist/notification/toast-service.d.ts +22 -0
  244. package/dist/pipeline/engine-manager-resolver.d.ts +15 -0
  245. package/dist/pipeline/pipeline-runner.d.ts +8 -0
  246. package/dist/pipeline/pipeline-validator.d.ts +13 -0
  247. package/dist/process/resource-monitor.d.ts +11 -0
  248. package/dist/python/python-env-manager.d.ts +12 -0
  249. package/dist/repl/interfaces.d.ts +31 -0
  250. package/dist/repl/repl-engine.d.ts +8 -0
  251. package/dist/resource-monitor-ClDGFyf6.mjs +57 -0
  252. package/dist/resource-monitor-IIEanuJt.js +74 -0
  253. package/dist/settle-sources-Bhsy57y-.js +38 -0
  254. package/dist/settle-sources-CDtNC8ub.mjs +33 -0
  255. package/dist/storage/fs-storage-backend.d.ts +40 -0
  256. package/dist/storage/storage-location-manager.d.ts +23 -0
  257. package/dist/storage/storage-manager.d.ts +83 -0
  258. package/dist/tar-BgAEMRBR.js +5434 -0
  259. package/dist/tar-ByMOPNM0.mjs +5429 -0
  260. package/dist/tls/cert-manager.d.ts +26 -0
  261. package/dist/tls/index.d.ts +1 -0
  262. package/package.json +343 -0
@@ -0,0 +1,488 @@
1
+ import { BaseAddon, DeviceFeature, DeviceType, errMsg, snapshotCapability, streamQualityLabel } from "@camstack/types";
2
+ import { execFile } from "node:child_process";
3
+ //#region src/builtins/snapshot/snapshot.addon.ts
4
+ /** Default cache window for non-battery cams (seconds). 10s feels live. */
5
+ var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
6
+ /** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
7
+ var BATTERY_DEFAULT_MAX_AGE_S = 3600;
8
+ /**
9
+ * SnapshotAddon — wrapper over the `snapshot` capability.
10
+ *
11
+ * Activated per-device (toggleable by user; default active). When active,
12
+ * caches fresh snapshots in memory. On cache miss, delegates to the native
13
+ * provider for this device via `ctx.getNativeProvider(snapshotCapability, id)`.
14
+ *
15
+ * No silent fallback — if the native fails, propagate the error up. Stale
16
+ * cache is returned ONLY when the native throws AND a stale entry exists,
17
+ * and the failure is logged so the fallback is never silent.
18
+ *
19
+ * Frame-grab fallback: when the native provider is absent (or returns null /
20
+ * throws), we ask the `stream-broker` capability for the device's RTSP
21
+ * restream URL and pipe one JPEG out of ffmpeg. stream-broker intentionally
22
+ * does NOT expose a `grabFrame` method — the orchestration (native-first,
23
+ * ffmpeg fallback, caching) is this addon's concern; stream-broker only
24
+ * publishes stream endpoints.
25
+ */
26
+ var SnapshotAddon = class extends BaseAddon {
27
+ cache = /* @__PURE__ */ new Map();
28
+ constructor() {
29
+ super({ staleTtlMs: 6e4 });
30
+ }
31
+ async onInitialize() {
32
+ this.ctx.logger.info("Snapshot wrapper initialized");
33
+ return [{
34
+ capability: snapshotCapability,
35
+ provider: {
36
+ getSnapshot: (input) => this.getSnapshot(input),
37
+ invalidateCache: (input) => this.invalidateCache(input),
38
+ getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
39
+ getDeviceLiveContribution: async () => null,
40
+ applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),
41
+ getStatus: async (input) => this.getStatus(input.deviceId)
42
+ }
43
+ }];
44
+ }
45
+ async onShutdown() {
46
+ this.cache.clear();
47
+ }
48
+ globalSettingsSchema() {
49
+ return this.schema({ sections: [{
50
+ id: "snapshot-cache",
51
+ title: "Snapshot Cache",
52
+ description: "Stale fallback when live capture fails. Per-device freshness is configured under Device → Snapshot → Max cache age.",
53
+ columns: 1,
54
+ fields: [this.field({
55
+ type: "number",
56
+ key: "staleTtlMs",
57
+ label: "Stale fallback TTL (ms)",
58
+ description: "If live capture fails, cached snapshot younger than this is still returned.",
59
+ min: 0,
60
+ max: 36e5,
61
+ step: 1e3,
62
+ default: 6e4
63
+ })]
64
+ }] });
65
+ }
66
+ async getSnapshot(input) {
67
+ const { deviceId, force } = input;
68
+ const meta = await this.lookupDeviceMeta(deviceId);
69
+ const deviceName = meta?.name;
70
+ const isBatteryDevice = meta?.isBattery ?? false;
71
+ const log = this.ctx.logger.withTags({
72
+ deviceId,
73
+ ...deviceName ? { deviceName } : {}
74
+ });
75
+ const now = Date.now();
76
+ const hit = this.cache.get(deviceId);
77
+ const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
78
+ const rawPref = prefs.snapshotStreamId;
79
+ const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
80
+ if (prefs.snapshotDebug) log.info("debug on", {
81
+ tags: { deviceId },
82
+ meta: { stream: effectiveStreamId ?? "auto" }
83
+ });
84
+ const defaultMaxAgeS = isBatteryDevice ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S;
85
+ const effectiveMaxAgeMs = (typeof prefs.snapshotMaxAgeS === "number" && prefs.snapshotMaxAgeS >= 0 ? prefs.snapshotMaxAgeS : defaultMaxAgeS) * 1e3;
86
+ if (!force && hit && now - hit.ts < effectiveMaxAgeMs) {
87
+ if (prefs.snapshotDebug) log.debug("snapshot: cache hit", {
88
+ tags: { deviceId },
89
+ meta: {
90
+ ageMs: now - hit.ts,
91
+ maxAgeMs: effectiveMaxAgeMs,
92
+ isBattery: isBatteryDevice
93
+ }
94
+ });
95
+ return hit.data;
96
+ }
97
+ let nativeError = null;
98
+ let nativeAbsent = false;
99
+ try {
100
+ const native = this.ctx.getNativeProvider(snapshotCapability, deviceId);
101
+ if (native) {
102
+ const result = await native.getSnapshot(input);
103
+ if (result) {
104
+ this.cache.set(deviceId, {
105
+ data: result,
106
+ ts: now,
107
+ streamId: effectiveStreamId ?? null
108
+ });
109
+ return result;
110
+ }
111
+ log.debug("native snapshot returned null — falling through to broker", { tags: { deviceId } });
112
+ } else {
113
+ nativeAbsent = true;
114
+ log.debug("native snapshot provider not resolved — falling through to broker", { tags: { deviceId } });
115
+ }
116
+ } catch (err) {
117
+ const msg = errMsg(err);
118
+ if (isAbsentNativeError(msg)) {
119
+ nativeAbsent = true;
120
+ log.debug("native snapshot absent", {
121
+ tags: { deviceId },
122
+ meta: { error: msg }
123
+ });
124
+ } else {
125
+ nativeError = err;
126
+ log.warn("native snapshot failed", {
127
+ tags: { deviceId },
128
+ meta: { error: msg }
129
+ });
130
+ }
131
+ }
132
+ if (!(isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId))) try {
133
+ const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
134
+ if (fallback) {
135
+ this.cache.set(deviceId, {
136
+ data: fallback,
137
+ ts: now,
138
+ streamId: effectiveStreamId ?? null
139
+ });
140
+ return fallback;
141
+ }
142
+ } catch (err) {
143
+ log.warn("stream-broker snapshot fallback failed", {
144
+ tags: { deviceId },
145
+ meta: { error: errMsg(err) }
146
+ });
147
+ }
148
+ else log.debug("snapshot: skipping broker fallback — battery device with absent native and no streaming broker", { tags: { deviceId } });
149
+ if (hit) {
150
+ const ageMs = now - hit.ts;
151
+ if (ageMs > this.config.staleTtlMs) log.warn("snapshot: all live paths failed — serving stale cache", {
152
+ tags: { deviceId },
153
+ meta: { ageMs }
154
+ });
155
+ return hit.data;
156
+ }
157
+ if (nativeError) throw nativeError;
158
+ if (nativeAbsent) return null;
159
+ return null;
160
+ }
161
+ /**
162
+ * Tell apart "native provider isn't registered for this device" from
163
+ * "native provider ran and threw a real error". The former is the steady
164
+ * state for cameras without a vendor snapshot endpoint and should not
165
+ * propagate as a 500; the latter should.
166
+ */
167
+ /**
168
+ * Pull one JPEG from the device's stream-broker RTSP restream using
169
+ * a short-lived ffmpeg invocation.
170
+ *
171
+ * Stream selection strategy (picks the broker that won't stall):
172
+ * 1. Explicit `preferredStreamId` (user set in per-device settings)
173
+ * — always honoured, even if currently idle. Operator choice
174
+ * wins.
175
+ * 2. Auto: highest-quality broker currently in `streaming` state.
176
+ * This is the whole point — prefer the stream that has active
177
+ * subscribers (usually `low` for detection, but `mid`/`high`
178
+ * if WebRTC is watching) so ffmpeg hits a warm pipe and
179
+ * doesn't race the broker's resume.
180
+ * 3. Fallback: highest-quality enabled entry regardless of status
181
+ * (will wake a suspended broker — retry-guarded against the
182
+ * cold-start error).
183
+ *
184
+ * The stream-broker auto-suspends idle streams on the "no demand"
185
+ * signal; snapshots used to default to `high` which was often the
186
+ * first to go idle, racing every snapshot with a broker resume.
187
+ * Now we ask the orchestrator of streams which one is warm and grab
188
+ * from there.
189
+ */
190
+ async grabFrameFromBroker(deviceId, preferredStreamId) {
191
+ const dev = await this.ctx.fetchDevice(deviceId);
192
+ const prefix = `${deviceId}/`;
193
+ const [deviceEntries, profileSlots] = await Promise.all([dev.cameraStreams?.getRtspEntries({}) ?? [], dev.cameraStreams?.getBrokerStreams({}) ?? []]);
194
+ const usable = deviceEntries.filter((e) => e.enabled && !!e.url);
195
+ if (usable.length === 0) return null;
196
+ if (preferredStreamId && preferredStreamId !== "auto") {
197
+ const explicit = usable.find((e) => e.brokerId === `${prefix}${preferredStreamId}`);
198
+ if (explicit) {
199
+ const grabbed = await this.runGrabWithResumeRetry(explicit.url, deviceId);
200
+ if (grabbed) return grabbed;
201
+ this.ctx.logger.debug("grabFrame: explicit stream failed — falling back to auto", { meta: { preferredStreamId } });
202
+ }
203
+ }
204
+ const ranked = [...usable].toSorted((a, b) => qualityRank(b.brokerId, prefix) - qualityRank(a.brokerId, prefix));
205
+ const statusByBrokerId = /* @__PURE__ */ new Map();
206
+ for (const slot of profileSlots) statusByBrokerId.set(slot.brokerId, slot.status);
207
+ const statuses = ranked.map((e) => ({
208
+ entry: e,
209
+ status: statusByBrokerId.get(e.brokerId) ?? "idle"
210
+ }));
211
+ const warm = statuses.find((s) => s.status === "streaming");
212
+ if (warm) {
213
+ const grabbed = await this.runGrabWithResumeRetry(warm.entry.url, deviceId);
214
+ if (grabbed) return grabbed;
215
+ }
216
+ for (const { entry } of statuses) {
217
+ const grabbed = await this.runGrabWithResumeRetry(entry.url, deviceId).catch(() => null);
218
+ if (grabbed) return grabbed;
219
+ }
220
+ return null;
221
+ }
222
+ /**
223
+ * Ffmpeg grab with one retry on the broker-cold-start error
224
+ * signature. Covers the window between "client connected" and
225
+ * "first keyframe" when a suspended broker resumes.
226
+ */
227
+ async runGrabWithResumeRetry(url, deviceId) {
228
+ let buf;
229
+ try {
230
+ buf = await runFfmpegFrameGrab(url, 15e3);
231
+ } catch (err) {
232
+ if (isBrokerColdError(errMsg(err))) {
233
+ this.ctx.logger.debug("grabFrame: broker-resume race — retrying in 1500ms", { tags: { deviceId } });
234
+ await new Promise((r) => setTimeout(r, 1500));
235
+ buf = await runFfmpegFrameGrab(url, 15e3);
236
+ } else throw err;
237
+ }
238
+ if (buf.length === 0) return null;
239
+ return {
240
+ base64: buf.toString("base64"),
241
+ contentType: "image/jpeg"
242
+ };
243
+ }
244
+ async invalidateCache(input) {
245
+ this.cache.delete(input.deviceId);
246
+ }
247
+ /**
248
+ * Non-throwing probe of the device's battery cap. Returns true only
249
+ * when a battery native is registered AND its current status says
250
+ * `sleeping: true`. Any error (no provider, native absent, getStatus
251
+ * missing, RPC timeout) is swallowed and treated as "awake" — we'd
252
+ * rather pay a wake-up than strand the caller on a cache that's
253
+ * semantically stale. Debug-logged for observability.
254
+ */
255
+ /**
256
+ * True when at least one of the device's brokers is actively
257
+ * streaming (status === 'streaming'). Used by the battery-cam guard
258
+ * around `grabFrameFromBroker` to allow the fallback ONLY when
259
+ * grabbing a frame is free (a consumer is already keeping the
260
+ * stream warm). When everything is suspended, the fallback would
261
+ * dial the camera and wake it — defeats the sleeping cache.
262
+ */
263
+ async hasStreamingBrokerForDevice(deviceId) {
264
+ try {
265
+ return (await (await this.ctx.fetchDevice(deviceId)).cameraStreams?.getBrokerStreams({}) ?? []).some((s) => s.status === "streaming");
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ /**
271
+ * Diagnostic status for the `status` auto-injected cap method. Reports
272
+ * the cache bookkeeping for this device — when the last snapshot was
273
+ * captured, how stale the cached image is, its size, and which stream
274
+ * was used. Returns null when the device has never been captured
275
+ * (cache miss) since the addon started.
276
+ */
277
+ async getStatus(deviceId) {
278
+ const hit = this.cache.get(deviceId);
279
+ if (!hit) return null;
280
+ return {
281
+ lastCapturedAt: hit.ts,
282
+ cacheAgeMs: Date.now() - hit.ts,
283
+ lastBytes: hit.data.base64.length,
284
+ lastStreamId: hit.streamId
285
+ };
286
+ }
287
+ async readDeviceSettings(deviceId) {
288
+ if (!this.ctx.settings) return {};
289
+ const raw = await this.ctx.settings.readDeviceStore(deviceId);
290
+ const rawAge = raw["snapshotMaxAgeS"];
291
+ const maxAgeS = typeof rawAge === "number" && rawAge >= 0 && Number.isFinite(rawAge) ? rawAge : void 0;
292
+ return {
293
+ snapshotStreamId: typeof raw["snapshotStreamId"] === "string" ? raw["snapshotStreamId"] : void 0,
294
+ snapshotDebug: raw["snapshotDebug"] === true,
295
+ ...maxAgeS !== void 0 ? { snapshotMaxAgeS: maxAgeS } : {}
296
+ };
297
+ }
298
+ async buildDeviceSettingsContribution(deviceId) {
299
+ const meta = await this.lookupDeviceMeta(deviceId);
300
+ if (meta && meta.type !== DeviceType.Camera) return null;
301
+ const current = await this.readDeviceSettings(deviceId);
302
+ const streamOptions = await this.getStreamOptions(deviceId);
303
+ const isBattery = await this.isDeviceBattery(deviceId);
304
+ const defaultMaxAgeS = isBattery ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S;
305
+ return { sections: [{
306
+ id: "snapshot-preferences",
307
+ title: "Snapshot",
308
+ tab: "snapshot",
309
+ order: 60,
310
+ fields: [
311
+ {
312
+ type: "select",
313
+ key: "snapshotStreamId",
314
+ label: "Preferred stream",
315
+ description: "Stream used when grabbing a snapshot",
316
+ options: streamOptions,
317
+ required: true,
318
+ value: current.snapshotStreamId || "auto"
319
+ },
320
+ {
321
+ type: "number",
322
+ key: "snapshotMaxAgeS",
323
+ label: "Max cache age (s)",
324
+ 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.`,
325
+ min: 0,
326
+ max: 24 * 3600,
327
+ step: 1,
328
+ value: current.snapshotMaxAgeS ?? defaultMaxAgeS
329
+ },
330
+ {
331
+ type: "boolean",
332
+ key: "snapshotDebug",
333
+ label: "Debug logging",
334
+ description: "Log stream selection and timing details for this device.",
335
+ value: current.snapshotDebug ?? false
336
+ }
337
+ ]
338
+ }] };
339
+ }
340
+ /**
341
+ * Single-trip device lookup against device-manager. Returns the
342
+ * fields the wrapper actually consults — name (logging) + battery
343
+ * flag (cache window + broker-fallback gate). Sourced from the
344
+ * device-manager registry rather than the battery cap so the answer
345
+ * survives a momentarily-unreachable provider (the very condition
346
+ * we're trying to be resilient to).
347
+ *
348
+ * Logged at debug + null return on failure: every call site already
349
+ * has a sensible fallback path (cache hit, conservative default, …),
350
+ * so we don't want a transient device-manager hiccup to throw.
351
+ */
352
+ async lookupDeviceMeta(deviceId) {
353
+ const api = this.ctx.api;
354
+ if (!api) return null;
355
+ try {
356
+ const found = await api.deviceManager.getDevice.query({ deviceId });
357
+ if (!found) return null;
358
+ const features = found.features ?? [];
359
+ const rawType = found.type;
360
+ return {
361
+ ...found.name ? { name: found.name } : {},
362
+ isBattery: features.includes(DeviceFeature.BatteryOperated),
363
+ ...rawType ? { type: rawType } : {}
364
+ };
365
+ } catch (err) {
366
+ this.ctx.logger.debug("deviceManager.getDevice failed during snapshot", {
367
+ tags: { deviceId },
368
+ meta: { error: err instanceof Error ? err.message : String(err) }
369
+ });
370
+ return null;
371
+ }
372
+ }
373
+ /** Settings-UI helper — battery flag drives the default max-age in the field description. */
374
+ async isDeviceBattery(deviceId) {
375
+ return (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
376
+ }
377
+ async getStreamOptions(deviceId) {
378
+ const prefix = `${deviceId}/`;
379
+ try {
380
+ return [{
381
+ value: "auto",
382
+ label: "Auto"
383
+ }, ...(await (await this.ctx.fetchDevice(deviceId)).cameraStreams?.getRtspEntries({}) ?? []).filter((e) => e.enabled).map((e) => e.brokerId.slice(prefix.length)).map((id) => ({
384
+ value: id,
385
+ label: streamQualityLabel(id)
386
+ }))];
387
+ } catch (err) {
388
+ this.ctx.logger.error("getStreamOptions failed", {
389
+ tags: { deviceId },
390
+ meta: { error: errMsg(err) }
391
+ });
392
+ return [{
393
+ value: "auto",
394
+ label: "Auto"
395
+ }];
396
+ }
397
+ }
398
+ async saveDeviceSettingsPatch(deviceId, patch) {
399
+ if (!this.ctx.settings) throw new Error("[snapshot] settings store unavailable — cannot persist per-device settings");
400
+ const next = { ...await this.ctx.settings.readDeviceStore(deviceId) };
401
+ if ("snapshotStreamId" in patch) {
402
+ const v = patch["snapshotStreamId"];
403
+ next["snapshotStreamId"] = typeof v === "string" && v.trim().length > 0 ? v.trim() : "";
404
+ }
405
+ if ("snapshotDebug" in patch) next["snapshotDebug"] = patch["snapshotDebug"] === true;
406
+ if ("snapshotMaxAgeS" in patch) {
407
+ const v = patch["snapshotMaxAgeS"];
408
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) next["snapshotMaxAgeS"] = v;
409
+ else delete next["snapshotMaxAgeS"];
410
+ }
411
+ await this.ctx.settings.writeDeviceStore(deviceId, next);
412
+ this.cache.delete(deviceId);
413
+ return { success: true };
414
+ }
415
+ };
416
+ function isAbsentNativeError(msg) {
417
+ return msg.includes("no provider for") || msg.includes("no native provider for capability");
418
+ }
419
+ /**
420
+ * Quality ordering for broker picker: `high` > `mid` > `low` > other.
421
+ * The streamId is the suffix after `${deviceId}/` in the brokerId.
422
+ * Unknown labels fall through to 0 so they land at the bottom of the
423
+ * preference list without crashing.
424
+ */
425
+ function qualityRank(brokerId, prefix) {
426
+ const normalized = (brokerId.startsWith(prefix) ? brokerId.slice(prefix.length) : brokerId).toLowerCase();
427
+ if (normalized.includes("high") || normalized === "main" || normalized === "hd") return 3;
428
+ if (normalized.includes("mid") || normalized === "medium") return 2;
429
+ if (normalized.includes("low") || normalized === "sub" || normalized === "sd") return 1;
430
+ return 0;
431
+ }
432
+ /**
433
+ * Detect ffmpeg failure signatures that indicate the broker wasn't
434
+ * streaming yet when we tried to read its RTSP restream — a race
435
+ * window that closes as soon as the broker lands its first keyframe.
436
+ * Used by `grabFrameFromBroker` to retry once instead of giving up.
437
+ */
438
+ function isBrokerColdError(msg) {
439
+ return msg.includes("Invalid data found when processing input") || msg.includes("Error opening input") || msg.includes("Connection refused") || msg.includes("No route to host");
440
+ }
441
+ function runFfmpegFrameGrab(url, timeoutMs) {
442
+ return new Promise((resolve, reject) => {
443
+ execFile("ffmpeg", [
444
+ "-loglevel",
445
+ "error",
446
+ "-rtsp_transport",
447
+ "tcp",
448
+ "-fflags",
449
+ "+discardcorrupt",
450
+ "-skip_frame",
451
+ "nokey",
452
+ "-i",
453
+ url,
454
+ "-vf",
455
+ "select=eq(pict_type\\,I)",
456
+ "-vsync",
457
+ "vfr",
458
+ "-frames:v",
459
+ "1",
460
+ "-q:v",
461
+ "3",
462
+ "-f",
463
+ "image2pipe",
464
+ "-vcodec",
465
+ "mjpeg",
466
+ "pipe:1"
467
+ ], {
468
+ encoding: "buffer",
469
+ maxBuffer: 16 * 1024 * 1024,
470
+ timeout: timeoutMs
471
+ }, (err, stdout, stderr) => {
472
+ if (err) {
473
+ const errWithMeta = err;
474
+ const stderrText = Buffer.isBuffer(stderr) ? stderr.toString("utf8").trim() : String(stderr ?? "").trim();
475
+ const parts = [err instanceof Error ? err.message : String(err)];
476
+ if (errWithMeta.killed) parts.push("killed=true");
477
+ if (errWithMeta.code !== void 0) parts.push(`code=${String(errWithMeta.code)}`);
478
+ if (errWithMeta.signal) parts.push(`signal=${errWithMeta.signal}`);
479
+ if (stderrText) parts.push(`stderr: ${stderrText.slice(0, 500)}`);
480
+ reject(new Error(parts.join(" — ")));
481
+ return;
482
+ }
483
+ resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
484
+ }).on("error", (e) => reject(e));
485
+ });
486
+ }
487
+ //#endregion
488
+ export { SnapshotAddon, SnapshotAddon as default };
@@ -0,0 +1,120 @@
1
+ import { ProviderRegistration, BaseAddon } from '@camstack/types';
2
+ interface SnapshotAddonConfig {
3
+ /**
4
+ * Last-resort cache age (ms): if every live capture path fails
5
+ * (native + stream-broker fallback) AND a stale entry is older
6
+ * than this, the wrapper still returns the stale image rather
7
+ * than `null`. Keeps the UI from going blank during transient
8
+ * camera unreachability. Per-device freshness is governed by
9
+ * `snapshotMaxAgeS` instead.
10
+ */
11
+ readonly staleTtlMs: number;
12
+ }
13
+ /**
14
+ * SnapshotAddon — wrapper over the `snapshot` capability.
15
+ *
16
+ * Activated per-device (toggleable by user; default active). When active,
17
+ * caches fresh snapshots in memory. On cache miss, delegates to the native
18
+ * provider for this device via `ctx.getNativeProvider(snapshotCapability, id)`.
19
+ *
20
+ * No silent fallback — if the native fails, propagate the error up. Stale
21
+ * cache is returned ONLY when the native throws AND a stale entry exists,
22
+ * and the failure is logged so the fallback is never silent.
23
+ *
24
+ * Frame-grab fallback: when the native provider is absent (or returns null /
25
+ * throws), we ask the `stream-broker` capability for the device's RTSP
26
+ * restream URL and pipe one JPEG out of ffmpeg. stream-broker intentionally
27
+ * does NOT expose a `grabFrame` method — the orchestration (native-first,
28
+ * ffmpeg fallback, caching) is this addon's concern; stream-broker only
29
+ * publishes stream endpoints.
30
+ */
31
+ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
32
+ private readonly cache;
33
+ constructor();
34
+ protected onInitialize(): Promise<ProviderRegistration[]>;
35
+ protected onShutdown(): Promise<void>;
36
+ protected globalSettingsSchema(): import('@camstack/types').ConfigUISchema;
37
+ private getSnapshot;
38
+ /**
39
+ * Tell apart "native provider isn't registered for this device" from
40
+ * "native provider ran and threw a real error". The former is the steady
41
+ * state for cameras without a vendor snapshot endpoint and should not
42
+ * propagate as a 500; the latter should.
43
+ */
44
+ /**
45
+ * Pull one JPEG from the device's stream-broker RTSP restream using
46
+ * a short-lived ffmpeg invocation.
47
+ *
48
+ * Stream selection strategy (picks the broker that won't stall):
49
+ * 1. Explicit `preferredStreamId` (user set in per-device settings)
50
+ * — always honoured, even if currently idle. Operator choice
51
+ * wins.
52
+ * 2. Auto: highest-quality broker currently in `streaming` state.
53
+ * This is the whole point — prefer the stream that has active
54
+ * subscribers (usually `low` for detection, but `mid`/`high`
55
+ * if WebRTC is watching) so ffmpeg hits a warm pipe and
56
+ * doesn't race the broker's resume.
57
+ * 3. Fallback: highest-quality enabled entry regardless of status
58
+ * (will wake a suspended broker — retry-guarded against the
59
+ * cold-start error).
60
+ *
61
+ * The stream-broker auto-suspends idle streams on the "no demand"
62
+ * signal; snapshots used to default to `high` which was often the
63
+ * first to go idle, racing every snapshot with a broker resume.
64
+ * Now we ask the orchestrator of streams which one is warm and grab
65
+ * from there.
66
+ */
67
+ private grabFrameFromBroker;
68
+ /**
69
+ * Ffmpeg grab with one retry on the broker-cold-start error
70
+ * signature. Covers the window between "client connected" and
71
+ * "first keyframe" when a suspended broker resumes.
72
+ */
73
+ private runGrabWithResumeRetry;
74
+ private invalidateCache;
75
+ /**
76
+ * Non-throwing probe of the device's battery cap. Returns true only
77
+ * when a battery native is registered AND its current status says
78
+ * `sleeping: true`. Any error (no provider, native absent, getStatus
79
+ * missing, RPC timeout) is swallowed and treated as "awake" — we'd
80
+ * rather pay a wake-up than strand the caller on a cache that's
81
+ * semantically stale. Debug-logged for observability.
82
+ */
83
+ /**
84
+ * True when at least one of the device's brokers is actively
85
+ * streaming (status === 'streaming'). Used by the battery-cam guard
86
+ * around `grabFrameFromBroker` to allow the fallback ONLY when
87
+ * grabbing a frame is free (a consumer is already keeping the
88
+ * stream warm). When everything is suspended, the fallback would
89
+ * dial the camera and wake it — defeats the sleeping cache.
90
+ */
91
+ private hasStreamingBrokerForDevice;
92
+ /**
93
+ * Diagnostic status for the `status` auto-injected cap method. Reports
94
+ * the cache bookkeeping for this device — when the last snapshot was
95
+ * captured, how stale the cached image is, its size, and which stream
96
+ * was used. Returns null when the device has never been captured
97
+ * (cache miss) since the addon started.
98
+ */
99
+ private getStatus;
100
+ private readDeviceSettings;
101
+ private buildDeviceSettingsContribution;
102
+ /**
103
+ * Single-trip device lookup against device-manager. Returns the
104
+ * fields the wrapper actually consults — name (logging) + battery
105
+ * flag (cache window + broker-fallback gate). Sourced from the
106
+ * device-manager registry rather than the battery cap so the answer
107
+ * survives a momentarily-unreachable provider (the very condition
108
+ * we're trying to be resilient to).
109
+ *
110
+ * Logged at debug + null return on failure: every call site already
111
+ * has a sensible fallback path (cache hit, conservative default, …),
112
+ * so we don't want a transient device-manager hiccup to throw.
113
+ */
114
+ private lookupDeviceMeta;
115
+ /** Settings-UI helper — battery flag drives the default max-age in the field description. */
116
+ private isDeviceBattery;
117
+ private getStreamOptions;
118
+ private saveDeviceSettingsPatch;
119
+ }
120
+ export {};
@@ -0,0 +1,8 @@
1
+ import { default as Database } from 'better-sqlite3';
2
+ export declare class ConfigStore {
3
+ private readonly db;
4
+ constructor(db: Database.Database);
5
+ save(addonId: string, stableId: string | null, data: Record<string, unknown>): void;
6
+ load(addonId: string, stableId: string | null): Record<string, unknown>;
7
+ remove(addonId: string, stableId: string | null): void;
8
+ }
@@ -0,0 +1,23 @@
1
+ import { default as Database } from 'better-sqlite3';
2
+ export interface DeviceRow {
3
+ readonly stableId: string;
4
+ readonly type: string;
5
+ readonly name: string;
6
+ readonly parentStableId: string | null;
7
+ readonly enabled: boolean;
8
+ }
9
+ interface InsertInput {
10
+ readonly stableId: string;
11
+ readonly type: string;
12
+ readonly name: string;
13
+ readonly parentStableId: string | null;
14
+ }
15
+ export declare class DeviceStore {
16
+ private readonly db;
17
+ constructor(db: Database.Database);
18
+ insert(addonId: string, device: InsertInput): void;
19
+ listByAddon(addonId: string): readonly DeviceRow[];
20
+ listChildren(addonId: string, parentStableId: string): readonly DeviceRow[];
21
+ remove(addonId: string, stableId: string): void;
22
+ }
23
+ export {};
@@ -0,0 +1,25 @@
1
+ import { IFilesystemBrowseProvider } from '@camstack/types';
2
+ export declare class FilesystemBrowseProvider implements IFilesystemBrowseProvider {
3
+ private readonly allowedRoots;
4
+ /** allowedRoots is injected (read from addon config) so it stays per-node + testable. */
5
+ constructor(allowedRoots: () => readonly string[]);
6
+ listAllowedRoots(): Promise<readonly string[]>;
7
+ browse({ path }: {
8
+ path: string;
9
+ }): Promise<{
10
+ path: string;
11
+ entries: readonly {
12
+ name: string;
13
+ path: string;
14
+ }[];
15
+ freeBytes: number;
16
+ totalBytes: number;
17
+ }>;
18
+ createDir({ path }: {
19
+ path: string;
20
+ }): Promise<{
21
+ path: string;
22
+ }>;
23
+ /** Allowed roots with symlinks resolved (falls back to the raw root if it does not exist). */
24
+ private resolvedRoots;
25
+ }