@camstack/system 1.2.93 → 1.2.94
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 +205 -94
- package/dist/builtins/snapshot/index.mjs +205 -94
- 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 +35 -37
- 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-BspfS8Q7.js} +6 -0
- package/dist/{dist-F1XDQDE1.mjs → dist-COd223t-.mjs} +1 -1
- 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-COd223t-.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 [{
|
|
@@ -1160,13 +1210,12 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1160
1210
|
}
|
|
1161
1211
|
});
|
|
1162
1212
|
}
|
|
1163
|
-
/** True only for a
|
|
1164
|
-
* question so the link path cannot accidentally treat "battery" as "asleep"
|
|
1165
|
-
* and stop refreshing a camera that is awake and streaming. */
|
|
1213
|
+
/** True only for a battery device that must not be snapshot-refreshed. */
|
|
1166
1214
|
async isSleepingBatteryDevice(deviceId) {
|
|
1167
1215
|
try {
|
|
1168
|
-
|
|
1169
|
-
|
|
1216
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1217
|
+
const state = await this.resolveSnapshotState(deviceId, meta);
|
|
1218
|
+
return state.reason === "sleeping" || state.reason === "unreachable" || state.reason === "waking";
|
|
1170
1219
|
} catch {
|
|
1171
1220
|
return false;
|
|
1172
1221
|
}
|
|
@@ -1180,28 +1229,31 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1180
1229
|
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
1181
1230
|
*/
|
|
1182
1231
|
async resolveSnapshotMedia(deviceId, streamId, force, width) {
|
|
1232
|
+
const resolutionMeta = {};
|
|
1183
1233
|
const image = await this.getSnapshot({
|
|
1184
1234
|
deviceId,
|
|
1185
1235
|
...streamId !== void 0 ? { streamId } : {},
|
|
1186
1236
|
force
|
|
1187
|
-
});
|
|
1237
|
+
}, 0, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, resolutionMeta);
|
|
1188
1238
|
if (!image) return null;
|
|
1189
1239
|
const capturedAt = this.cache.get(deviceId, streamId)?.ts ?? Date.now();
|
|
1190
1240
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1191
|
-
const isBattery =
|
|
1241
|
+
const isBattery = resolutionMeta.isBattery ?? false;
|
|
1192
1242
|
if (width === void 0) return {
|
|
1193
1243
|
bytes: Buffer.from(image.base64, "base64"),
|
|
1194
1244
|
contentType: image.contentType,
|
|
1195
1245
|
capturedAt,
|
|
1196
1246
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery),
|
|
1247
|
+
variantKey: resolutionMeta.stateReason,
|
|
1197
1248
|
servedWidth: void 0
|
|
1198
1249
|
};
|
|
1199
|
-
const thumb = await this.thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs);
|
|
1250
|
+
const thumb = await this.thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs, resolutionMeta.stateReason);
|
|
1200
1251
|
return {
|
|
1201
1252
|
bytes: thumb.bytes,
|
|
1202
1253
|
contentType: image.contentType,
|
|
1203
1254
|
capturedAt,
|
|
1204
1255
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery),
|
|
1256
|
+
variantKey: resolutionMeta.stateReason,
|
|
1205
1257
|
servedWidth: thumb.width
|
|
1206
1258
|
};
|
|
1207
1259
|
}
|
|
@@ -1217,24 +1269,28 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1217
1269
|
async peekFreshFrame(deviceId, streamId) {
|
|
1218
1270
|
const hit = this.cache.get(deviceId, streamId);
|
|
1219
1271
|
if (!hit) return null;
|
|
1220
|
-
const
|
|
1272
|
+
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1273
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1274
|
+
const state = await this.resolveSnapshotState(deviceId, meta);
|
|
1275
|
+
const maxAgeS = effectiveMaxAgeS(prefs, state.isBattery);
|
|
1221
1276
|
if (Date.now() - hit.ts >= maxAgeS * 1e3) return null;
|
|
1222
1277
|
return {
|
|
1223
1278
|
capturedAt: hit.ts,
|
|
1224
|
-
maxAgeS
|
|
1279
|
+
maxAgeS,
|
|
1280
|
+
variantKey: state.reason
|
|
1225
1281
|
};
|
|
1226
1282
|
}
|
|
1227
1283
|
/**
|
|
1228
|
-
* The frame at a card-sized width, derived once per
|
|
1229
|
-
* per capture.
|
|
1284
|
+
* The frame at a card-sized width, derived once per
|
|
1285
|
+
* (device, stream, width, state treatment) per capture.
|
|
1230
1286
|
*
|
|
1231
1287
|
* A failed resize falls back to the FULL frame — a visible card beats a
|
|
1232
1288
|
* broken one — but never silently: the card would otherwise keep costing a
|
|
1233
1289
|
* megabyte with nothing anywhere saying why, which is the shape of the bug
|
|
1234
1290
|
* this whole change came from.
|
|
1235
1291
|
*/
|
|
1236
|
-
async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs) {
|
|
1237
|
-
const cached = this.variants.get(deviceId, streamId, width, capturedAt);
|
|
1292
|
+
async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs, variantKey) {
|
|
1293
|
+
const cached = this.variants.get(deviceId, streamId, width, capturedAt, variantKey);
|
|
1238
1294
|
if (cached !== void 0) return {
|
|
1239
1295
|
bytes: cached,
|
|
1240
1296
|
width
|
|
@@ -1242,7 +1298,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1242
1298
|
const full = Buffer.from(image.base64, "base64");
|
|
1243
1299
|
try {
|
|
1244
1300
|
const resized = await this.resize(full, width);
|
|
1245
|
-
this.variants.set(deviceId, streamId, width, capturedAt, resized);
|
|
1301
|
+
this.variants.set(deviceId, streamId, width, capturedAt, resized, variantKey);
|
|
1246
1302
|
if (prefs.snapshotDebug) this.ctx.logger.info("snapshot: thumbnail derived", {
|
|
1247
1303
|
tags: { deviceId },
|
|
1248
1304
|
meta: {
|
|
@@ -1281,6 +1337,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1281
1337
|
}
|
|
1282
1338
|
this.cache.clear();
|
|
1283
1339
|
this.variants.clear();
|
|
1340
|
+
this.stateFrames.clear();
|
|
1341
|
+
this.wakingDevices.clear();
|
|
1284
1342
|
this.captureFlight.clear();
|
|
1285
1343
|
this.ownerCache = null;
|
|
1286
1344
|
}
|
|
@@ -1313,18 +1371,16 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1313
1371
|
})]
|
|
1314
1372
|
}] });
|
|
1315
1373
|
}
|
|
1316
|
-
async getSnapshot(input, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY, maximumCaptureWaitMs = Number.POSITIVE_INFINITY) {
|
|
1374
|
+
async getSnapshot(input, minimumCaptureWaitMs = 0, maximumCacheAgeMs = Number.POSITIVE_INFINITY, maximumCaptureWaitMs = Number.POSITIVE_INFINITY, resolutionMeta) {
|
|
1317
1375
|
const { deviceId } = input;
|
|
1318
1376
|
const force = input.force === true;
|
|
1319
1377
|
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1320
1378
|
const deviceName = meta?.name;
|
|
1321
|
-
const isBatteryDevice = meta?.isBattery ?? false;
|
|
1322
1379
|
const log = this.ctx.logger.withTags({
|
|
1323
1380
|
deviceId,
|
|
1324
1381
|
...deviceName ? { deviceName } : {}
|
|
1325
1382
|
});
|
|
1326
1383
|
const now = Date.now();
|
|
1327
|
-
if (meta?.disabled === true) return await this.courtesyImage(deviceId, "disabled", deviceName);
|
|
1328
1384
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1329
1385
|
const rawPref = prefs.snapshotStreamId;
|
|
1330
1386
|
const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
|
|
@@ -1333,6 +1389,24 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1333
1389
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
1334
1390
|
});
|
|
1335
1391
|
const hit = this.cache.get(deviceId, effectiveStreamId);
|
|
1392
|
+
const state = await this.resolveSnapshotState(deviceId, meta);
|
|
1393
|
+
const isBatteryDevice = state.isBattery;
|
|
1394
|
+
if (resolutionMeta) {
|
|
1395
|
+
resolutionMeta.isBattery = isBatteryDevice;
|
|
1396
|
+
delete resolutionMeta.stateReason;
|
|
1397
|
+
}
|
|
1398
|
+
if (state.reason !== void 0 && (!force || state.reason === "disabled" || state.reason === "unreachable" || state.reason === "waking")) {
|
|
1399
|
+
if (resolutionMeta) resolutionMeta.stateReason = state.reason;
|
|
1400
|
+
log.debug("snapshot: camera state overrides raw cache", {
|
|
1401
|
+
tags: { deviceId },
|
|
1402
|
+
meta: {
|
|
1403
|
+
reason: state.reason,
|
|
1404
|
+
hasCache: hit !== void 0,
|
|
1405
|
+
ageMs: hit ? now - hit.ts : null
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
return await this.stateImage(deviceId, state.reason, deviceName, hit);
|
|
1409
|
+
}
|
|
1336
1410
|
const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3));
|
|
1337
1411
|
const decision = decideSnapshotServe({
|
|
1338
1412
|
now,
|
|
@@ -1351,19 +1425,6 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1351
1425
|
});
|
|
1352
1426
|
return hit ? hit.data : null;
|
|
1353
1427
|
}
|
|
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
1428
|
const flightKey = `${deviceId}:${effectiveStreamId ?? "auto"}`;
|
|
1368
1429
|
const flight = this.captureFlight.run(flightKey, () => this.captureFresh({
|
|
1369
1430
|
deviceId,
|
|
@@ -1482,6 +1543,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1482
1543
|
if (native) {
|
|
1483
1544
|
const result = await this.nativePool.run(() => native.getSnapshot(input));
|
|
1484
1545
|
if (result) {
|
|
1546
|
+
this.stateFrames.delete(deviceId);
|
|
1485
1547
|
this.cache.set(deviceId, effectiveStreamId, {
|
|
1486
1548
|
data: result,
|
|
1487
1549
|
ts: now,
|
|
@@ -1516,6 +1578,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1516
1578
|
if (!(isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId))) try {
|
|
1517
1579
|
const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
|
|
1518
1580
|
if (fallback) {
|
|
1581
|
+
this.stateFrames.delete(deviceId);
|
|
1519
1582
|
this.cache.set(deviceId, effectiveStreamId, {
|
|
1520
1583
|
data: fallback,
|
|
1521
1584
|
ts: now,
|
|
@@ -1669,6 +1732,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1669
1732
|
dropDeviceCaches(deviceId) {
|
|
1670
1733
|
this.cache.deleteDevice(deviceId);
|
|
1671
1734
|
this.variants.deleteDevice(deviceId);
|
|
1735
|
+
this.stateFrames.delete(deviceId);
|
|
1736
|
+
this.wakingDevices.delete(deviceId);
|
|
1672
1737
|
this.captureFlight.invalidatePrefix(`${deviceId}:`);
|
|
1673
1738
|
}
|
|
1674
1739
|
/**
|
|
@@ -1706,55 +1771,64 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1706
1771
|
}
|
|
1707
1772
|
}
|
|
1708
1773
|
/**
|
|
1709
|
-
*
|
|
1774
|
+
* Resolve the state that must be visible on a snapshot.
|
|
1710
1775
|
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
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.)
|
|
1776
|
+
* Uses the same two canonical sources as wake-on-play:
|
|
1777
|
+
* 1. `deviceManager.getDevice` for the registry's BatteryOperated feature;
|
|
1778
|
+
* 2. `deviceState.getCapSlice('battery')` for the durable runtime mirror.
|
|
1718
1779
|
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
* battery
|
|
1722
|
-
*
|
|
1780
|
+
* A valid battery slice is itself positive evidence that the device is
|
|
1781
|
+
* battery-operated. That closes the field failure where "Baby monitor" had a
|
|
1782
|
+
* sleeping battery slice but a stale persisted feature projection, so the
|
|
1783
|
+
* snapshot wrapper classified it as mains and a Viewer thumbnail logged in
|
|
1784
|
+
* to the sleeping camera. The reverse is deliberately not guessed: no
|
|
1785
|
+
* feature + no valid slice remains a mains/unknown camera and follows the
|
|
1786
|
+
* ordinary capture path.
|
|
1723
1787
|
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1788
|
+
* Once battery-operated is established, a missing slice fails safe toward
|
|
1789
|
+
* `sleeping` through the shared `deriveBatteryPresence` function. This avoids
|
|
1790
|
+
* a background wake without inventing an `unreachable` fault.
|
|
1726
1791
|
*/
|
|
1727
|
-
async
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
* the stream error about what a camera is doing.
|
|
1735
|
-
*
|
|
1736
|
-
* A failed read answers `awake`: unknown must never become a fault badge,
|
|
1737
|
-
* and "assume awake" is the pre-existing, deliberately conservative failure
|
|
1738
|
-
* direction of the gate this replaces.
|
|
1739
|
-
*/
|
|
1740
|
-
async readBatteryPresence(deviceId) {
|
|
1792
|
+
async resolveSnapshotState(deviceId, meta) {
|
|
1793
|
+
if (meta?.disabled === true) return {
|
|
1794
|
+
isBattery: meta.isBattery,
|
|
1795
|
+
reason: "disabled"
|
|
1796
|
+
};
|
|
1797
|
+
const waking = this.isDeviceWaking(deviceId);
|
|
1798
|
+
let batteryStatus = null;
|
|
1741
1799
|
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()
|
|
1800
|
+
const raw = await this.ctx.api.deviceState.getCapSlice.query({
|
|
1801
|
+
deviceId,
|
|
1802
|
+
capName: batteryCapability.name
|
|
1750
1803
|
});
|
|
1804
|
+
const parsed = BatteryStatusSchema.safeParse(raw);
|
|
1805
|
+
if (parsed.success) batteryStatus = parsed.data;
|
|
1751
1806
|
} catch (err) {
|
|
1752
|
-
this.ctx.logger.
|
|
1807
|
+
this.ctx.logger.debug("snapshot: battery runtime-state read failed", {
|
|
1753
1808
|
tags: { deviceId },
|
|
1754
1809
|
meta: { error: errMsg(err) }
|
|
1755
1810
|
});
|
|
1756
|
-
return "awake";
|
|
1757
1811
|
}
|
|
1812
|
+
if (!(meta?.isBattery === true || batteryStatus !== null || waking)) return { isBattery: false };
|
|
1813
|
+
if (waking) return {
|
|
1814
|
+
isBattery: true,
|
|
1815
|
+
reason: "waking"
|
|
1816
|
+
};
|
|
1817
|
+
const presence = deriveBatteryPresence({
|
|
1818
|
+
status: batteryStatus,
|
|
1819
|
+
nowMs: Date.now()
|
|
1820
|
+
});
|
|
1821
|
+
return presence === "awake" ? { isBattery: true } : {
|
|
1822
|
+
isBattery: true,
|
|
1823
|
+
reason: presence
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
isDeviceWaking(deviceId) {
|
|
1827
|
+
const startedAt = this.wakingDevices.get(deviceId);
|
|
1828
|
+
if (startedAt === void 0) return false;
|
|
1829
|
+
if (Date.now() - startedAt <= SNAPSHOT_WAKING_TTL_MS) return true;
|
|
1830
|
+
this.wakingDevices.delete(deviceId);
|
|
1831
|
+
return false;
|
|
1758
1832
|
}
|
|
1759
1833
|
/**
|
|
1760
1834
|
* True when at least one of the device's brokers is actively
|
|
@@ -1930,12 +2004,48 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1930
2004
|
};
|
|
1931
2005
|
}
|
|
1932
2006
|
/**
|
|
2007
|
+
* Render a state-labelled answer. A cached real frame is the background, not
|
|
2008
|
+
* the answer by itself; if it cannot be decoded, fall back to the generated
|
|
2009
|
+
* courtesy background rather than leaking an unlabelled stale frame.
|
|
2010
|
+
*/
|
|
2011
|
+
async stateImage(deviceId, reason, deviceName, hit) {
|
|
2012
|
+
const resolvedName = deviceName ?? `#${String(deviceId)}`;
|
|
2013
|
+
if (!hit) return await this.courtesyImage(deviceId, reason, resolvedName);
|
|
2014
|
+
const key = `${String(hit.ts)}:${hit.streamId ?? "auto"}:${reason}:${resolvedName}`;
|
|
2015
|
+
const deviceFrames = this.stateFrames.get(deviceId) ?? /* @__PURE__ */ new Map();
|
|
2016
|
+
this.stateFrames.set(deviceId, deviceFrames);
|
|
2017
|
+
const cached = deviceFrames.get(key);
|
|
2018
|
+
if (cached !== void 0) return {
|
|
2019
|
+
base64: cached.toString("base64"),
|
|
2020
|
+
contentType: "image/jpeg"
|
|
2021
|
+
};
|
|
2022
|
+
try {
|
|
2023
|
+
const bytes = await renderCourtesyOverlayJpeg(Buffer.from(hit.data.base64, "base64"), {
|
|
2024
|
+
deviceName: resolvedName,
|
|
2025
|
+
reason
|
|
2026
|
+
});
|
|
2027
|
+
deviceFrames.set(key, bytes);
|
|
2028
|
+
return {
|
|
2029
|
+
base64: bytes.toString("base64"),
|
|
2030
|
+
contentType: "image/jpeg"
|
|
2031
|
+
};
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
this.ctx.logger.warn("snapshot: state overlay failed — using generated courtesy background", {
|
|
2034
|
+
tags: { deviceId },
|
|
2035
|
+
meta: {
|
|
2036
|
+
reason,
|
|
2037
|
+
error: errMsg(err)
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
return await this.courtesyImage(deviceId, reason, resolvedName);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
1933
2044
|
* Single-trip device lookup against device-manager. Returns the
|
|
1934
|
-
* fields the wrapper
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
* we're trying to be resilient to).
|
|
2045
|
+
* stable registry fields the wrapper consults. Battery classification is
|
|
2046
|
+
* completed by `resolveSnapshotState` from this feature flag PLUS the
|
|
2047
|
+
* canonical runtime-state mirror, so a stale persisted feature projection
|
|
2048
|
+
* cannot authorize a snapshot dial against a known sleeping battery camera.
|
|
1939
2049
|
*
|
|
1940
2050
|
* Logged at debug + null return on failure: every call site already
|
|
1941
2051
|
* has a sensible fallback path (cache hit, conservative default, …),
|
|
@@ -1967,7 +2077,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1967
2077
|
}
|
|
1968
2078
|
/** Settings-UI helper — battery flag drives the default max-age in the field description. */
|
|
1969
2079
|
async isDeviceBattery(deviceId) {
|
|
1970
|
-
|
|
2080
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
2081
|
+
return (await this.resolveSnapshotState(deviceId, meta)).isBattery;
|
|
1971
2082
|
}
|
|
1972
2083
|
/**
|
|
1973
2084
|
* 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.
|
|
@@ -60,8 +60,8 @@ export declare const RESIZE_TIMEOUT_MS = 10000;
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function resizeJpeg(bytes: Buffer, width: number, timeoutMs?: number): Promise<Buffer>;
|
|
62
62
|
/**
|
|
63
|
-
* Resized frames, keyed by (device, stream, width) AND
|
|
64
|
-
* source frame's timestamp.
|
|
63
|
+
* Resized frames, keyed by (device, stream, width, state treatment) AND
|
|
64
|
+
* validated against the source frame's timestamp.
|
|
65
65
|
*
|
|
66
66
|
* The timestamp is the whole correctness argument: a variant outlives nothing.
|
|
67
67
|
* When the underlying frame is recaptured its `capturedAt` moves, every variant
|
|
@@ -75,8 +75,8 @@ export declare class SnapshotVariantCache {
|
|
|
75
75
|
private static key;
|
|
76
76
|
/** The variant for this exact frame, or undefined when it is missing or was
|
|
77
77
|
* derived from an older capture. */
|
|
78
|
-
get(deviceId: number, streamId: string | undefined, width: number, sourceTs: number): Buffer | undefined;
|
|
79
|
-
set(deviceId: number, streamId: string | undefined, width: number, sourceTs: number, bytes: Buffer): void;
|
|
78
|
+
get(deviceId: number, streamId: string | undefined, width: number, sourceTs: number, variantKey?: string): Buffer | undefined;
|
|
79
|
+
set(deviceId: number, streamId: string | undefined, width: number, sourceTs: number, bytes: Buffer, variantKey?: string): void;
|
|
80
80
|
deleteDevice(deviceId: number): void;
|
|
81
81
|
clear(): void;
|
|
82
82
|
}
|