@camstack/system 1.2.92 → 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 +207 -75
- package/dist/builtins/snapshot/index.mjs +207 -75
- package/dist/builtins/snapshot/snapshot-courtesy.d.ts +24 -2
- 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 -26
- 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-BH2tZ9ZN.js → dist-BspfS8Q7.js} +105 -2
- package/dist/{dist-DXDvrBi0.mjs → dist-COd223t-.mjs} +94 -3
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as
|
|
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`
|
|
@@ -293,6 +293,8 @@ function courtesyLabel(reason) {
|
|
|
293
293
|
case "disabled": return "Disabled";
|
|
294
294
|
case "offline": return "Offline";
|
|
295
295
|
case "sleeping": return "Sleeping";
|
|
296
|
+
case "unreachable": return "Unreachable";
|
|
297
|
+
case "waking": return "Waking";
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
/**
|
|
@@ -304,6 +306,8 @@ function courtesyBackground(reason) {
|
|
|
304
306
|
case "disabled": return "#2b2b31";
|
|
305
307
|
case "offline": return "#3a2b2b";
|
|
306
308
|
case "sleeping": return "#232b3a";
|
|
309
|
+
case "waking": return "#243247";
|
|
310
|
+
case "unreachable": return "#3a2b2b";
|
|
307
311
|
}
|
|
308
312
|
}
|
|
309
313
|
/** XML escaping for text placed inside an SVG `<text>` node. */
|
|
@@ -318,15 +322,27 @@ function escapeCourtesyText(value) {
|
|
|
318
322
|
* frame read the same, with a floor so a thumbnail stays legible.
|
|
319
323
|
*/
|
|
320
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) {
|
|
321
335
|
const stateSize = Math.max(12, Math.round(spec.width / 12));
|
|
322
336
|
const nameSize = Math.max(9, Math.round(spec.width / 26));
|
|
323
337
|
const state = escapeCourtesyText(courtesyLabel(spec.reason));
|
|
324
338
|
const name = escapeCourtesyText(spec.deviceName);
|
|
325
339
|
const midY = spec.height / 2;
|
|
340
|
+
const washOpacity = overlay ? "0.62" : "1";
|
|
341
|
+
const textStroke = Math.max(1, Math.round(spec.width / 320));
|
|
326
342
|
return [
|
|
327
343
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${String(spec.width)}" height="${String(spec.height)}">`,
|
|
328
|
-
`<rect width="100%" height="100%" fill="${courtesyBackground(spec.reason)}"/>`,
|
|
329
|
-
`<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)}">`,
|
|
330
346
|
`<text x="50%" y="${String(Math.round(midY))}" font-size="${String(stateSize)}" fill="#e8e8ee">${state}</text>`,
|
|
331
347
|
`<text x="50%" y="${String(Math.round(midY + stateSize))}" font-size="${String(nameSize)}" fill="#9a9aa8">${name}</text>`,
|
|
332
348
|
`</g></svg>`
|
|
@@ -348,6 +364,24 @@ function renderCourtesyJpeg(spec) {
|
|
|
348
364
|
return bytes;
|
|
349
365
|
});
|
|
350
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
|
+
}
|
|
351
385
|
//#endregion
|
|
352
386
|
//#region src/builtins/snapshot/snapshot-link-url.ts
|
|
353
387
|
/**
|
|
@@ -541,7 +575,8 @@ function parseSnapshotMediaRequest(url) {
|
|
|
541
575
|
* The response's entity tag.
|
|
542
576
|
*
|
|
543
577
|
* It has to identify the VARIANT, not just the frame: the same capture can now
|
|
544
|
-
* be served at several widths
|
|
578
|
+
* be served at several widths, from several streams, and with a camera-state
|
|
579
|
+
* treatment, and a plain
|
|
545
580
|
* `"<device>-<capturedAt>"` would let a client that fetched `?w=320` receive a
|
|
546
581
|
* 304 for `?w=960` and render the small image at full size. The base form is
|
|
547
582
|
* unchanged for a plain request, so the identity
|
|
@@ -552,6 +587,7 @@ function snapshotEtag(variant, capturedAt) {
|
|
|
552
587
|
const parts = [`${String(variant.deviceId)}-${String(capturedAt)}`];
|
|
553
588
|
if (variant.streamId !== void 0) parts.push(`s${variant.streamId}`);
|
|
554
589
|
if (variant.width !== void 0) parts.push(`w${String(variant.width)}`);
|
|
590
|
+
if (variant.variantKey !== void 0) parts.push(`v${variant.variantKey}`);
|
|
555
591
|
return `"${parts.join("-")}"`;
|
|
556
592
|
}
|
|
557
593
|
/**
|
|
@@ -582,7 +618,8 @@ function createSnapshotMediaHandler(deps) {
|
|
|
582
618
|
const peekedEtag = snapshotEtag({
|
|
583
619
|
deviceId: parsed.deviceId,
|
|
584
620
|
streamId: parsed.streamId,
|
|
585
|
-
width: parsed.width
|
|
621
|
+
width: parsed.width,
|
|
622
|
+
variantKey: peeked.variantKey
|
|
586
623
|
}, peeked.capturedAt);
|
|
587
624
|
if (inm === peekedEtag) {
|
|
588
625
|
res.writeHead(304, {
|
|
@@ -613,7 +650,8 @@ function createSnapshotMediaHandler(deps) {
|
|
|
613
650
|
const etag = snapshotEtag({
|
|
614
651
|
deviceId: parsed.deviceId,
|
|
615
652
|
streamId: parsed.streamId,
|
|
616
|
-
width: "servedWidth" in media ? media.servedWidth : parsed.width
|
|
653
|
+
width: "servedWidth" in media ? media.servedWidth : parsed.width,
|
|
654
|
+
variantKey: media.variantKey
|
|
617
655
|
}, media.capturedAt);
|
|
618
656
|
const cacheControl = `private, max-age=${Math.max(0, Math.floor(media.maxAgeS))}`;
|
|
619
657
|
if (inm === etag) {
|
|
@@ -716,8 +754,8 @@ function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
|
|
|
716
754
|
});
|
|
717
755
|
}
|
|
718
756
|
/**
|
|
719
|
-
* Resized frames, keyed by (device, stream, width) AND
|
|
720
|
-
* source frame's timestamp.
|
|
757
|
+
* Resized frames, keyed by (device, stream, width, state treatment) AND
|
|
758
|
+
* validated against the source frame's timestamp.
|
|
721
759
|
*
|
|
722
760
|
* The timestamp is the whole correctness argument: a variant outlives nothing.
|
|
723
761
|
* When the underlying frame is recaptured its `capturedAt` moves, every variant
|
|
@@ -728,17 +766,17 @@ function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
|
|
|
728
766
|
var SnapshotVariantCache = class SnapshotVariantCache {
|
|
729
767
|
byKey = /* @__PURE__ */ new Map();
|
|
730
768
|
keysByDevice = /* @__PURE__ */ new Map();
|
|
731
|
-
static key(deviceId, streamId, width) {
|
|
732
|
-
return `${deviceId}:${streamId ?? "auto"}:${width}`;
|
|
769
|
+
static key(deviceId, streamId, width, variantKey) {
|
|
770
|
+
return `${deviceId}:${streamId ?? "auto"}:${width}:${variantKey ?? "raw"}`;
|
|
733
771
|
}
|
|
734
772
|
/** The variant for this exact frame, or undefined when it is missing or was
|
|
735
773
|
* derived from an older capture. */
|
|
736
|
-
get(deviceId, streamId, width, sourceTs) {
|
|
737
|
-
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));
|
|
738
776
|
return entry !== void 0 && entry.sourceTs === sourceTs ? entry.bytes : void 0;
|
|
739
777
|
}
|
|
740
|
-
set(deviceId, streamId, width, sourceTs, bytes) {
|
|
741
|
-
const key = SnapshotVariantCache.key(deviceId, streamId, width);
|
|
778
|
+
set(deviceId, streamId, width, sourceTs, bytes, variantKey) {
|
|
779
|
+
const key = SnapshotVariantCache.key(deviceId, streamId, width, variantKey);
|
|
742
780
|
this.byKey.set(key, {
|
|
743
781
|
bytes,
|
|
744
782
|
sourceTs
|
|
@@ -779,6 +817,9 @@ var LINK_MINT_DEADLINE_MS = 2500;
|
|
|
779
817
|
var LINK_BATCH_WAIT_MS = 750;
|
|
780
818
|
/** Frames older than this are never represented as current by the Viewer. */
|
|
781
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;
|
|
782
823
|
/** Default cache window for non-battery cams (seconds). 10s feels live. */
|
|
783
824
|
var NON_BATTERY_DEFAULT_MAX_AGE_S = 10;
|
|
784
825
|
/** Default cache window for battery cams (seconds). 1h ≈ "don't wake the cam unless asked". */
|
|
@@ -823,9 +864,14 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
823
864
|
* A courtesy frame is a pure function of those three, so it is rendered once
|
|
824
865
|
* and reused for as long as the process lives — a disabled camera must not
|
|
825
866
|
* cost an ffmpeg run per poll. Unbounded on purpose: the key space is
|
|
826
|
-
* (
|
|
867
|
+
* (state reasons × this node's own cameras), not user input.
|
|
827
868
|
*/
|
|
828
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();
|
|
829
875
|
/**
|
|
830
876
|
* De-dupes concurrent captures per `${deviceId}:${streamId}` and holds a
|
|
831
877
|
* settled SUCCESS for COALESCE_MS so a grid-mount burst (and a row of refresh
|
|
@@ -897,6 +943,12 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
897
943
|
const deviceId = event.data.deviceId;
|
|
898
944
|
if (typeof deviceId === "number") this.evictRemovedDevice(deviceId, "device-unregistered");
|
|
899
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
|
+
});
|
|
900
952
|
await this.serveMediaDataPlane();
|
|
901
953
|
await this.ensureLinkDataPlane();
|
|
902
954
|
return [{
|
|
@@ -1158,13 +1210,12 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1158
1210
|
}
|
|
1159
1211
|
});
|
|
1160
1212
|
}
|
|
1161
|
-
/** True only for a
|
|
1162
|
-
* question so the link path cannot accidentally treat "battery" as "asleep"
|
|
1163
|
-
* and stop refreshing a camera that is awake and streaming. */
|
|
1213
|
+
/** True only for a battery device that must not be snapshot-refreshed. */
|
|
1164
1214
|
async isSleepingBatteryDevice(deviceId) {
|
|
1165
1215
|
try {
|
|
1166
|
-
|
|
1167
|
-
|
|
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";
|
|
1168
1219
|
} catch {
|
|
1169
1220
|
return false;
|
|
1170
1221
|
}
|
|
@@ -1178,28 +1229,31 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1178
1229
|
* effective per-device `maxAgeS` for `Cache-Control`. Null → 404.
|
|
1179
1230
|
*/
|
|
1180
1231
|
async resolveSnapshotMedia(deviceId, streamId, force, width) {
|
|
1232
|
+
const resolutionMeta = {};
|
|
1181
1233
|
const image = await this.getSnapshot({
|
|
1182
1234
|
deviceId,
|
|
1183
1235
|
...streamId !== void 0 ? { streamId } : {},
|
|
1184
1236
|
force
|
|
1185
|
-
});
|
|
1237
|
+
}, 0, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, resolutionMeta);
|
|
1186
1238
|
if (!image) return null;
|
|
1187
1239
|
const capturedAt = this.cache.get(deviceId, streamId)?.ts ?? Date.now();
|
|
1188
1240
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1189
|
-
const isBattery =
|
|
1241
|
+
const isBattery = resolutionMeta.isBattery ?? false;
|
|
1190
1242
|
if (width === void 0) return {
|
|
1191
1243
|
bytes: Buffer.from(image.base64, "base64"),
|
|
1192
1244
|
contentType: image.contentType,
|
|
1193
1245
|
capturedAt,
|
|
1194
1246
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery),
|
|
1247
|
+
variantKey: resolutionMeta.stateReason,
|
|
1195
1248
|
servedWidth: void 0
|
|
1196
1249
|
};
|
|
1197
|
-
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);
|
|
1198
1251
|
return {
|
|
1199
1252
|
bytes: thumb.bytes,
|
|
1200
1253
|
contentType: image.contentType,
|
|
1201
1254
|
capturedAt,
|
|
1202
1255
|
maxAgeS: effectiveMaxAgeS(prefs, isBattery),
|
|
1256
|
+
variantKey: resolutionMeta.stateReason,
|
|
1203
1257
|
servedWidth: thumb.width
|
|
1204
1258
|
};
|
|
1205
1259
|
}
|
|
@@ -1215,24 +1269,28 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1215
1269
|
async peekFreshFrame(deviceId, streamId) {
|
|
1216
1270
|
const hit = this.cache.get(deviceId, streamId);
|
|
1217
1271
|
if (!hit) return null;
|
|
1218
|
-
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);
|
|
1219
1276
|
if (Date.now() - hit.ts >= maxAgeS * 1e3) return null;
|
|
1220
1277
|
return {
|
|
1221
1278
|
capturedAt: hit.ts,
|
|
1222
|
-
maxAgeS
|
|
1279
|
+
maxAgeS,
|
|
1280
|
+
variantKey: state.reason
|
|
1223
1281
|
};
|
|
1224
1282
|
}
|
|
1225
1283
|
/**
|
|
1226
|
-
* The frame at a card-sized width, derived once per
|
|
1227
|
-
* per capture.
|
|
1284
|
+
* The frame at a card-sized width, derived once per
|
|
1285
|
+
* (device, stream, width, state treatment) per capture.
|
|
1228
1286
|
*
|
|
1229
1287
|
* A failed resize falls back to the FULL frame — a visible card beats a
|
|
1230
1288
|
* broken one — but never silently: the card would otherwise keep costing a
|
|
1231
1289
|
* megabyte with nothing anywhere saying why, which is the shape of the bug
|
|
1232
1290
|
* this whole change came from.
|
|
1233
1291
|
*/
|
|
1234
|
-
async thumbnailBytes(deviceId, streamId, width, capturedAt, image, prefs) {
|
|
1235
|
-
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);
|
|
1236
1294
|
if (cached !== void 0) return {
|
|
1237
1295
|
bytes: cached,
|
|
1238
1296
|
width
|
|
@@ -1240,7 +1298,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1240
1298
|
const full = Buffer.from(image.base64, "base64");
|
|
1241
1299
|
try {
|
|
1242
1300
|
const resized = await this.resize(full, width);
|
|
1243
|
-
this.variants.set(deviceId, streamId, width, capturedAt, resized);
|
|
1301
|
+
this.variants.set(deviceId, streamId, width, capturedAt, resized, variantKey);
|
|
1244
1302
|
if (prefs.snapshotDebug) this.ctx.logger.info("snapshot: thumbnail derived", {
|
|
1245
1303
|
tags: { deviceId },
|
|
1246
1304
|
meta: {
|
|
@@ -1279,6 +1337,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1279
1337
|
}
|
|
1280
1338
|
this.cache.clear();
|
|
1281
1339
|
this.variants.clear();
|
|
1340
|
+
this.stateFrames.clear();
|
|
1341
|
+
this.wakingDevices.clear();
|
|
1282
1342
|
this.captureFlight.clear();
|
|
1283
1343
|
this.ownerCache = null;
|
|
1284
1344
|
}
|
|
@@ -1311,18 +1371,16 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1311
1371
|
})]
|
|
1312
1372
|
}] });
|
|
1313
1373
|
}
|
|
1314
|
-
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) {
|
|
1315
1375
|
const { deviceId } = input;
|
|
1316
1376
|
const force = input.force === true;
|
|
1317
1377
|
const meta = await this.lookupDeviceMeta(deviceId);
|
|
1318
1378
|
const deviceName = meta?.name;
|
|
1319
|
-
const isBatteryDevice = meta?.isBattery ?? false;
|
|
1320
1379
|
const log = this.ctx.logger.withTags({
|
|
1321
1380
|
deviceId,
|
|
1322
1381
|
...deviceName ? { deviceName } : {}
|
|
1323
1382
|
});
|
|
1324
1383
|
const now = Date.now();
|
|
1325
|
-
if (meta?.disabled === true) return await this.courtesyImage(deviceId, "disabled", deviceName);
|
|
1326
1384
|
const prefs = await this.readDeviceSettings(deviceId).catch(() => ({}));
|
|
1327
1385
|
const rawPref = prefs.snapshotStreamId;
|
|
1328
1386
|
const effectiveStreamId = input.streamId ?? (rawPref && rawPref !== "auto" ? rawPref : void 0);
|
|
@@ -1331,6 +1389,24 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1331
1389
|
meta: { stream: effectiveStreamId ?? "auto" }
|
|
1332
1390
|
});
|
|
1333
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
|
+
}
|
|
1334
1410
|
const effectiveMaxAgeMs = Math.min(maximumCacheAgeMs, Math.max(0, effectiveMaxAgeS(prefs, isBatteryDevice) * 1e3));
|
|
1335
1411
|
const decision = decideSnapshotServe({
|
|
1336
1412
|
now,
|
|
@@ -1349,17 +1425,6 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1349
1425
|
});
|
|
1350
1426
|
return hit ? hit.data : null;
|
|
1351
1427
|
}
|
|
1352
|
-
if (!force && isBatteryDevice && await this.isDeviceSleeping(deviceId)) {
|
|
1353
|
-
log.debug("snapshot: sleeping battery cam and no force — serving cache, not capturing", {
|
|
1354
|
-
tags: { deviceId },
|
|
1355
|
-
meta: {
|
|
1356
|
-
hasCache: hit !== void 0,
|
|
1357
|
-
ageMs: hit ? now - hit.ts : null
|
|
1358
|
-
}
|
|
1359
|
-
});
|
|
1360
|
-
if (hit) return hit.data;
|
|
1361
|
-
return await this.courtesyImage(deviceId, "sleeping", deviceName);
|
|
1362
|
-
}
|
|
1363
1428
|
const flightKey = `${deviceId}:${effectiveStreamId ?? "auto"}`;
|
|
1364
1429
|
const flight = this.captureFlight.run(flightKey, () => this.captureFresh({
|
|
1365
1430
|
deviceId,
|
|
@@ -1478,6 +1543,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1478
1543
|
if (native) {
|
|
1479
1544
|
const result = await this.nativePool.run(() => native.getSnapshot(input));
|
|
1480
1545
|
if (result) {
|
|
1546
|
+
this.stateFrames.delete(deviceId);
|
|
1481
1547
|
this.cache.set(deviceId, effectiveStreamId, {
|
|
1482
1548
|
data: result,
|
|
1483
1549
|
ts: now,
|
|
@@ -1512,6 +1578,7 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1512
1578
|
if (!(isBatteryDevice && nativeAbsent && !await this.hasStreamingBrokerForDevice(deviceId))) try {
|
|
1513
1579
|
const fallback = await this.grabFrameFromBroker(deviceId, effectiveStreamId);
|
|
1514
1580
|
if (fallback) {
|
|
1581
|
+
this.stateFrames.delete(deviceId);
|
|
1515
1582
|
this.cache.set(deviceId, effectiveStreamId, {
|
|
1516
1583
|
data: fallback,
|
|
1517
1584
|
ts: now,
|
|
@@ -1665,6 +1732,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1665
1732
|
dropDeviceCaches(deviceId) {
|
|
1666
1733
|
this.cache.deleteDevice(deviceId);
|
|
1667
1734
|
this.variants.deleteDevice(deviceId);
|
|
1735
|
+
this.stateFrames.delete(deviceId);
|
|
1736
|
+
this.wakingDevices.delete(deviceId);
|
|
1668
1737
|
this.captureFlight.invalidatePrefix(`${deviceId}:`);
|
|
1669
1738
|
}
|
|
1670
1739
|
/**
|
|
@@ -1702,38 +1771,64 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1702
1771
|
}
|
|
1703
1772
|
}
|
|
1704
1773
|
/**
|
|
1705
|
-
*
|
|
1774
|
+
* Resolve the state that must be visible on a snapshot.
|
|
1706
1775
|
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
* in the UI, so if the operator can see "sleeping" on screen, this
|
|
1711
|
-
* sees it too. (Both spec harnesses already model exactly this wiring
|
|
1712
|
-
* — like the orphaned doc comment above, it outlived the gate it was
|
|
1713
|
-
* 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.
|
|
1714
1779
|
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
* battery
|
|
1718
|
-
*
|
|
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.
|
|
1719
1787
|
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
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.
|
|
1722
1791
|
*/
|
|
1723
|
-
async
|
|
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;
|
|
1724
1799
|
try {
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1800
|
+
const raw = await this.ctx.api.deviceState.getCapSlice.query({
|
|
1801
|
+
deviceId,
|
|
1802
|
+
capName: batteryCapability.name
|
|
1803
|
+
});
|
|
1728
1804
|
const parsed = BatteryStatusSchema.safeParse(raw);
|
|
1729
|
-
|
|
1805
|
+
if (parsed.success) batteryStatus = parsed.data;
|
|
1730
1806
|
} catch (err) {
|
|
1731
|
-
this.ctx.logger.
|
|
1807
|
+
this.ctx.logger.debug("snapshot: battery runtime-state read failed", {
|
|
1732
1808
|
tags: { deviceId },
|
|
1733
1809
|
meta: { error: errMsg(err) }
|
|
1734
1810
|
});
|
|
1735
|
-
return false;
|
|
1736
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;
|
|
1737
1832
|
}
|
|
1738
1833
|
/**
|
|
1739
1834
|
* True when at least one of the device's brokers is actively
|
|
@@ -1909,12 +2004,48 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1909
2004
|
};
|
|
1910
2005
|
}
|
|
1911
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
|
+
/**
|
|
1912
2044
|
* Single-trip device lookup against device-manager. Returns the
|
|
1913
|
-
* fields the wrapper
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
*
|
|
1917
|
-
* 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.
|
|
1918
2049
|
*
|
|
1919
2050
|
* Logged at debug + null return on failure: every call site already
|
|
1920
2051
|
* has a sensible fallback path (cache hit, conservative default, …),
|
|
@@ -1946,7 +2077,8 @@ var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
|
1946
2077
|
}
|
|
1947
2078
|
/** Settings-UI helper — battery flag drives the default max-age in the field description. */
|
|
1948
2079
|
async isDeviceBattery(deviceId) {
|
|
1949
|
-
|
|
2080
|
+
const meta = await this.lookupDeviceMeta(deviceId);
|
|
2081
|
+
return (await this.resolveSnapshotState(deviceId, meta)).isBattery;
|
|
1950
2082
|
}
|
|
1951
2083
|
/**
|
|
1952
2084
|
* Feeds the settings-UI stream picker. Pinned to the ingest owner via
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Why a camera cannot produce a frame right now.
|
|
3
|
+
*
|
|
4
|
+
* `sleeping` and `unreachable` are deliberately two reasons and not one. A
|
|
5
|
+
* battery camera in low-power mode is WORKING — that tile must not read as a
|
|
6
|
+
* fault, which is D62's rule in the third state — while a battery camera
|
|
7
|
+
* nothing has been heard from for hours is a genuine fault the operator has to
|
|
8
|
+
* see (a flat battery, or a camera off the LAN). Painting the second as the
|
|
9
|
+
* first is how a dead camera stayed green for 30 hours. The two are separated
|
|
10
|
+
* by `deriveBatteryPresence`, never by a caller's own guess.
|
|
11
|
+
*/
|
|
12
|
+
export type CourtesyReason = 'disabled' | 'offline' | 'sleeping' | 'unreachable' | 'waking';
|
|
3
13
|
/** Default tile size when the caller asks for no particular width. */
|
|
4
14
|
export declare const COURTESY_DEFAULT_WIDTH = 640;
|
|
5
15
|
/** The word the frame carries. Deliberately the operator's vocabulary. */
|
|
@@ -25,6 +35,11 @@ export interface CourtesyFrameSpec {
|
|
|
25
35
|
* frame read the same, with a floor so a thumbnail stays legible.
|
|
26
36
|
*/
|
|
27
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;
|
|
28
43
|
/** Cache key — a courtesy frame is a pure function of these four. */
|
|
29
44
|
export declare function courtesyCacheKey(spec: CourtesyFrameSpec): string;
|
|
30
45
|
/**
|
|
@@ -34,3 +49,10 @@ export declare function courtesyCacheKey(spec: CourtesyFrameSpec): string;
|
|
|
34
49
|
* does, so a broken courtesy path is never mistaken for a broken camera.
|
|
35
50
|
*/
|
|
36
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
|
}
|