@camstack/system 1.2.93 → 1.2.95
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/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- 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 +218 -102
- package/dist/builtins/snapshot/index.mjs +218 -102
- package/dist/builtins/snapshot/snapshot-courtesy.d.ts +13 -1
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +7 -1
- package/dist/builtins/snapshot/snapshot-resize.d.ts +4 -4
- package/dist/builtins/snapshot/snapshot.addon.d.ts +37 -38
- 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 +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-BMus2E5R.js → dist-BQoU9nGS.js} +16 -1
- package/dist/{dist-F1XDQDE1.mjs → dist-hKJyHgFg.mjs} +11 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ct as DeviceFeature, Gt as EventCategory, M as deriveBatteryPresence, Rt as nodePin, T as batteryCapability, Tt as DeviceType, i as BatteryStatusSchema, mt as streamQualityLabel, ut as snapshotCapability, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-hKJyHgFg.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
|
|
@@ -249,7 +249,7 @@ function raceForResult(promise, timeoutMs) {
|
|
|
249
249
|
//#endregion
|
|
250
250
|
//#region src/builtins/snapshot/snapshot-courtesy.ts
|
|
251
251
|
/**
|
|
252
|
-
*
|
|
252
|
+
* State treatment for a camera that cannot produce a current frame.
|
|
253
253
|
*
|
|
254
254
|
* ── Why this exists ───────────────────────────────────────────────────────
|
|
255
255
|
* A disabled, offline or sleeping camera has no frame, and until 2026-08-11 the
|
|
@@ -260,9 +260,9 @@ function raceForResult(promise, timeoutMs) {
|
|
|
260
260
|
* who deliberately switched a camera off saw the same thing as a fault ([D62]:
|
|
261
261
|
* "an off switch is REPORTED off; disabled must never look like broken").
|
|
262
262
|
*
|
|
263
|
-
* So the service answers with
|
|
264
|
-
* camera's own name.
|
|
265
|
-
*
|
|
263
|
+
* So the service answers with an image that SAYS what is going on, carrying
|
|
264
|
+
* the camera's own name. When a real cached frame exists it remains visible
|
|
265
|
+
* under the treatment; otherwise the generated courtesy background is used.
|
|
266
266
|
*
|
|
267
267
|
* ── Why sharp and not ffmpeg ──────────────────────────────────────────────
|
|
268
268
|
* The first draft of this file shelled out to `ffmpeg` with a `drawtext`
|
|
@@ -294,6 +294,7 @@ function courtesyLabel(reason) {
|
|
|
294
294
|
case "offline": return "Offline";
|
|
295
295
|
case "sleeping": return "Sleeping";
|
|
296
296
|
case "unreachable": return "Unreachable";
|
|
297
|
+
case "waking": return "Waking";
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
/**
|
|
@@ -305,6 +306,7 @@ function courtesyBackground(reason) {
|
|
|
305
306
|
case "disabled": return "#2b2b31";
|
|
306
307
|
case "offline": return "#3a2b2b";
|
|
307
308
|
case "sleeping": return "#232b3a";
|
|
309
|
+
case "waking": return "#243247";
|
|
308
310
|
case "unreachable": return "#3a2b2b";
|
|
309
311
|
}
|
|
310
312
|
}
|
|
@@ -320,15 +322,27 @@ function escapeCourtesyText(value) {
|
|
|
320
322
|
* frame read the same, with a floor so a thumbnail stays legible.
|
|
321
323
|
*/
|
|
322
324
|
function buildCourtesySvg(spec) {
|
|
325
|
+
return buildStateSvg(spec, false);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The same state treatment as a courtesy frame, but translucent so the last
|
|
329
|
+
* real camera frame remains visible underneath it.
|
|
330
|
+
*/
|
|
331
|
+
function buildCourtesyOverlaySvg(spec) {
|
|
332
|
+
return buildStateSvg(spec, true);
|
|
333
|
+
}
|
|
334
|
+
function buildStateSvg(spec, overlay) {
|
|
323
335
|
const stateSize = Math.max(12, Math.round(spec.width / 12));
|
|
324
336
|
const nameSize = Math.max(9, Math.round(spec.width / 26));
|
|
325
337
|
const state = escapeCourtesyText(courtesyLabel(spec.reason));
|
|
326
338
|
const name = escapeCourtesyText(spec.deviceName);
|
|
327
339
|
const midY = spec.height / 2;
|
|
340
|
+
const washOpacity = overlay ? "0.62" : "1";
|
|
341
|
+
const textStroke = Math.max(1, Math.round(spec.width / 320));
|
|
328
342
|
return [
|
|
329
343
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${String(spec.width)}" height="${String(spec.height)}">`,
|
|
330
|
-
`<rect width="100%" height="100%" fill="${courtesyBackground(spec.reason)}"/>`,
|
|
331
|
-
`<g font-family="${COURTESY_FONT_STACK}" text-anchor="middle">`,
|
|
344
|
+
`<rect width="100%" height="100%" fill="${courtesyBackground(spec.reason)}" fill-opacity="${washOpacity}"/>`,
|
|
345
|
+
`<g font-family="${COURTESY_FONT_STACK}" text-anchor="middle" paint-order="stroke" stroke="#111118" stroke-width="${String(textStroke)}">`,
|
|
332
346
|
`<text x="50%" y="${String(Math.round(midY))}" font-size="${String(stateSize)}" fill="#e8e8ee">${state}</text>`,
|
|
333
347
|
`<text x="50%" y="${String(Math.round(midY + stateSize))}" font-size="${String(nameSize)}" fill="#9a9aa8">${name}</text>`,
|
|
334
348
|
`</g></svg>`
|
|
@@ -350,6 +364,24 @@ function renderCourtesyJpeg(spec) {
|
|
|
350
364
|
return bytes;
|
|
351
365
|
});
|
|
352
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Paint the state treatment over a real cached frame.
|
|
369
|
+
*
|
|
370
|
+
* The source dimensions are authoritative: overlays preserve the original
|
|
371
|
+
* frame rather than resizing it to the generated courtesy-frame default.
|
|
372
|
+
*/
|
|
373
|
+
async function renderCourtesyOverlayJpeg(background, spec) {
|
|
374
|
+
const metadata = await sharp(background).metadata();
|
|
375
|
+
if (!metadata.width || !metadata.height) throw new Error("cached snapshot has no renderable dimensions");
|
|
376
|
+
const overlay = Buffer.from(buildCourtesyOverlaySvg({
|
|
377
|
+
...spec,
|
|
378
|
+
width: metadata.width,
|
|
379
|
+
height: metadata.height
|
|
380
|
+
}));
|
|
381
|
+
const bytes = await sharp(background).composite([{ input: overlay }]).jpeg({ quality: 82 }).toBuffer();
|
|
382
|
+
if (bytes.length === 0) throw new Error("courtesy overlay produced no bytes");
|
|
383
|
+
return bytes;
|
|
384
|
+
}
|
|
353
385
|
//#endregion
|
|
354
386
|
//#region src/builtins/snapshot/snapshot-link-url.ts
|
|
355
387
|
/**
|
|
@@ -543,7 +575,8 @@ function parseSnapshotMediaRequest(url) {
|
|
|
543
575
|
* The response's entity tag.
|
|
544
576
|
*
|
|
545
577
|
* It has to identify the VARIANT, not just the frame: the same capture can now
|
|
546
|
-
* be served at several widths
|
|
578
|
+
* be served at several widths, from several streams, and with a camera-state
|
|
579
|
+
* treatment, and a plain
|
|
547
580
|
* `"<device>-<capturedAt>"` would let a client that fetched `?w=320` receive a
|
|
548
581
|
* 304 for `?w=960` and render the small image at full size. The base form is
|
|
549
582
|
* unchanged for a plain request, so the identity
|
|
@@ -554,6 +587,7 @@ function snapshotEtag(variant, capturedAt) {
|
|
|
554
587
|
const parts = [`${String(variant.deviceId)}-${String(capturedAt)}`];
|
|
555
588
|
if (variant.streamId !== void 0) parts.push(`s${variant.streamId}`);
|
|
556
589
|
if (variant.width !== void 0) parts.push(`w${String(variant.width)}`);
|
|
590
|
+
if (variant.variantKey !== void 0) parts.push(`v${variant.variantKey}`);
|
|
557
591
|
return `"${parts.join("-")}"`;
|
|
558
592
|
}
|
|
559
593
|
/**
|
|
@@ -584,7 +618,8 @@ function createSnapshotMediaHandler(deps) {
|
|
|
584
618
|
const peekedEtag = snapshotEtag({
|
|
585
619
|
deviceId: parsed.deviceId,
|
|
586
620
|
streamId: parsed.streamId,
|
|
587
|
-
width: parsed.width
|
|
621
|
+
width: parsed.width,
|
|
622
|
+
variantKey: peeked.variantKey
|
|
588
623
|
}, peeked.capturedAt);
|
|
589
624
|
if (inm === peekedEtag) {
|
|
590
625
|
res.writeHead(304, {
|
|
@@ -615,7 +650,8 @@ function createSnapshotMediaHandler(deps) {
|
|
|
615
650
|
const etag = snapshotEtag({
|
|
616
651
|
deviceId: parsed.deviceId,
|
|
617
652
|
streamId: parsed.streamId,
|
|
618
|
-
width: "servedWidth" in media ? media.servedWidth : parsed.width
|
|
653
|
+
width: "servedWidth" in media ? media.servedWidth : parsed.width,
|
|
654
|
+
variantKey: media.variantKey
|
|
619
655
|
}, media.capturedAt);
|
|
620
656
|
const cacheControl = `private, max-age=${Math.max(0, Math.floor(media.maxAgeS))}`;
|
|
621
657
|
if (inm === etag) {
|
|
@@ -718,8 +754,8 @@ function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
|
|
|
718
754
|
});
|
|
719
755
|
}
|
|
720
756
|
/**
|
|
721
|
-
* Resized frames, keyed by (device, stream, width) AND
|
|
722
|
-
* source frame's timestamp.
|
|
757
|
+
* Resized frames, keyed by (device, stream, width, state treatment) AND
|
|
758
|
+
* validated against the source frame's timestamp.
|
|
723
759
|
*
|
|
724
760
|
* The timestamp is the whole correctness argument: a variant outlives nothing.
|
|
725
761
|
* When the underlying frame is recaptured its `capturedAt` moves, every variant
|
|
@@ -730,17 +766,17 @@ function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
|
|
|
730
766
|
var SnapshotVariantCache = class SnapshotVariantCache {
|
|
731
767
|
byKey = /* @__PURE__ */ new Map();
|
|
732
768
|
keysByDevice = /* @__PURE__ */ new Map();
|
|
733
|
-
static key(deviceId, streamId, width) {
|
|
734
|
-
return `${deviceId}:${streamId ?? "auto"}:${width}`;
|
|
769
|
+
static key(deviceId, streamId, width, variantKey) {
|
|
770
|
+
return `${deviceId}:${streamId ?? "auto"}:${width}:${variantKey ?? "raw"}`;
|
|
735
771
|
}
|
|
736
772
|
/** The variant for this exact frame, or undefined when it is missing or was
|
|
737
773
|
* derived from an older capture. */
|
|
738
|
-
get(deviceId, streamId, width, sourceTs) {
|
|
739
|
-
const entry = this.byKey.get(SnapshotVariantCache.key(deviceId, streamId, width));
|
|
774
|
+
get(deviceId, streamId, width, sourceTs, variantKey) {
|
|
775
|
+
const entry = this.byKey.get(SnapshotVariantCache.key(deviceId, streamId, width, variantKey));
|
|
740
776
|
return entry !== void 0 && entry.sourceTs === sourceTs ? entry.bytes : void 0;
|
|
741
777
|
}
|
|
742
|
-
set(deviceId, streamId, width, sourceTs, bytes) {
|
|
743
|
-
const key = SnapshotVariantCache.key(deviceId, streamId, width);
|
|
778
|
+
set(deviceId, streamId, width, sourceTs, bytes, variantKey) {
|
|
779
|
+
const key = SnapshotVariantCache.key(deviceId, streamId, width, variantKey);
|
|
744
780
|
this.byKey.set(key, {
|
|
745
781
|
bytes,
|
|
746
782
|
sourceTs
|
|
@@ -781,6 +817,9 @@ var LINK_MINT_DEADLINE_MS = 2500;
|
|
|
781
817
|
var LINK_BATCH_WAIT_MS = 750;
|
|
782
818
|
/** Frames older than this are never represented as current by the Viewer. */
|
|
783
819
|
var LINK_CURRENT_MAX_AGE_MS = 15e3;
|
|
820
|
+
/** Wake-on-play's 8 s firmware wait + 20 s broker settle, with scheduling
|
|
821
|
+
* slack. A lost completion event must not leave a camera "Waking" forever. */
|
|
822
|
+
var SNAPSHOT_WAKING_TTL_MS = 35e3;
|
|
784
823
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
785
824
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
786
825
|
/** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
|
|
@@ -825,9 +864,14 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
825
864
|
* A courtesy frame is a pure function of those three, so it is rendered once
|
|
826
865
|
* and reused for as long as the process lives — a disabled camera must not
|
|
827
866
|
* cost an ffmpeg run per poll. Unbounded on purpose: the key space is
|
|
828
|
-
* (
|
|
867
|
+
* (state reasons × this node's own cameras), not user input.
|
|
829
868
|
*/
|
|
830
869
|
courtesyFrames = /* @__PURE__ */ new Map();
|
|
870
|
+
/** State overlays over real cached frames, grouped by device so invalidation
|
|
871
|
+
* can discard every generation in O(1). */
|
|
872
|
+
stateFrames = /* @__PURE__ */ new Map();
|
|
873
|
+
/** Devices inside the explicit battery wake-in-progress window. */
|
|
874
|
+
wakingDevices = /* @__PURE__ */ new Map();
|
|
831
875
|
/**
|
|
832
876
|
* De-dupes concurrent captures per `${deviceId}:${streamId}` and holds a
|
|
833
877
|
* settled SUCCESS for COALESCE_MS so a grid-mount burst (and a row of refresh
|
|
@@ -899,6 +943,12 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
899
943
|
const deviceId = event.data.deviceId;
|
|
900
944
|
if (typeof deviceId === "number") this.evictRemovedDevice(deviceId, "device-unregistered");
|
|
901
945
|
});
|
|
946
|
+
this.subscribe({ category: EventCategory.BatteryOnWakeStarted }, (event) => {
|
|
947
|
+
this.wakingDevices.set(event.data.deviceId, Date.now());
|
|
948
|
+
});
|
|
949
|
+
this.subscribe({ category: EventCategory.BatteryOnStatusChanged }, (event) => {
|
|
950
|
+
this.wakingDevices.delete(event.data.deviceId);
|
|
951
|
+
});
|
|
902
952
|
await this.serveMediaDataPlane();
|
|
903
953
|
await this.ensureLinkDataPlane();
|
|
904
954
|
return [{
|
|
@@ -1099,11 +1149,11 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1099
1149
|
const expMs = snapshotLinkExpiry(Date.now(), SNAPSHOT_LINK_TTL_MS);
|
|
1100
1150
|
const served = this.linkDataPlane !== null && secret !== null;
|
|
1101
1151
|
const prefix = this.linkRoutePrefix();
|
|
1102
|
-
const
|
|
1152
|
+
const stateByDevice = /* @__PURE__ */ new Map();
|
|
1103
1153
|
const refreshes = input.targets.map(async (target) => {
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
if (
|
|
1154
|
+
const state = await this.snapshotStateForLink(target.deviceId);
|
|
1155
|
+
stateByDevice.set(target.deviceId, state);
|
|
1156
|
+
if (state.reason === void 0) await this.awaitFreshEnough(target.deviceId);
|
|
1107
1157
|
});
|
|
1108
1158
|
await raceForResult(Promise.allSettled(refreshes), LINK_BATCH_WAIT_MS);
|
|
1109
1159
|
const rows = input.targets.map((target) => {
|
|
@@ -1126,7 +1176,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1126
1176
|
expiresAt: served ? expMs : null,
|
|
1127
1177
|
width: width ?? null,
|
|
1128
1178
|
neverCaptured: capturedAt === null,
|
|
1129
|
-
sleeping:
|
|
1179
|
+
sleeping: stateByDevice.get(deviceId)?.reason === "sleeping",
|
|
1180
|
+
stateReason: stateByDevice.get(deviceId)?.reason ?? null
|
|
1130
1181
|
};
|
|
1131
1182
|
});
|
|
1132
1183
|
this.ctx.logger.debug("snapshot: minted links", { meta: {
|
|
@@ -1160,15 +1211,18 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1160
1211
|
}
|
|
1161
1212
|
});
|
|
1162
1213
|
}
|
|
1163
|
-
/**
|
|
1164
|
-
*
|
|
1165
|
-
|
|
1166
|
-
async isSleepingBatteryDevice(deviceId) {
|
|
1214
|
+
/** Resolve the state advertised by a minted link before deciding whether a
|
|
1215
|
+
* background capture is allowed. */
|
|
1216
|
+
async snapshotStateForLink(deviceId) {
|
|
1167
1217
|
try {
|
|
1168
|
-
|
|
1169
|
-
return await this.
|
|
1170
|
-
} catch {
|
|
1171
|
-
|
|
1218
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1219
|
+
return await this.resolveSnapshotState(deviceId, meta);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
this.ctx.logger.debug("snapshot: link state resolution failed", {
|
|
1222
|
+
tags: { deviceId },
|
|
1223
|
+
meta: { error: errMsg(err) }
|
|
1224
|
+
});
|
|
1225
|
+
return { isBattery: false };
|
|
1172
1226
|
}
|
|
1173
1227
|
}
|
|
1174
1228
|
/**
|
|
@@ -1180,28 +1234,31 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1180
1234
|
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
1181
1235
|
*/
|
|
1182
1236
|
async resolveSnapshotMedia(deviceId, streamId, force, width) {
|
|
1237
|
+
const resolutionMeta = {};
|
|
1183
1238
|
const image = await this.getSnapshot({
|
|
1184
1239
|
deviceId,
|
|
1185
1240
|
...streamId !== void 0 ? { streamId } : {},
|
|
1186
1241
|
force
|
|
1187
|
-
});
|
|
1242
|
+
}, 0, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, resolutionMeta);
|
|
1188
1243
|
if (!image) return null;
|
|
1189
1244
|
const capturedAt = this.cache.get(deviceId, streamId)?.ts ?? Date.now();
|
|
1190
1245
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1191
|
-
const isBattery =
|
|
1246
|
+
const isBattery = resolutionMeta.isBattery ?? false;
|
|
1192
1247
|
if (width === void 0) return {
|
|
1193
1248
|
bytes: Buffer.from(image.base64, "base64"),
|
|
1194
1249
|
contentType: image.contentType,
|
|
1195
1250
|
capturedAt,
|
|
1196
1251
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery),
|
|
1252
|
+
variantKey: resolutionMeta.stateReason,
|
|
1197
1253
|
servedWidth: void 0
|
|
1198
1254
|
};
|
|
1199
|
-
const thumb = await this.thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs);
|
|
1255
|
+
const thumb = await this.thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs, resolutionMeta.stateReason);
|
|
1200
1256
|
return {
|
|
1201
1257
|
bytes: thumb.bytes,
|
|
1202
1258
|
contentType: image.contentType,
|
|
1203
1259
|
capturedAt,
|
|
1204
1260
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery),
|
|
1261
|
+
variantKey: resolutionMeta.stateReason,
|
|
1205
1262
|
servedWidth: thumb.width
|
|
1206
1263
|
};
|
|
1207
1264
|
}
|
|
@@ -1217,24 +1274,28 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1217
1274
|
async peekFreshFrame(deviceId, streamId) {
|
|
1218
1275
|
const hit = this.cache.get(deviceId, streamId);
|
|
1219
1276
|
if (!hit) return null;
|
|
1220
|
-
const
|
|
1277
|
+
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1278
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1279
|
+
const state = await this.resolveSnapshotState(deviceId, meta);
|
|
1280
|
+
const maxAgeS = effectiveMaxAgeS(prefs, state.isBattery);
|
|
1221
1281
|
if (Date.now() - hit.ts >= maxAgeS * 1e3) return null;
|
|
1222
1282
|
return {
|
|
1223
1283
|
capturedAt: hit.ts,
|
|
1224
|
-
maxAgeS
|
|
1284
|
+
maxAgeS,
|
|
1285
|
+
variantKey: state.reason
|
|
1225
1286
|
};
|
|
1226
1287
|
}
|
|
1227
1288
|
/**
|
|
1228
|
-
* The frame at a card-sized width, derived once per
|
|
1229
|
-
* per capture.
|
|
1289
|
+
* The frame at a card-sized width, derived once per
|
|
1290
|
+
* (device, stream, width, state treatment) per capture.
|
|
1230
1291
|
*
|
|
1231
1292
|
* A failed resize falls back to the FULL frame — a visible card beats a
|
|
1232
1293
|
* broken one — but never silently: the card would otherwise keep costing a
|
|
1233
1294
|
* megabyte with nothing anywhere saying why, which is the shape of the bug
|
|
1234
1295
|
* this whole change came from.
|
|
1235
1296
|
*/
|
|
1236
|
-
async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs) {
|
|
1237
|
-
const cached = this.variants.get(deviceId, streamId, width, capturedAt);
|
|
1297
|
+
async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs, variantKey) {
|
|
1298
|
+
const cached = this.variants.get(deviceId, streamId, width, capturedAt, variantKey);
|
|
1238
1299
|
if (cached !== void 0) return {
|
|
1239
1300
|
bytes: cached,
|
|
1240
1301
|
width
|
|
@@ -1242,7 +1303,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1242
1303
|
const full = Buffer.from(image.base64, "base64");
|
|
1243
1304
|
try {
|
|
1244
1305
|
const resized = await this.resize(full, width);
|
|
1245
|
-
this.variants.set(deviceId, streamId, width, capturedAt, resized);
|
|
1306
|
+
this.variants.set(deviceId, streamId, width, capturedAt, resized, variantKey);
|
|
1246
1307
|
if (prefs.snapshotDebug) this.ctx.logger.info("snapshot: thumbnail derived", {
|
|
1247
1308
|
tags: { deviceId },
|
|
1248
1309
|
meta: {
|
|
@@ -1281,6 +1342,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1281
1342
|
}
|
|
1282
1343
|
this.cache.clear();
|
|
1283
1344
|
this.variants.clear();
|
|
1345
|
+
this.stateFrames.clear();
|
|
1346
|
+
this.wakingDevices.clear();
|
|
1284
1347
|
this.captureFlight.clear();
|
|
1285
1348
|
this.ownerCache = null;
|
|
1286
1349
|
}
|
|
@@ -1313,18 +1376,16 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1313
1376
|
})]
|
|
1314
1377
|
}] });
|
|
1315
1378
|
}
|
|
1316
|
-
async getSnapshot(input, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY, maximumCaptureWaitMs = Number.POSITIVE_INFINITY) {
|
|
1379
|
+
async getSnapshot(input, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY, maximumCaptureWaitMs = Number.POSITIVE_INFINITY, resolutionMeta) {
|
|
1317
1380
|
const { deviceId } = input;
|
|
1318
1381
|
const force = input.force === true;
|
|
1319
1382
|
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1320
1383
|
const deviceName = meta?.name;
|
|
1321
|
-
const isBatteryDevice = meta?.isBattery ?? false;
|
|
1322
1384
|
const log = this.ctx.logger.withTags({
|
|
1323
1385
|
deviceId,
|
|
1324
1386
|
...deviceName ? { deviceName } : {}
|
|
1325
1387
|
});
|
|
1326
1388
|
const now = Date.now();
|
|
1327
|
-
if (meta?.disabled === true) return await this.courtesyImage(deviceId, "disabled", deviceName);
|
|
1328
1389
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1329
1390
|
const rawPref = prefs.snapshotStreamId;
|
|
1330
1391
|
const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
|
|
@@ -1333,6 +1394,24 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1333
1394
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
1334
1395
|
});
|
|
1335
1396
|
const hit = this.cache.get(deviceId, effectiveStreamId);
|
|
1397
|
+
const state = await this.resolveSnapshotState(deviceId, meta);
|
|
1398
|
+
const isBatteryDevice = state.isBattery;
|
|
1399
|
+
if (resolutionMeta) {
|
|
1400
|
+
resolutionMeta.isBattery = isBatteryDevice;
|
|
1401
|
+
delete resolutionMeta.stateReason;
|
|
1402
|
+
}
|
|
1403
|
+
if (state.reason !== void 0 && (!force || state.reason === "disabled" || state.reason === "unreachable" || state.reason === "waking")) {
|
|
1404
|
+
if (resolutionMeta) resolutionMeta.stateReason = state.reason;
|
|
1405
|
+
log.debug("snapshot: camera state overrides raw cache", {
|
|
1406
|
+
tags: { deviceId },
|
|
1407
|
+
meta: {
|
|
1408
|
+
reason: state.reason,
|
|
1409
|
+
hasCache: hit !== void 0,
|
|
1410
|
+
ageMs: hit ? now - hit.ts : null
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
return await this.stateImage(deviceId, state.reason, deviceName, hit);
|
|
1414
|
+
}
|
|
1336
1415
|
const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3));
|
|
1337
1416
|
const decision = decideSnapshotServe({
|
|
1338
1417
|
now,
|
|
@@ -1351,19 +1430,6 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1351
1430
|
});
|
|
1352
1431
|
return hit ? hit.data : null;
|
|
1353
1432
|
}
|
|
1354
|
-
const presence = isBatteryDevice ? await this.readBatteryPresence(deviceId) : "awake";
|
|
1355
|
-
if (!force && presence !== "awake") {
|
|
1356
|
-
log.debug("snapshot: battery cam not awake and no force — serving cache, not capturing", {
|
|
1357
|
-
tags: { deviceId },
|
|
1358
|
-
meta: {
|
|
1359
|
-
presence,
|
|
1360
|
-
hasCache: hit !== void 0,
|
|
1361
|
-
ageMs: hit ? now - hit.ts : null
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
if (hit) return hit.data;
|
|
1365
|
-
return await this.courtesyImage(deviceId, presence, deviceName);
|
|
1366
|
-
}
|
|
1367
1433
|
const flightKey = `${deviceId}:${effectiveStreamId ?? "auto"}`;
|
|
1368
1434
|
const flight = this.captureFlight.run(flightKey, () => this.captureFresh({
|
|
1369
1435
|
deviceId,
|
|
@@ -1482,6 +1548,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1482
1548
|
if (native) {
|
|
1483
1549
|
const result = await this.nativePool.run(() => native.getSnapshot(input));
|
|
1484
1550
|
if (result) {
|
|
1551
|
+
this.stateFrames.delete(deviceId);
|
|
1485
1552
|
this.cache.set(deviceId, effectiveStreamId, {
|
|
1486
1553
|
data: result,
|
|
1487
1554
|
ts: now,
|
|
@@ -1516,6 +1583,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1516
1583
|
if (!(isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId))) try {
|
|
1517
1584
|
const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
|
|
1518
1585
|
if (fallback) {
|
|
1586
|
+
this.stateFrames.delete(deviceId);
|
|
1519
1587
|
this.cache.set(deviceId, effectiveStreamId, {
|
|
1520
1588
|
data: fallback,
|
|
1521
1589
|
ts: now,
|
|
@@ -1669,6 +1737,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1669
1737
|
dropDeviceCaches(deviceId) {
|
|
1670
1738
|
this.cache.deleteDevice(deviceId);
|
|
1671
1739
|
this.variants.deleteDevice(deviceId);
|
|
1740
|
+
this.stateFrames.delete(deviceId);
|
|
1741
|
+
this.wakingDevices.delete(deviceId);
|
|
1672
1742
|
this.captureFlight.invalidatePrefix(`${deviceId}:`);
|
|
1673
1743
|
}
|
|
1674
1744
|
/**
|
|
@@ -1706,55 +1776,64 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1706
1776
|
}
|
|
1707
1777
|
}
|
|
1708
1778
|
/**
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
* `fetchDevice(id).state.battery` is a `SliceHandle` over the hub's
|
|
1712
|
-
* runtime-state mirror: a local read, no RPC, no failure branch to get
|
|
1713
|
-
* wrong. It is the same mirror that feeds the battery badge elsewhere
|
|
1714
|
-
* in the UI, so if the operator can see "sleeping" on screen, this
|
|
1715
|
-
* sees it too. (Both spec harnesses already model exactly this wiring
|
|
1716
|
-
* — like the orphaned doc comment above, it outlived the gate it was
|
|
1717
|
-
* built for.)
|
|
1779
|
+
* Resolve the state that must be visible on a snapshot.
|
|
1718
1780
|
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
* battery
|
|
1722
|
-
* WHETHER IT IS ASLEEP, never whether it has a battery.
|
|
1781
|
+
* Uses the same two canonical sources as wake-on-play:
|
|
1782
|
+
* 1. `deviceManager.getDevice` for the registry's BatteryOperated feature;
|
|
1783
|
+
* 2. `deviceState.getCapSlice('battery')` for the durable runtime mirror.
|
|
1723
1784
|
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
* The device's three-valued battery presence (D173) — `awake`, `sleeping`,
|
|
1732
|
-
* or `unreachable`. Same mirror read as the sleep gate, one derivation
|
|
1733
|
-
* (`deriveBatteryPresence`) so this layer cannot disagree with the badge or
|
|
1734
|
-
* the stream error about what a camera is doing.
|
|
1785
|
+
* A valid battery slice is itself positive evidence that the device is
|
|
1786
|
+
* battery-operated. That closes the field failure where "Baby monitor" had a
|
|
1787
|
+
* sleeping battery slice but a stale persisted feature projection, so the
|
|
1788
|
+
* snapshot wrapper classified it as mains and a Viewer thumbnail logged in
|
|
1789
|
+
* to the sleeping camera. The reverse is deliberately not guessed: no
|
|
1790
|
+
* feature + no valid slice remains a mains/unknown camera and follows the
|
|
1791
|
+
* ordinary capture path.
|
|
1735
1792
|
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1793
|
+
* Once battery-operated is established, a missing slice fails safe toward
|
|
1794
|
+
* `sleeping` through the shared `deriveBatteryPresence` function. This avoids
|
|
1795
|
+
* a background wake without inventing an `unreachable` fault.
|
|
1739
1796
|
*/
|
|
1740
|
-
async
|
|
1797
|
+
async resolveSnapshotState(deviceId, meta) {
|
|
1798
|
+
if (meta?.disabled === true) return {
|
|
1799
|
+
isBattery: meta.isBattery,
|
|
1800
|
+
reason: "disabled"
|
|
1801
|
+
};
|
|
1802
|
+
const waking = this.isDeviceWaking(deviceId);
|
|
1803
|
+
let batteryStatus = null;
|
|
1741
1804
|
try {
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const parsed = BatteryStatusSchema.safeParse(raw);
|
|
1746
|
-
if (!parsed.success) return "awake";
|
|
1747
|
-
return deriveBatteryPresence({
|
|
1748
|
-
status: parsed.data,
|
|
1749
|
-
nowMs: Date.now()
|
|
1805
|
+
const raw = await this.ctx.api.deviceState.getCapSlice.query({
|
|
1806
|
+
deviceId,
|
|
1807
|
+
capName: batteryCapability.name
|
|
1750
1808
|
});
|
|
1809
|
+
const parsed = BatteryStatusSchema.safeParse(raw);
|
|
1810
|
+
if (parsed.success) batteryStatus = parsed.data;
|
|
1751
1811
|
} catch (err) {
|
|
1752
|
-
this.ctx.logger.
|
|
1812
|
+
this.ctx.logger.debug("snapshot: battery runtime-state read failed", {
|
|
1753
1813
|
tags: { deviceId },
|
|
1754
1814
|
meta: { error: errMsg(err) }
|
|
1755
1815
|
});
|
|
1756
|
-
return "awake";
|
|
1757
1816
|
}
|
|
1817
|
+
if (!(meta?.isBattery === true || batteryStatus !== null || waking)) return { isBattery: false };
|
|
1818
|
+
if (waking) return {
|
|
1819
|
+
isBattery: true,
|
|
1820
|
+
reason: "waking"
|
|
1821
|
+
};
|
|
1822
|
+
const presence = deriveBatteryPresence({
|
|
1823
|
+
status: batteryStatus,
|
|
1824
|
+
nowMs: Date.now()
|
|
1825
|
+
});
|
|
1826
|
+
return presence === "awake" ? { isBattery: true } : {
|
|
1827
|
+
isBattery: true,
|
|
1828
|
+
reason: presence
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
isDeviceWaking(deviceId) {
|
|
1832
|
+
const startedAt = this.wakingDevices.get(deviceId);
|
|
1833
|
+
if (startedAt === void 0) return false;
|
|
1834
|
+
if (Date.now() - startedAt <= SNAPSHOT_WAKING_TTL_MS) return true;
|
|
1835
|
+
this.wakingDevices.delete(deviceId);
|
|
1836
|
+
return false;
|
|
1758
1837
|
}
|
|
1759
1838
|
/**
|
|
1760
1839
|
* True when at least one of the device's brokers is actively
|
|
@@ -1930,12 +2009,48 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1930
2009
|
};
|
|
1931
2010
|
}
|
|
1932
2011
|
/**
|
|
2012
|
+
* Render a state-labelled answer. A cached real frame is the background, not
|
|
2013
|
+
* the answer by itself; if it cannot be decoded, fall back to the generated
|
|
2014
|
+
* courtesy background rather than leaking an unlabelled stale frame.
|
|
2015
|
+
*/
|
|
2016
|
+
async stateImage(deviceId, reason, deviceName, hit) {
|
|
2017
|
+
const resolvedName = deviceName ?? `#${String(deviceId)}`;
|
|
2018
|
+
if (!hit) return await this.courtesyImage(deviceId, reason, resolvedName);
|
|
2019
|
+
const key = `${String(hit.ts)}:${hit.streamId ?? "auto"}:${reason}:${resolvedName}`;
|
|
2020
|
+
const deviceFrames = this.stateFrames.get(deviceId) ?? /* @__PURE__ */ new Map();
|
|
2021
|
+
this.stateFrames.set(deviceId, deviceFrames);
|
|
2022
|
+
const cached = deviceFrames.get(key);
|
|
2023
|
+
if (cached !== void 0) return {
|
|
2024
|
+
base64: cached.toString("base64"),
|
|
2025
|
+
contentType: "image/jpeg"
|
|
2026
|
+
};
|
|
2027
|
+
try {
|
|
2028
|
+
const bytes = await renderCourtesyOverlayJpeg(Buffer.from(hit.data.base64, "base64"), {
|
|
2029
|
+
deviceName: resolvedName,
|
|
2030
|
+
reason
|
|
2031
|
+
});
|
|
2032
|
+
deviceFrames.set(key, bytes);
|
|
2033
|
+
return {
|
|
2034
|
+
base64: bytes.toString("base64"),
|
|
2035
|
+
contentType: "image/jpeg"
|
|
2036
|
+
};
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
this.ctx.logger.warn("snapshot: state overlay failed — using generated courtesy background", {
|
|
2039
|
+
tags: { deviceId },
|
|
2040
|
+
meta: {
|
|
2041
|
+
reason,
|
|
2042
|
+
error: errMsg(err)
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
return await this.courtesyImage(deviceId, reason, resolvedName);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
1933
2049
|
* Single-trip device lookup against device-manager. Returns the
|
|
1934
|
-
* fields the wrapper
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
* we're trying to be resilient to).
|
|
2050
|
+
* stable registry fields the wrapper consults. Battery classification is
|
|
2051
|
+
* completed by `resolveSnapshotState` from this feature flag PLUS the
|
|
2052
|
+
* canonical runtime-state mirror, so a stale persisted feature projection
|
|
2053
|
+
* cannot authorize a snapshot dial against a known sleeping battery camera.
|
|
1939
2054
|
*
|
|
1940
2055
|
* Logged at debug + null return on failure: every call site already
|
|
1941
2056
|
* has a sensible fallback path (cache hit, conservative default, …),
|
|
@@ -1967,7 +2082,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1967
2082
|
}
|
|
1968
2083
|
/** Settings-UI helper — battery flag drives the default max-age in the field description. */
|
|
1969
2084
|
async isDeviceBattery(deviceId) {
|
|
1970
|
-
|
|
2085
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
2086
|
+
return (await this.resolveSnapshotState(deviceId, meta)).isBattery;
|
|
1971
2087
|
}
|
|
1972
2088
|
/**
|
|
1973
2089
|
* Feeds the settings-UI stream picker. Pinned to the ingest owner via
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* first is how a dead camera stayed green for 30 hours. The two are separated
|
|
10
10
|
* by `deriveBatteryPresence`, never by a caller's own guess.
|
|
11
11
|
*/
|
|
12
|
-
export type CourtesyReason = 'disabled' | 'offline' | 'sleeping' | 'unreachable';
|
|
12
|
+
export type CourtesyReason = 'disabled' | 'offline' | 'sleeping' | 'unreachable' | 'waking';
|
|
13
13
|
/** Default tile size when the caller asks for no particular width. */
|
|
14
14
|
export declare const COURTESY_DEFAULT_WIDTH = 640;
|
|
15
15
|
/** The word the frame carries. Deliberately the operator's vocabulary. */
|
|
@@ -35,6 +35,11 @@ export interface CourtesyFrameSpec {
|
|
|
35
35
|
* frame read the same, with a floor so a thumbnail stays legible.
|
|
36
36
|
*/
|
|
37
37
|
export declare function buildCourtesySvg(spec: CourtesyFrameSpec): string;
|
|
38
|
+
/**
|
|
39
|
+
* The same state treatment as a courtesy frame, but translucent so the last
|
|
40
|
+
* real camera frame remains visible underneath it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildCourtesyOverlaySvg(spec: CourtesyFrameSpec): string;
|
|
38
43
|
/** Cache key — a courtesy frame is a pure function of these four. */
|
|
39
44
|
export declare function courtesyCacheKey(spec: CourtesyFrameSpec): string;
|
|
40
45
|
/**
|
|
@@ -44,3 +49,10 @@ export declare function courtesyCacheKey(spec: CourtesyFrameSpec): string;
|
|
|
44
49
|
* does, so a broken courtesy path is never mistaken for a broken camera.
|
|
45
50
|
*/
|
|
46
51
|
export declare function renderCourtesyJpeg(spec: CourtesyFrameSpec): Promise<Buffer>;
|
|
52
|
+
/**
|
|
53
|
+
* Paint the state treatment over a real cached frame.
|
|
54
|
+
*
|
|
55
|
+
* The source dimensions are authoritative: overlays preserve the original
|
|
56
|
+
* frame rather than resizing it to the generated courtesy-frame default.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderCourtesyOverlayJpeg(background: Buffer, spec: Pick<CourtesyFrameSpec, 'deviceName' | 'reason'>): Promise<Buffer>;
|
|
@@ -9,6 +9,8 @@ export interface SnapshotMedia {
|
|
|
9
9
|
readonly capturedAt: number;
|
|
10
10
|
/** Effective per-device max cache age (seconds) → `Cache-Control: max-age`. */
|
|
11
11
|
readonly maxAgeS: number;
|
|
12
|
+
/** State treatment painted over the capture (sleeping, waking, …). */
|
|
13
|
+
readonly variantKey?: string | undefined;
|
|
12
14
|
/**
|
|
13
15
|
* The width the returned BYTES are actually at, or undefined when they are the
|
|
14
16
|
* frame as captured.
|
|
@@ -27,6 +29,8 @@ export interface SnapshotPeek {
|
|
|
27
29
|
readonly capturedAt: number;
|
|
28
30
|
/** Effective per-device max cache age (seconds). */
|
|
29
31
|
readonly maxAgeS: number;
|
|
32
|
+
/** State treatment that would be painted over this capture. */
|
|
33
|
+
readonly variantKey?: string | undefined;
|
|
30
34
|
}
|
|
31
35
|
export interface SnapshotMediaHandlerDeps {
|
|
32
36
|
/**
|
|
@@ -91,7 +95,8 @@ export declare function parseSnapshotMediaRequest(url: string): SnapshotMediaReq
|
|
|
91
95
|
* The response's entity tag.
|
|
92
96
|
*
|
|
93
97
|
* It has to identify the VARIANT, not just the frame: the same capture can now
|
|
94
|
-
* be served at several widths
|
|
98
|
+
* be served at several widths, from several streams, and with a camera-state
|
|
99
|
+
* treatment, and a plain
|
|
95
100
|
* `"<device>-<capturedAt>"` would let a client that fetched `?w=320` receive a
|
|
96
101
|
* 304 for `?w=960` and render the small image at full size. The base form is
|
|
97
102
|
* unchanged for a plain request, so the identity
|
|
@@ -106,6 +111,7 @@ export interface SnapshotVariantIdentity {
|
|
|
106
111
|
readonly deviceId: number;
|
|
107
112
|
readonly streamId: string | undefined;
|
|
108
113
|
readonly width: number | undefined;
|
|
114
|
+
readonly variantKey?: string | undefined;
|
|
109
115
|
}
|
|
110
116
|
/**
|
|
111
117
|
* Create a data-plane handler that serves per-device snapshots as JPEG images.
|