@camstack/system 1.2.38 → 1.2.40
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 +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/core-blocks/block-package.d.ts +59 -0
- package/dist/builtins/core-blocks/block-supervisor.d.ts +86 -0
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +18 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +533 -68
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +528 -69
- package/dist/builtins/core-blocks/index.d.ts +5 -3
- package/dist/builtins/core-blocks/index.js +6 -0
- package/dist/builtins/core-blocks/index.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +14 -6
- package/dist/builtins/device-manager/device-manager.addon.mjs +14 -6
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- 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/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.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 +281 -16
- package/dist/builtins/snapshot/index.mjs +282 -17
- package/dist/builtins/snapshot/snapshot-cache.d.ts +45 -0
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +32 -2
- package/dist/builtins/snapshot/snapshot-resize.d.ts +50 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +13 -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/integration-registry.d.ts +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 +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-DO76YVO2.mjs → dist-BXS0DP92.mjs} +24 -2
- package/dist/{dist-CFinlH7b.js → dist-Cj0LAuXi.js} +24 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -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-Cj0LAuXi.js");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
//#region src/builtins/snapshot/snapshot-coalescing.ts
|
|
9
9
|
/**
|
|
@@ -202,6 +202,32 @@ function raceForResult(promise, timeoutMs) {
|
|
|
202
202
|
//#endregion
|
|
203
203
|
//#region src/builtins/snapshot/snapshot-media-handler.ts
|
|
204
204
|
/**
|
|
205
|
+
* The widths a thumbnail may be served at.
|
|
206
|
+
*
|
|
207
|
+
* A ladder, not a free integer: the width is a cache key AND an ffmpeg run, so
|
|
208
|
+
* honouring `?w=` verbatim would let any caller mint unbounded work and
|
|
209
|
+
* unbounded memory. Twelve tiles measured at 181–240 px collapse onto one
|
|
210
|
+
* variant instead of twelve.
|
|
211
|
+
*
|
|
212
|
+
* Rungs are chosen for the card sizes the viewer actually renders (180–390 px)
|
|
213
|
+
* with headroom for a 2× device pixel ratio.
|
|
214
|
+
*/
|
|
215
|
+
var SNAPSHOT_WIDTH_LADDER = [
|
|
216
|
+
160,
|
|
217
|
+
240,
|
|
218
|
+
320,
|
|
219
|
+
480,
|
|
220
|
+
640,
|
|
221
|
+
960
|
|
222
|
+
];
|
|
223
|
+
/** Snap UP to the next rung — never below what was asked, so the client is not
|
|
224
|
+
* handed an image it has to upscale. Above the ladder, the largest rung: the
|
|
225
|
+
* point is to stop serving the 4K original. */
|
|
226
|
+
function snapSnapshotWidth(requested) {
|
|
227
|
+
for (const rung of SNAPSHOT_WIDTH_LADDER) if (requested <= rung) return rung;
|
|
228
|
+
return SNAPSHOT_WIDTH_LADDER[SNAPSHOT_WIDTH_LADDER.length - 1] ?? requested;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
205
231
|
* Parse the handler-relative path (`/<deviceId>.jpg?query`) into a request.
|
|
206
232
|
* Returns null for a malformed / nested / non-numeric id so the handler answers
|
|
207
233
|
* 404 without ever reaching `getMedia`.
|
|
@@ -220,13 +246,33 @@ function parseSnapshotMediaRequest(url) {
|
|
|
220
246
|
const rawStream = params.get("streamId");
|
|
221
247
|
const streamId = rawStream !== null && rawStream.length > 0 ? rawStream : void 0;
|
|
222
248
|
const rawForce = params.get("force");
|
|
249
|
+
const force = rawForce === "1" || rawForce === "true";
|
|
250
|
+
const rawWidth = params.get("w");
|
|
223
251
|
return {
|
|
224
252
|
deviceId,
|
|
225
253
|
streamId,
|
|
226
|
-
force
|
|
254
|
+
force,
|
|
255
|
+
width: rawWidth !== null && /^\d+$/.test(rawWidth) && Number.parseInt(rawWidth, 10) > 0 ? snapSnapshotWidth(Number.parseInt(rawWidth, 10)) : void 0
|
|
227
256
|
};
|
|
228
257
|
}
|
|
229
258
|
/**
|
|
259
|
+
* The response's entity tag.
|
|
260
|
+
*
|
|
261
|
+
* It has to identify the VARIANT, not just the frame: the same capture can now
|
|
262
|
+
* be served at several widths and from several streams, and a plain
|
|
263
|
+
* `"<device>-<capturedAt>"` would let a client that fetched `?w=320` receive a
|
|
264
|
+
* 304 for `?w=960` and render the small image at full size. The base form is
|
|
265
|
+
* unchanged for a plain request, so the identity
|
|
266
|
+
* `snapshot.getSnapshotOverview` advertises still matches the frame nobody
|
|
267
|
+
* asked to resize.
|
|
268
|
+
*/
|
|
269
|
+
function snapshotEtag(request, capturedAt) {
|
|
270
|
+
const parts = [`${String(request.deviceId)}-${String(capturedAt)}`];
|
|
271
|
+
if (request.streamId !== void 0) parts.push(`s${request.streamId}`);
|
|
272
|
+
if (request.width !== void 0) parts.push(`w${String(request.width)}`);
|
|
273
|
+
return `"${parts.join("-")}"`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
230
276
|
* Create a data-plane handler that serves per-device snapshots as JPEG images.
|
|
231
277
|
* `deps.getMedia` is called once per request; null → 404, throw → 500. A
|
|
232
278
|
* conditional GET with a matching `If-None-Match` produces a 304.
|
|
@@ -244,7 +290,7 @@ function createSnapshotMediaHandler(deps) {
|
|
|
244
290
|
}
|
|
245
291
|
let media;
|
|
246
292
|
try {
|
|
247
|
-
media = await deps.getMedia(parsed.deviceId, parsed.streamId, parsed.force);
|
|
293
|
+
media = await deps.getMedia(parsed.deviceId, parsed.streamId, parsed.force, parsed.width);
|
|
248
294
|
} catch {
|
|
249
295
|
const body = "Internal server error";
|
|
250
296
|
res.writeHead(500, {
|
|
@@ -259,7 +305,7 @@ function createSnapshotMediaHandler(deps) {
|
|
|
259
305
|
res.writeHead(404).end();
|
|
260
306
|
return;
|
|
261
307
|
}
|
|
262
|
-
const etag =
|
|
308
|
+
const etag = snapshotEtag(parsed, media.capturedAt);
|
|
263
309
|
const cacheControl = `private, max-age=${Math.max(0, Math.floor(media.maxAgeS))}`;
|
|
264
310
|
if (req.headers["if-none-match"] === etag) {
|
|
265
311
|
res.writeHead(304, {
|
|
@@ -279,6 +325,182 @@ function createSnapshotMediaHandler(deps) {
|
|
|
279
325
|
};
|
|
280
326
|
}
|
|
281
327
|
//#endregion
|
|
328
|
+
//#region src/builtins/snapshot/snapshot-cache.ts
|
|
329
|
+
/** A request with no explicit stream is its OWN entry, not a wildcard: the
|
|
330
|
+
* per-device preference decides what it captures, and conflating it with an
|
|
331
|
+
* explicit request is how the original bug read. */
|
|
332
|
+
var AUTO = "auto";
|
|
333
|
+
function keyOf(deviceId, streamId) {
|
|
334
|
+
return `${deviceId}:${streamId ?? AUTO}`;
|
|
335
|
+
}
|
|
336
|
+
var SnapshotCache = class {
|
|
337
|
+
byKey = /* @__PURE__ */ new Map();
|
|
338
|
+
/** deviceId → its live keys, so invalidation is O(streams) not O(cache). */
|
|
339
|
+
keysByDevice = /* @__PURE__ */ new Map();
|
|
340
|
+
get(deviceId, streamId) {
|
|
341
|
+
return this.byKey.get(keyOf(deviceId, streamId));
|
|
342
|
+
}
|
|
343
|
+
set(deviceId, streamId, entry) {
|
|
344
|
+
const key = keyOf(deviceId, streamId);
|
|
345
|
+
this.byKey.set(key, entry);
|
|
346
|
+
const keys = this.keysByDevice.get(deviceId) ?? /* @__PURE__ */ new Set();
|
|
347
|
+
keys.add(key);
|
|
348
|
+
this.keysByDevice.set(deviceId, keys);
|
|
349
|
+
}
|
|
350
|
+
/** The newest entry for a device, whichever stream produced it. */
|
|
351
|
+
latest(deviceId) {
|
|
352
|
+
let newest;
|
|
353
|
+
for (const key of this.keysByDevice.get(deviceId) ?? []) {
|
|
354
|
+
const entry = this.byKey.get(key);
|
|
355
|
+
if (entry !== void 0 && (newest === void 0 || entry.ts > newest.ts)) newest = entry;
|
|
356
|
+
}
|
|
357
|
+
return newest;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Drop every stream of one device.
|
|
361
|
+
*
|
|
362
|
+
* Device-wide on purpose: a settings change or an operator refresh
|
|
363
|
+
* invalidates the CAMERA, and leaving a sibling entry behind would keep
|
|
364
|
+
* serving the pre-change frame from the other key.
|
|
365
|
+
*/
|
|
366
|
+
deleteDevice(deviceId) {
|
|
367
|
+
for (const key of this.keysByDevice.get(deviceId) ?? []) this.byKey.delete(key);
|
|
368
|
+
this.keysByDevice.delete(deviceId);
|
|
369
|
+
}
|
|
370
|
+
clear() {
|
|
371
|
+
this.byKey.clear();
|
|
372
|
+
this.keysByDevice.clear();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/builtins/snapshot/snapshot-resize.ts
|
|
377
|
+
/**
|
|
378
|
+
* Downscaling a captured frame to a card-sized thumbnail.
|
|
379
|
+
*
|
|
380
|
+
* Applied AFTER capture rather than during it, and that is deliberate: the two
|
|
381
|
+
* capture paths (the vendor's native HTTP snapshot and the ffmpeg keyframe
|
|
382
|
+
* grab) produce a JPEG by different routes, and only one of them has an
|
|
383
|
+
* ffmpeg filter chain to hook into. Resizing the finished bytes gives both the
|
|
384
|
+
* same behaviour with one implementation.
|
|
385
|
+
*
|
|
386
|
+
* The cost is one ffmpeg run per (device, stream, width) per cache window — not
|
|
387
|
+
* one per request. A camera whose card is on screen pays it once every
|
|
388
|
+
* `snapshotMaxAgeS`.
|
|
389
|
+
*
|
|
390
|
+
* `ffmpeg` is already this addon's dependency (the keyframe grab shells out to
|
|
391
|
+
* it), so this adds no new one.
|
|
392
|
+
*/
|
|
393
|
+
var RESIZE_TIMEOUT_MS = 1e4;
|
|
394
|
+
/**
|
|
395
|
+
* Scale to `width`, preserving aspect ratio (`-2` keeps the height even, which
|
|
396
|
+
* mjpeg requires).
|
|
397
|
+
*
|
|
398
|
+
* Rejects on a non-zero exit or a timeout. It NEVER falls back to the original
|
|
399
|
+
* silently — a caller that quietly served the 4K frame when the resize failed
|
|
400
|
+
* would reproduce exactly the bug this whole change exists to fix, and nobody
|
|
401
|
+
* would see it. The caller decides, and logs.
|
|
402
|
+
*/
|
|
403
|
+
function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
const child = (0, node_child_process.spawn)("ffmpeg", [
|
|
406
|
+
"-loglevel",
|
|
407
|
+
"error",
|
|
408
|
+
"-f",
|
|
409
|
+
"image2pipe",
|
|
410
|
+
"-i",
|
|
411
|
+
"pipe:0",
|
|
412
|
+
"-vf",
|
|
413
|
+
`scale=${String(width)}:-2:flags=bilinear`,
|
|
414
|
+
"-frames:v",
|
|
415
|
+
"1",
|
|
416
|
+
"-q:v",
|
|
417
|
+
"5",
|
|
418
|
+
"-f",
|
|
419
|
+
"image2pipe",
|
|
420
|
+
"-vcodec",
|
|
421
|
+
"mjpeg",
|
|
422
|
+
"pipe:1"
|
|
423
|
+
], { stdio: [
|
|
424
|
+
"pipe",
|
|
425
|
+
"pipe",
|
|
426
|
+
"pipe"
|
|
427
|
+
] });
|
|
428
|
+
const out = [];
|
|
429
|
+
let err = "";
|
|
430
|
+
let settled = false;
|
|
431
|
+
const timer = setTimeout(() => {
|
|
432
|
+
if (settled) return;
|
|
433
|
+
settled = true;
|
|
434
|
+
child.kill("SIGKILL");
|
|
435
|
+
reject(/* @__PURE__ */ new Error(`snapshot resize timed out after ${String(timeoutMs)}ms`));
|
|
436
|
+
}, timeoutMs);
|
|
437
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
438
|
+
child.stderr.on("data", (chunk) => {
|
|
439
|
+
err += chunk.toString();
|
|
440
|
+
});
|
|
441
|
+
child.on("error", (e) => {
|
|
442
|
+
if (settled) return;
|
|
443
|
+
settled = true;
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
reject(e);
|
|
446
|
+
});
|
|
447
|
+
child.on("close", (code) => {
|
|
448
|
+
if (settled) return;
|
|
449
|
+
settled = true;
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
const buf = Buffer.concat(out);
|
|
452
|
+
if (code !== 0 || buf.length === 0) {
|
|
453
|
+
reject(/* @__PURE__ */ new Error(`snapshot resize failed (code ${String(code)}): ${err.trim() || "no output"}`));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
resolve(buf);
|
|
457
|
+
});
|
|
458
|
+
child.stdin.on("error", () => {});
|
|
459
|
+
child.stdin.end(bytes);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resized frames, keyed by (device, stream, width) AND validated against the
|
|
464
|
+
* source frame's timestamp.
|
|
465
|
+
*
|
|
466
|
+
* The timestamp is the whole correctness argument: a variant outlives nothing.
|
|
467
|
+
* When the underlying frame is recaptured its `capturedAt` moves, every variant
|
|
468
|
+
* derived from the old one stops matching, and the next request re-derives.
|
|
469
|
+
* Without that check a card would keep showing a thumbnail of a frame the
|
|
470
|
+
* full-size view had already replaced.
|
|
471
|
+
*/
|
|
472
|
+
var SnapshotVariantCache = class SnapshotVariantCache {
|
|
473
|
+
byKey = /* @__PURE__ */ new Map();
|
|
474
|
+
keysByDevice = /* @__PURE__ */ new Map();
|
|
475
|
+
static key(deviceId, streamId, width) {
|
|
476
|
+
return `${deviceId}:${streamId ?? "auto"}:${width}`;
|
|
477
|
+
}
|
|
478
|
+
/** The variant for this exact frame, or undefined when it is missing or was
|
|
479
|
+
* derived from an older capture. */
|
|
480
|
+
get(deviceId, streamId, width, sourceTs) {
|
|
481
|
+
const entry = this.byKey.get(SnapshotVariantCache.key(deviceId, streamId, width));
|
|
482
|
+
return entry !== void 0 && entry.sourceTs === sourceTs ? entry.bytes : void 0;
|
|
483
|
+
}
|
|
484
|
+
set(deviceId, streamId, width, sourceTs, bytes) {
|
|
485
|
+
const key = SnapshotVariantCache.key(deviceId, streamId, width);
|
|
486
|
+
this.byKey.set(key, {
|
|
487
|
+
bytes,
|
|
488
|
+
sourceTs
|
|
489
|
+
});
|
|
490
|
+
const keys = this.keysByDevice.get(deviceId) ?? /* @__PURE__ */ new Set();
|
|
491
|
+
keys.add(key);
|
|
492
|
+
this.keysByDevice.set(deviceId, keys);
|
|
493
|
+
}
|
|
494
|
+
deleteDevice(deviceId) {
|
|
495
|
+
for (const key of this.keysByDevice.get(deviceId) ?? []) this.byKey.delete(key);
|
|
496
|
+
this.keysByDevice.delete(deviceId);
|
|
497
|
+
}
|
|
498
|
+
clear() {
|
|
499
|
+
this.byKey.clear();
|
|
500
|
+
this.keysByDevice.clear();
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
//#endregion
|
|
282
504
|
//#region src/builtins/snapshot/snapshot.addon.ts
|
|
283
505
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
284
506
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
@@ -314,7 +536,10 @@ function effectiveMaxAgeS(prefs, isBattery) {
|
|
|
314
536
|
* publishes stream endpoints.
|
|
315
537
|
*/
|
|
316
538
|
var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
317
|
-
cache =
|
|
539
|
+
cache = new SnapshotCache();
|
|
540
|
+
/** Card-sized derivatives of the frames above, keyed by width and validated
|
|
541
|
+
* against the source frame's timestamp. */
|
|
542
|
+
variants = new SnapshotVariantCache();
|
|
318
543
|
/**
|
|
319
544
|
* De-dupes concurrent captures per `${deviceId}:${streamId}` and holds a
|
|
320
545
|
* settled SUCCESS for COALESCE_MS so a grid-mount burst (and a row of refresh
|
|
@@ -372,7 +597,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
372
597
|
* base64 path.
|
|
373
598
|
*/
|
|
374
599
|
async serveMediaDataPlane() {
|
|
375
|
-
const handler = createSnapshotMediaHandler({ getMedia: (deviceId, streamId, force) => this.resolveSnapshotMedia(deviceId, streamId, force) });
|
|
600
|
+
const handler = createSnapshotMediaHandler({ getMedia: (deviceId, streamId, force, width) => this.resolveSnapshotMedia(deviceId, streamId, force, width) });
|
|
376
601
|
try {
|
|
377
602
|
this.mediaDataPlane = await this.ctx.dataPlane?.serve({
|
|
378
603
|
prefix: "media",
|
|
@@ -393,29 +618,67 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
393
618
|
* the cache entry (matching the ETag `getSnapshotOverview` advertises) and the
|
|
394
619
|
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
395
620
|
*/
|
|
396
|
-
async resolveSnapshotMedia(deviceId, streamId, force) {
|
|
621
|
+
async resolveSnapshotMedia(deviceId, streamId, force, width) {
|
|
397
622
|
const image = await this.getSnapshot({
|
|
398
623
|
deviceId,
|
|
399
624
|
...streamId !== void 0 ? { streamId } : {},
|
|
400
625
|
force
|
|
401
626
|
});
|
|
402
627
|
if (!image) return null;
|
|
403
|
-
const capturedAt = this.cache.get(deviceId)?.ts ?? Date.now();
|
|
628
|
+
const capturedAt = this.cache.get(deviceId, streamId)?.ts ?? Date.now();
|
|
404
629
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
405
630
|
const isBattery = (await this.lookupDeviceMeta(deviceId))?.isBattery ?? false;
|
|
406
631
|
return {
|
|
407
|
-
bytes: Buffer.from(image.base64, "base64"),
|
|
632
|
+
bytes: width === void 0 ? Buffer.from(image.base64, "base64") : await this.thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs),
|
|
408
633
|
contentType: image.contentType,
|
|
409
634
|
capturedAt,
|
|
410
635
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery)
|
|
411
636
|
};
|
|
412
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* The frame at a card-sized width, derived once per (device, stream, width)
|
|
640
|
+
* per capture.
|
|
641
|
+
*
|
|
642
|
+
* A failed resize falls back to the FULL frame — a visible card beats a
|
|
643
|
+
* broken one — but never silently: the card would otherwise keep costing a
|
|
644
|
+
* megabyte with nothing anywhere saying why, which is the shape of the bug
|
|
645
|
+
* this whole change came from.
|
|
646
|
+
*/
|
|
647
|
+
async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs) {
|
|
648
|
+
const cached = this.variants.get(deviceId, streamId, width, capturedAt);
|
|
649
|
+
if (cached !== void 0) return cached;
|
|
650
|
+
const full = Buffer.from(image.base64, "base64");
|
|
651
|
+
try {
|
|
652
|
+
const resized = await resizeJpeg(full, width);
|
|
653
|
+
this.variants.set(deviceId, streamId, width, capturedAt, resized);
|
|
654
|
+
if (prefs.snapshotDebug) this.ctx.logger.info("snapshot: thumbnail derived", {
|
|
655
|
+
tags: { deviceId },
|
|
656
|
+
meta: {
|
|
657
|
+
width,
|
|
658
|
+
fullBytes: full.length,
|
|
659
|
+
thumbBytes: resized.length
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
return resized;
|
|
663
|
+
} catch (err) {
|
|
664
|
+
this.ctx.logger.warn("snapshot: resize failed — serving the full frame", {
|
|
665
|
+
tags: { deviceId },
|
|
666
|
+
meta: {
|
|
667
|
+
width,
|
|
668
|
+
bytes: full.length,
|
|
669
|
+
error: require_dist.errMsg(err)
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
return full;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
413
675
|
async onShutdown() {
|
|
414
676
|
if (this.mediaDataPlane) {
|
|
415
677
|
await this.mediaDataPlane.dispose().catch(() => void 0);
|
|
416
678
|
this.mediaDataPlane = null;
|
|
417
679
|
}
|
|
418
680
|
this.cache.clear();
|
|
681
|
+
this.variants.clear();
|
|
419
682
|
this.captureFlight.clear();
|
|
420
683
|
this.ownerCache = null;
|
|
421
684
|
}
|
|
@@ -459,7 +722,6 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
459
722
|
...deviceName ? { deviceName } : {}
|
|
460
723
|
});
|
|
461
724
|
const now = Date.now();
|
|
462
|
-
const hit = this.cache.get(deviceId);
|
|
463
725
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
464
726
|
const rawPref = prefs.snapshotStreamId;
|
|
465
727
|
const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
|
|
@@ -467,6 +729,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
467
729
|
tags: { deviceId },
|
|
468
730
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
469
731
|
});
|
|
732
|
+
const hit = this.cache.get(deviceId, effectiveStreamId);
|
|
470
733
|
const effectiveMaxAgeMs = effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3;
|
|
471
734
|
const decision = decideSnapshotServe({
|
|
472
735
|
now,
|
|
@@ -575,7 +838,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
575
838
|
if (native) {
|
|
576
839
|
const result = await this.nativePool.run(() => native.getSnapshot(input));
|
|
577
840
|
if (result) {
|
|
578
|
-
this.cache.set(deviceId, {
|
|
841
|
+
this.cache.set(deviceId, effectiveStreamId, {
|
|
579
842
|
data: result,
|
|
580
843
|
ts: now,
|
|
581
844
|
streamId: effectiveStreamId ?? null
|
|
@@ -609,7 +872,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
609
872
|
if (!(isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId))) try {
|
|
610
873
|
const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
|
|
611
874
|
if (fallback) {
|
|
612
|
-
this.cache.set(deviceId, {
|
|
875
|
+
this.cache.set(deviceId, effectiveStreamId, {
|
|
613
876
|
data: fallback,
|
|
614
877
|
ts: now,
|
|
615
878
|
streamId: effectiveStreamId ?? null
|
|
@@ -755,7 +1018,8 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
755
1018
|
};
|
|
756
1019
|
}
|
|
757
1020
|
async invalidateCache(input) {
|
|
758
|
-
this.cache.
|
|
1021
|
+
this.cache.deleteDevice(input.deviceId);
|
|
1022
|
+
this.variants.deleteDevice(input.deviceId);
|
|
759
1023
|
this.captureFlight.invalidatePrefix(`${input.deviceId}:`);
|
|
760
1024
|
}
|
|
761
1025
|
/**
|
|
@@ -829,7 +1093,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
829
1093
|
getSnapshotOverview(input) {
|
|
830
1094
|
const now = Date.now();
|
|
831
1095
|
const rows = input.deviceIds.map((deviceId) => {
|
|
832
|
-
const hit = this.cache.
|
|
1096
|
+
const hit = this.cache.latest(deviceId);
|
|
833
1097
|
if (!hit) return {
|
|
834
1098
|
deviceId,
|
|
835
1099
|
lastCapturedAt: null,
|
|
@@ -853,7 +1117,7 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
853
1117
|
* (cache miss) since the addon started.
|
|
854
1118
|
*/
|
|
855
1119
|
async getStatus(deviceId) {
|
|
856
|
-
const hit = this.cache.
|
|
1120
|
+
const hit = this.cache.latest(deviceId);
|
|
857
1121
|
if (!hit) return null;
|
|
858
1122
|
return {
|
|
859
1123
|
lastCapturedAt: hit.ts,
|
|
@@ -1001,7 +1265,8 @@ var SnapshotAddon = class SnapshotAddon extends require_dist.BaseAddon {
|
|
|
1001
1265
|
else delete next["snapshotMaxAgeS"];
|
|
1002
1266
|
}
|
|
1003
1267
|
await this.ctx.settings.writeDeviceStore(deviceId, next);
|
|
1004
|
-
this.cache.
|
|
1268
|
+
this.cache.deleteDevice(deviceId);
|
|
1269
|
+
this.variants.deleteDevice(deviceId);
|
|
1005
1270
|
this.captureFlight.invalidatePrefix(`${deviceId}:`);
|
|
1006
1271
|
return { success: true };
|
|
1007
1272
|
}
|