@camstack/system 1.1.50 → 1.1.52
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/addon-runner.js +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/device-manager/device-linked-devices.d.ts +47 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1840 -1712
- package/dist/builtins/device-manager/device-manager.addon.mjs +1840 -1712
- package/dist/builtins/doorbell/index.d.ts +2 -0
- package/dist/builtins/doorbell/index.js +8 -0
- package/dist/builtins/doorbell/index.mjs +2 -0
- package/dist/builtins/doorbell/trigger-engine.d.ts +69 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +87 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +489 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +483 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +162 -12
- package/dist/builtins/snapshot/index.mjs +162 -12
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +38 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +26 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-CC3E-_3h.mjs → dist-B_PSvr-m.mjs} +237 -13
- package/dist/{dist-CQYo6Zp8.js → dist-BduBhv6N.js} +242 -12
- package/dist/http/file-data-plane.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -23
- package/dist/index.mjs +35 -24
- package/dist/kernel/addon-installer.d.ts +8 -0
- package/dist/kernel/deps/manifest-native-deps.d.ts +1 -1
- package/dist/kernel/deps/npm-command.d.ts +38 -0
- package/dist/{manifest-python-deps-DqATCsK0.mjs → manifest-python-deps-CIyvuHAB.mjs} +268 -124
- package/dist/{manifest-python-deps-Wy8dNJpr.js → manifest-python-deps-CPAMH5gV.js} +279 -123
- package/dist/{model-download-service-C-IHWnXx.mjs → model-download-service-Cp9f4dk6.mjs} +1 -1
- package/dist/{model-download-service-D-Umz4-L.js → model-download-service-hf0ookyy.js} +1 -1
- package/package.json +13 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.js");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
//#region src/builtins/snapshot/snapshot-coalescing.ts
|
|
9
9
|
/**
|
|
@@ -200,12 +200,102 @@ function raceForResult(promise, timeoutMs) {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
//#endregion
|
|
203
|
+
//#region src/builtins/snapshot/snapshot-media-handler.ts
|
|
204
|
+
/**
|
|
205
|
+
* Parse the handler-relative path (`/<deviceId>.jpg?query`) into a request.
|
|
206
|
+
* Returns null for a malformed / nested / non-numeric id so the handler answers
|
|
207
|
+
* 404 without ever reaching `getMedia`.
|
|
208
|
+
*/
|
|
209
|
+
function parseSnapshotMediaRequest(url) {
|
|
210
|
+
const qIdx = url.indexOf("?");
|
|
211
|
+
const rawPath = qIdx === -1 ? url : url.slice(0, qIdx);
|
|
212
|
+
const query = qIdx === -1 ? "" : url.slice(qIdx + 1);
|
|
213
|
+
const segment = rawPath.replace(/^\/+/, "");
|
|
214
|
+
if (segment.length === 0 || segment.includes("/")) return null;
|
|
215
|
+
const idPart = segment.replace(/\.jpe?g$/i, "");
|
|
216
|
+
if (!/^\d+$/.test(idPart)) return null;
|
|
217
|
+
const deviceId = Number.parseInt(idPart, 10);
|
|
218
|
+
if (!Number.isSafeInteger(deviceId)) return null;
|
|
219
|
+
const params = new URLSearchParams(query);
|
|
220
|
+
const rawStream = params.get("streamId");
|
|
221
|
+
const streamId = rawStream !== null && rawStream.length > 0 ? rawStream : void 0;
|
|
222
|
+
const rawForce = params.get("force");
|
|
223
|
+
return {
|
|
224
|
+
deviceId,
|
|
225
|
+
streamId,
|
|
226
|
+
force: rawForce === "1" || rawForce === "true"
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Create a data-plane handler that serves per-device snapshots as JPEG images.
|
|
231
|
+
* `deps.getMedia` is called once per request; null → 404, throw → 500. A
|
|
232
|
+
* conditional GET with a matching `If-None-Match` produces a 304.
|
|
233
|
+
*/
|
|
234
|
+
function createSnapshotMediaHandler(deps) {
|
|
235
|
+
return async (req, res) => {
|
|
236
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
237
|
+
res.writeHead(405, { allow: "GET, HEAD" }).end();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const parsed = parseSnapshotMediaRequest(req.url ?? "/");
|
|
241
|
+
if (parsed === null) {
|
|
242
|
+
res.writeHead(404).end();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
let media;
|
|
246
|
+
try {
|
|
247
|
+
media = await deps.getMedia(parsed.deviceId, parsed.streamId, parsed.force);
|
|
248
|
+
} catch {
|
|
249
|
+
const body = "Internal server error";
|
|
250
|
+
res.writeHead(500, {
|
|
251
|
+
"content-type": "text/plain",
|
|
252
|
+
"content-length": String(Buffer.byteLength(body))
|
|
253
|
+
});
|
|
254
|
+
if (req.method === "HEAD") res.end();
|
|
255
|
+
else res.end(body);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (media === null) {
|
|
259
|
+
res.writeHead(404).end();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const etag = `"${parsed.deviceId}-${media.capturedAt}"`;
|
|
263
|
+
const cacheControl = `private, max-age=${Math.max(0, Math.floor(media.maxAgeS))}`;
|
|
264
|
+
if (req.headers["if-none-match"] === etag) {
|
|
265
|
+
res.writeHead(304, {
|
|
266
|
+
etag,
|
|
267
|
+
"cache-control": cacheControl
|
|
268
|
+
}).end();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
res.writeHead(200, {
|
|
272
|
+
"content-type": media.contentType,
|
|
273
|
+
"cache-control": cacheControl,
|
|
274
|
+
etag,
|
|
275
|
+
"content-length": String(media.bytes.byteLength)
|
|
276
|
+
});
|
|
277
|
+
if (req.method === "HEAD") res.end();
|
|
278
|
+
else res.end(Buffer.from(media.bytes));
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
203
282
|
//#region src/builtins/snapshot/snapshot.addon.ts
|
|
204
283
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
205
284
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
206
285
|
/** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
|
|
207
286
|
var BATTERY_DEFAULT_MAX_AGE_S = 3600;
|
|
208
287
|
/**
|
|
288
|
+
* Effective per-device max cache age (seconds): the operator override
|
|
289
|
+
* (`snapshotMaxAgeS`) when a finite non-negative number, else the battery-aware
|
|
290
|
+
* default. Shared by the freshness gate (`getSnapshot`) and the HTTP endpoint's
|
|
291
|
+
* `Cache-Control: max-age` (so the browser/`expo-image` layer caches for exactly
|
|
292
|
+
* as long as the wrapper considers the frame fresh).
|
|
293
|
+
*/
|
|
294
|
+
function effectiveMaxAgeS(prefs, isBattery) {
|
|
295
|
+
const def = isBattery ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S;
|
|
296
|
+
return typeof prefs.snapshotMaxAgeS === "number" && prefs.snapshotMaxAgeS >= 0 ? prefs.snapshotMaxAgeS : def;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
209
299
|
* SnapshotAddon — wrapper over the `snapshot` capability.
|
|
210
300
|
*
|
|
211
301
|
* Activated per-device (toggleable by user; default active). When active,
|
|
@@ -245,25 +335,86 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
245
335
|
*/
|
|
246
336
|
ownerCache = null;
|
|
247
337
|
static OWNER_CACHE_TTL_MS = 3e4;
|
|
338
|
+
/**
|
|
339
|
+
* Handle for the `/addon/snapshot/media/<deviceId>.jpg` HTTP data-plane
|
|
340
|
+
* endpoint (§3.1). Null when the host provides no `ctx.dataPlane` facility
|
|
341
|
+
* (the tRPC `getSnapshot` base64 surface still works — the endpoint is a
|
|
342
|
+
* strictly-additive delivery path). Disposed on shutdown.
|
|
343
|
+
*/
|
|
344
|
+
mediaDataPlane = null;
|
|
248
345
|
constructor() {
|
|
249
346
|
super({ staleTtlMs: 6e4 });
|
|
250
347
|
}
|
|
251
348
|
async onInitialize() {
|
|
252
349
|
this.ctx.logger.info("Snapshot wrapper initialized");
|
|
350
|
+
const provider = {
|
|
351
|
+
getSnapshot: (input) => this.getSnapshot(input),
|
|
352
|
+
invalidateCache: (input) => this.invalidateCache(input),
|
|
353
|
+
getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
|
|
354
|
+
getDeviceLiveContribution: async () => null,
|
|
355
|
+
applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),
|
|
356
|
+
getStatus: async (input) => this.getStatus(input.deviceId),
|
|
357
|
+
getSnapshotOverview: (input) => this.getSnapshotOverview(input)
|
|
358
|
+
};
|
|
359
|
+
await this.serveMediaDataPlane();
|
|
253
360
|
return [{
|
|
254
361
|
capability: require_dist.snapshotCapability,
|
|
255
|
-
provider
|
|
256
|
-
getSnapshot: (input) => this.getSnapshot(input),
|
|
257
|
-
invalidateCache: (input) => this.invalidateCache(input),
|
|
258
|
-
getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
|
|
259
|
-
getDeviceLiveContribution: async () => null,
|
|
260
|
-
applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),
|
|
261
|
-
getStatus: async (input) => this.getStatus(input.deviceId),
|
|
262
|
-
getSnapshotOverview: (input) => this.getSnapshotOverview(input)
|
|
263
|
-
}
|
|
362
|
+
provider
|
|
264
363
|
}];
|
|
265
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Serve the per-device snapshot image endpoint on the addon data-plane
|
|
367
|
+
* (§3.1). Delivery: `GET /addon/snapshot/media/<deviceId>.jpg` → raw JPEG with
|
|
368
|
+
* `ETag` + `Cache-Control: private, max-age=<device maxAgeS>`, so a grid is N
|
|
369
|
+
* independent `<Image>` tiles served from the coalesced server cache (no
|
|
370
|
+
* base64 tax, no batch head-of-line blocking, browser/`expo-image` caching for
|
|
371
|
+
* free). Best-effort: a host without a `dataPlane` facility keeps the tRPC
|
|
372
|
+
* base64 path.
|
|
373
|
+
*/
|
|
374
|
+
async serveMediaDataPlane() {
|
|
375
|
+
const handler = createSnapshotMediaHandler({ getMedia: (deviceId, streamId, force) => this.resolveSnapshotMedia(deviceId, streamId, force) });
|
|
376
|
+
try {
|
|
377
|
+
this.mediaDataPlane = await this.ctx.dataPlane?.serve({
|
|
378
|
+
prefix: "media",
|
|
379
|
+
access: "authenticated",
|
|
380
|
+
handler
|
|
381
|
+
}) ?? null;
|
|
382
|
+
this.ctx.logger.info("snapshot media data-plane served", { meta: { baseUrl: this.mediaDataPlane ? `/addon/${this.ctx.id}/media` : "(no dataPlane facility)" } });
|
|
383
|
+
} catch (err) {
|
|
384
|
+
this.ctx.logger.warn("snapshot media data-plane failed to serve", { meta: { error: require_dist.errMsg(err) } });
|
|
385
|
+
this.mediaDataPlane = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Resolve a device (+ optional stream, + force) to renderable JPEG bytes for
|
|
390
|
+
* the HTTP endpoint. Runs the same coalesced capture ladder as the tRPC
|
|
391
|
+
* `getSnapshot` (so every consumer shares one upstream pull), then decodes the
|
|
392
|
+
* base64 cache into raw bytes and stamps the frame identity: `capturedAt` from
|
|
393
|
+
* the cache entry (matching the ETag `getSnapshotOverview` advertises) and the
|
|
394
|
+
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
395
|
+
*/
|
|
396
|
+
async resolveSnapshotMedia(deviceId, streamId, force) {
|
|
397
|
+
const image = await this.getSnapshot({
|
|
398
|
+
deviceId,
|
|
399
|
+
...streamId !== void 0 ? { streamId } : {},
|
|
400
|
+
force
|
|
401
|
+
});
|
|
402
|
+
if (!image) return null;
|
|
403
|
+
const capturedAt = this.cache.get(deviceId)?.ts ?? Date.now();
|
|
404
|
+
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
405
|
+
const isBattery = (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
|
|
406
|
+
return {
|
|
407
|
+
bytes: Buffer.from(image.base64, "base64"),
|
|
408
|
+
contentType: image.contentType,
|
|
409
|
+
capturedAt,
|
|
410
|
+
maxAgeS: effectiveMaxAgeS(prefs, isBattery)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
266
413
|
async onShutdown() {
|
|
414
|
+
if (this.mediaDataPlane) {
|
|
415
|
+
await this.mediaDataPlane.dispose().catch(() => void 0);
|
|
416
|
+
this.mediaDataPlane = null;
|
|
417
|
+
}
|
|
267
418
|
this.cache.clear();
|
|
268
419
|
this.captureFlight.clear();
|
|
269
420
|
this.ownerCache = null;
|
|
@@ -316,8 +467,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
316
467
|
tags: { deviceId },
|
|
317
468
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
318
469
|
});
|
|
319
|
-
const
|
|
320
|
-
const effectiveMaxAgeMs = (typeof prefs.snapshotMaxAgeS === "number" && prefs.snapshotMaxAgeS >= 0 ? prefs.snapshotMaxAgeS : defaultMaxAgeS) * 1e3;
|
|
470
|
+
const effectiveMaxAgeMs = effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3;
|
|
321
471
|
const decision = decideSnapshotServe({
|
|
322
472
|
now,
|
|
323
473
|
cachedAt: hit?.ts ?? null,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as streamQualityLabel, B as nodePin, X as snapshotCapability, ct as DeviceFeature, it as BaseAddon, rt as errMsg, ut as DeviceType } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
//#region src/builtins/snapshot/snapshot-coalescing.ts
|
|
4
4
|
/**
|
|
@@ -195,12 +195,102 @@ function raceForResult(promise, timeoutMs) {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
//#endregion
|
|
198
|
+
//#region src/builtins/snapshot/snapshot-media-handler.ts
|
|
199
|
+
/**
|
|
200
|
+
* Parse the handler-relative path (`/<deviceId>.jpg?query`) into a request.
|
|
201
|
+
* Returns null for a malformed / nested / non-numeric id so the handler answers
|
|
202
|
+
* 404 without ever reaching `getMedia`.
|
|
203
|
+
*/
|
|
204
|
+
function parseSnapshotMediaRequest(url) {
|
|
205
|
+
const qIdx = url.indexOf("?");
|
|
206
|
+
const rawPath = qIdx === -1 ? url : url.slice(0, qIdx);
|
|
207
|
+
const query = qIdx === -1 ? "" : url.slice(qIdx + 1);
|
|
208
|
+
const segment = rawPath.replace(/^\/+/, "");
|
|
209
|
+
if (segment.length === 0 || segment.includes("/")) return null;
|
|
210
|
+
const idPart = segment.replace(/\.jpe?g$/i, "");
|
|
211
|
+
if (!/^\d+$/.test(idPart)) return null;
|
|
212
|
+
const deviceId = Number.parseInt(idPart, 10);
|
|
213
|
+
if (!Number.isSafeInteger(deviceId)) return null;
|
|
214
|
+
const params = new URLSearchParams(query);
|
|
215
|
+
const rawStream = params.get("streamId");
|
|
216
|
+
const streamId = rawStream !== null && rawStream.length > 0 ? rawStream : void 0;
|
|
217
|
+
const rawForce = params.get("force");
|
|
218
|
+
return {
|
|
219
|
+
deviceId,
|
|
220
|
+
streamId,
|
|
221
|
+
force: rawForce === "1" || rawForce === "true"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Create a data-plane handler that serves per-device snapshots as JPEG images.
|
|
226
|
+
* `deps.getMedia` is called once per request; null → 404, throw → 500. A
|
|
227
|
+
* conditional GET with a matching `If-None-Match` produces a 304.
|
|
228
|
+
*/
|
|
229
|
+
function createSnapshotMediaHandler(deps) {
|
|
230
|
+
return async (req, res) => {
|
|
231
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
232
|
+
res.writeHead(405, { allow: "GET, HEAD" }).end();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const parsed = parseSnapshotMediaRequest(req.url ?? "/");
|
|
236
|
+
if (parsed === null) {
|
|
237
|
+
res.writeHead(404).end();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let media;
|
|
241
|
+
try {
|
|
242
|
+
media = await deps.getMedia(parsed.deviceId, parsed.streamId, parsed.force);
|
|
243
|
+
} catch {
|
|
244
|
+
const body = "Internal server error";
|
|
245
|
+
res.writeHead(500, {
|
|
246
|
+
"content-type": "text/plain",
|
|
247
|
+
"content-length": String(Buffer.byteLength(body))
|
|
248
|
+
});
|
|
249
|
+
if (req.method === "HEAD") res.end();
|
|
250
|
+
else res.end(body);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (media === null) {
|
|
254
|
+
res.writeHead(404).end();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const etag = `"${parsed.deviceId}-${media.capturedAt}"`;
|
|
258
|
+
const cacheControl = `private, max-age=${Math.max(0, Math.floor(media.maxAgeS))}`;
|
|
259
|
+
if (req.headers["if-none-match"] === etag) {
|
|
260
|
+
res.writeHead(304, {
|
|
261
|
+
etag,
|
|
262
|
+
"cache-control": cacheControl
|
|
263
|
+
}).end();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
res.writeHead(200, {
|
|
267
|
+
"content-type": media.contentType,
|
|
268
|
+
"cache-control": cacheControl,
|
|
269
|
+
etag,
|
|
270
|
+
"content-length": String(media.bytes.byteLength)
|
|
271
|
+
});
|
|
272
|
+
if (req.method === "HEAD") res.end();
|
|
273
|
+
else res.end(Buffer.from(media.bytes));
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
198
277
|
//#region src/builtins/snapshot/snapshot.addon.ts
|
|
199
278
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
200
279
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
201
280
|
/** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
|
|
202
281
|
var BATTERY_DEFAULT_MAX_AGE_S = 3600;
|
|
203
282
|
/**
|
|
283
|
+
* Effective per-device max cache age (seconds): the operator override
|
|
284
|
+
* (`snapshotMaxAgeS`) when a finite non-negative number, else the battery-aware
|
|
285
|
+
* default. Shared by the freshness gate (`getSnapshot`) and the HTTP endpoint's
|
|
286
|
+
* `Cache-Control: max-age` (so the browser/`expo-image` layer caches for exactly
|
|
287
|
+
* as long as the wrapper considers the frame fresh).
|
|
288
|
+
*/
|
|
289
|
+
function effectiveMaxAgeS(prefs, isBattery) {
|
|
290
|
+
const def = isBattery ? BATTERY_DEFAULT_MAX_AGE_S : NON_BATTERY_DEFAULT_MAX_AGE_S;
|
|
291
|
+
return typeof prefs.snapshotMaxAgeS === "number" && prefs.snapshotMaxAgeS >= 0 ? prefs.snapshotMaxAgeS : def;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
204
294
|
* SnapshotAddon — wrapper over the `snapshot` capability.
|
|
205
295
|
*
|
|
206
296
|
* Activated per-device (toggleable by user; default active). When active,
|
|
@@ -240,25 +330,86 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
240
330
|
*/
|
|
241
331
|
ownerCache = null;
|
|
242
332
|
static OWNER_CACHE_TTL_MS = 3e4;
|
|
333
|
+
/**
|
|
334
|
+
* Handle for the `/addon/snapshot/media/<deviceId>.jpg` HTTP data-plane
|
|
335
|
+
* endpoint (§3.1). Null when the host provides no `ctx.dataPlane` facility
|
|
336
|
+
* (the tRPC `getSnapshot` base64 surface still works — the endpoint is a
|
|
337
|
+
* strictly-additive delivery path). Disposed on shutdown.
|
|
338
|
+
*/
|
|
339
|
+
mediaDataPlane = null;
|
|
243
340
|
constructor() {
|
|
244
341
|
super({ staleTtlMs: 6e4 });
|
|
245
342
|
}
|
|
246
343
|
async onInitialize() {
|
|
247
344
|
this.ctx.logger.info("Snapshot wrapper initialized");
|
|
345
|
+
const provider = {
|
|
346
|
+
getSnapshot: (input) => this.getSnapshot(input),
|
|
347
|
+
invalidateCache: (input) => this.invalidateCache(input),
|
|
348
|
+
getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
|
|
349
|
+
getDeviceLiveContribution: async () => null,
|
|
350
|
+
applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),
|
|
351
|
+
getStatus: async (input) => this.getStatus(input.deviceId),
|
|
352
|
+
getSnapshotOverview: (input) => this.getSnapshotOverview(input)
|
|
353
|
+
};
|
|
354
|
+
await this.serveMediaDataPlane();
|
|
248
355
|
return [{
|
|
249
356
|
capability: snapshotCapability,
|
|
250
|
-
provider
|
|
251
|
-
getSnapshot: (input) => this.getSnapshot(input),
|
|
252
|
-
invalidateCache: (input) => this.invalidateCache(input),
|
|
253
|
-
getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
|
|
254
|
-
getDeviceLiveContribution: async () => null,
|
|
255
|
-
applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch),
|
|
256
|
-
getStatus: async (input) => this.getStatus(input.deviceId),
|
|
257
|
-
getSnapshotOverview: (input) => this.getSnapshotOverview(input)
|
|
258
|
-
}
|
|
357
|
+
provider
|
|
259
358
|
}];
|
|
260
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Serve the per-device snapshot image endpoint on the addon data-plane
|
|
362
|
+
* (§3.1). Delivery: `GET /addon/snapshot/media/<deviceId>.jpg` → raw JPEG with
|
|
363
|
+
* `ETag` + `Cache-Control: private, max-age=<device maxAgeS>`, so a grid is N
|
|
364
|
+
* independent `<Image>` tiles served from the coalesced server cache (no
|
|
365
|
+
* base64 tax, no batch head-of-line blocking, browser/`expo-image` caching for
|
|
366
|
+
* free). Best-effort: a host without a `dataPlane` facility keeps the tRPC
|
|
367
|
+
* base64 path.
|
|
368
|
+
*/
|
|
369
|
+
async serveMediaDataPlane() {
|
|
370
|
+
const handler = createSnapshotMediaHandler({ getMedia: (deviceId, streamId, force) => this.resolveSnapshotMedia(deviceId, streamId, force) });
|
|
371
|
+
try {
|
|
372
|
+
this.mediaDataPlane = await this.ctx.dataPlane?.serve({
|
|
373
|
+
prefix: "media",
|
|
374
|
+
access: "authenticated",
|
|
375
|
+
handler
|
|
376
|
+
}) ?? null;
|
|
377
|
+
this.ctx.logger.info("snapshot media data-plane served", { meta: { baseUrl: this.mediaDataPlane ? `/addon/${this.ctx.id}/media` : "(no dataPlane facility)" } });
|
|
378
|
+
} catch (err) {
|
|
379
|
+
this.ctx.logger.warn("snapshot media data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
380
|
+
this.mediaDataPlane = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Resolve a device (+ optional stream, + force) to renderable JPEG bytes for
|
|
385
|
+
* the HTTP endpoint. Runs the same coalesced capture ladder as the tRPC
|
|
386
|
+
* `getSnapshot` (so every consumer shares one upstream pull), then decodes the
|
|
387
|
+
* base64 cache into raw bytes and stamps the frame identity: `capturedAt` from
|
|
388
|
+
* the cache entry (matching the ETag `getSnapshotOverview` advertises) and the
|
|
389
|
+
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
390
|
+
*/
|
|
391
|
+
async resolveSnapshotMedia(deviceId, streamId, force) {
|
|
392
|
+
const image = await this.getSnapshot({
|
|
393
|
+
deviceId,
|
|
394
|
+
...streamId !== void 0 ? { streamId } : {},
|
|
395
|
+
force
|
|
396
|
+
});
|
|
397
|
+
if (!image) return null;
|
|
398
|
+
const capturedAt = this.cache.get(deviceId)?.ts ?? Date.now();
|
|
399
|
+
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
400
|
+
const isBattery = (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
|
|
401
|
+
return {
|
|
402
|
+
bytes: Buffer.from(image.base64, "base64"),
|
|
403
|
+
contentType: image.contentType,
|
|
404
|
+
capturedAt,
|
|
405
|
+
maxAgeS: effectiveMaxAgeS(prefs, isBattery)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
261
408
|
async onShutdown() {
|
|
409
|
+
if (this.mediaDataPlane) {
|
|
410
|
+
await this.mediaDataPlane.dispose().catch(() => void 0);
|
|
411
|
+
this.mediaDataPlane = null;
|
|
412
|
+
}
|
|
262
413
|
this.cache.clear();
|
|
263
414
|
this.captureFlight.clear();
|
|
264
415
|
this.ownerCache = null;
|
|
@@ -311,8 +462,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
311
462
|
tags: { deviceId },
|
|
312
463
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
313
464
|
});
|
|
314
|
-
const
|
|
315
|
-
const effectiveMaxAgeMs = (typeof prefs.snapshotMaxAgeS === "number" && prefs.snapshotMaxAgeS >= 0 ? prefs.snapshotMaxAgeS : defaultMaxAgeS) * 1e3;
|
|
465
|
+
const effectiveMaxAgeMs = effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3;
|
|
316
466
|
const decision = decideSnapshotServe({
|
|
317
467
|
now,
|
|
318
468
|
cachedAt: hit?.ts ?? null,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DataPlaneHandler } from '@camstack/types';
|
|
2
|
+
/** A renderable snapshot frame resolved for one device. */
|
|
3
|
+
export interface SnapshotMedia {
|
|
4
|
+
/** Raw image bytes (already decoded from the wrapper's base64 cache). */
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
/** MIME type — practically always `image/jpeg`. */
|
|
7
|
+
readonly contentType: string;
|
|
8
|
+
/** Epoch ms of the cached capture — drives the ETag and frame identity. */
|
|
9
|
+
readonly capturedAt: number;
|
|
10
|
+
/** Effective per-device max cache age (seconds) → `Cache-Control: max-age`. */
|
|
11
|
+
readonly maxAgeS: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SnapshotMediaHandlerDeps {
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a device (+ optional stream, + force) to a renderable frame. Runs
|
|
16
|
+
* the wrapper's coalesced capture ladder; returns null when no frame can be
|
|
17
|
+
* produced (→ 404). Throwing produces an opaque 500 (caller logs).
|
|
18
|
+
*/
|
|
19
|
+
readonly getMedia: (deviceId: number, streamId: string | undefined, force: boolean) => Promise<SnapshotMedia | null>;
|
|
20
|
+
}
|
|
21
|
+
/** Parsed `/<deviceId>[.jpg][?streamId=…&force=1]` request. */
|
|
22
|
+
export interface SnapshotMediaRequest {
|
|
23
|
+
readonly deviceId: number;
|
|
24
|
+
readonly streamId: string | undefined;
|
|
25
|
+
readonly force: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse the handler-relative path (`/<deviceId>.jpg?query`) into a request.
|
|
29
|
+
* Returns null for a malformed / nested / non-numeric id so the handler answers
|
|
30
|
+
* 404 without ever reaching `getMedia`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseSnapshotMediaRequest(url: string): SnapshotMediaRequest | null;
|
|
33
|
+
/**
|
|
34
|
+
* Create a data-plane handler that serves per-device snapshots as JPEG images.
|
|
35
|
+
* `deps.getMedia` is called once per request; null → 404, throw → 500. A
|
|
36
|
+
* conditional GET with a matching `If-None-Match` produces a 304.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createSnapshotMediaHandler(deps: SnapshotMediaHandlerDeps): DataPlaneHandler;
|
|
@@ -50,8 +50,34 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
50
50
|
*/
|
|
51
51
|
private ownerCache;
|
|
52
52
|
private static readonly OWNER_CACHE_TTL_MS;
|
|
53
|
+
/**
|
|
54
|
+
* Handle for the `/addon/snapshot/media/<deviceId>.jpg` HTTP data-plane
|
|
55
|
+
* endpoint (§3.1). Null when the host provides no `ctx.dataPlane` facility
|
|
56
|
+
* (the tRPC `getSnapshot` base64 surface still works — the endpoint is a
|
|
57
|
+
* strictly-additive delivery path). Disposed on shutdown.
|
|
58
|
+
*/
|
|
59
|
+
private mediaDataPlane;
|
|
53
60
|
constructor();
|
|
54
61
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Serve the per-device snapshot image endpoint on the addon data-plane
|
|
64
|
+
* (§3.1). Delivery: `GET /addon/snapshot/media/<deviceId>.jpg` → raw JPEG with
|
|
65
|
+
* `ETag` + `Cache-Control: private, max-age=<device maxAgeS>`, so a grid is N
|
|
66
|
+
* independent `<Image>` tiles served from the coalesced server cache (no
|
|
67
|
+
* base64 tax, no batch head-of-line blocking, browser/`expo-image` caching for
|
|
68
|
+
* free). Best-effort: a host without a `dataPlane` facility keeps the tRPC
|
|
69
|
+
* base64 path.
|
|
70
|
+
*/
|
|
71
|
+
private serveMediaDataPlane;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a device (+ optional stream, + force) to renderable JPEG bytes for
|
|
74
|
+
* the HTTP endpoint. Runs the same coalesced capture ladder as the tRPC
|
|
75
|
+
* `getSnapshot` (so every consumer shares one upstream pull), then decodes the
|
|
76
|
+
* base64 cache into raw bytes and stamps the frame identity: `capturedAt` from
|
|
77
|
+
* the cache entry (matching the ETag `getSnapshotOverview` advertises) and the
|
|
78
|
+
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
79
|
+
*/
|
|
80
|
+
private resolveSnapshotMedia;
|
|
55
81
|
protected onShutdown(): Promise<void>;
|
|
56
82
|
/**
|
|
57
83
|
* Drop the cached ingest owner whenever addon-level settings change —
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
node_fs = require_chunk.__toESM(node_fs);
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as filesystemBrowseCapability, Q as storageProviderCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let better_sqlite3 = require("better-sqlite3");
|
|
9
9
|
better_sqlite3 = require_chunk.__toESM(better_sqlite3);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Y as settingsStoreCapability, it as BaseAddon, mt as asJsonObject, rt as errMsg, s as RUNTIME_DEFAULTS, wt as parseJsonUnknown } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.js");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as parseJsonObject,
|
|
1
|
+
import { Ct as parseJsonObject, Z as storageCapability, it as BaseAddon, u as StorageLocationTypeSchema } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import { buildStorageLocationRegistry } from "@camstack/system";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.js");
|
|
7
7
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
8
8
|
/**
|
|
9
9
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -108,9 +108,9 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
|
|
|
108
108
|
type: "text",
|
|
109
109
|
key: "tokenExpiry",
|
|
110
110
|
label: "Token Expiry",
|
|
111
|
-
description: "JWT token lifetime (e.g. 24h, 7d,
|
|
112
|
-
placeholder: "
|
|
113
|
-
default: "
|
|
111
|
+
description: "JWT token lifetime (e.g. 24h, 7d, 30d)",
|
|
112
|
+
placeholder: "30d",
|
|
113
|
+
default: "30d"
|
|
114
114
|
}]
|
|
115
115
|
}] };
|
|
116
116
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { it as BaseAddon, rt as errMsg, xt as hydrateSchema } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
3
3
|
/**
|
|
4
4
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -103,9 +103,9 @@ var SystemConfigAddon = class extends BaseAddon {
|
|
|
103
103
|
type: "text",
|
|
104
104
|
key: "tokenExpiry",
|
|
105
105
|
label: "Token Expiry",
|
|
106
|
-
description: "JWT token lifetime (e.g. 24h, 7d,
|
|
107
|
-
placeholder: "
|
|
108
|
-
default: "
|
|
106
|
+
description: "JWT token lifetime (e.g. 24h, 7d, 30d)",
|
|
107
|
+
placeholder: "30d",
|
|
108
|
+
default: "30d"
|
|
109
109
|
}]
|
|
110
110
|
}] };
|
|
111
111
|
}
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { L as logDestinationCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|