@camstack/system 1.2.267 → 1.2.268
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/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.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/camera-grid/addon.d.ts +13 -2
- package/dist/builtins/camera-grid/grid-canvas-derivation.d.ts +19 -0
- package/dist/builtins/camera-grid/grid-instances.d.ts +0 -2
- package/dist/builtins/camera-grid/grid-plan.d.ts +8 -6
- package/dist/builtins/camera-grid/grid-profiles.d.ts +33 -39
- package/dist/builtins/camera-grid/grid-stream-descriptor.d.ts +7 -4
- package/dist/builtins/camera-grid/grid-stream-relay.d.ts +10 -7
- package/dist/builtins/camera-grid/index.d.ts +3 -2
- package/dist/builtins/camera-grid/index.js +364 -257
- package/dist/builtins/camera-grid/index.mjs +361 -255
- 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 +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- 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 +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- 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 +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- 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-Ck0rjYOQ.mjs → dist-BYnnRAYv.mjs} +16 -6
- package/dist/{dist-BbPssgI6.js → dist-DWNlc-PG.js} +16 -6
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-Dg_69cOY.js → retired-settings-keys-BjSmLqyQ.js} +1 -1
- package/dist/{retired-settings-keys-BZpDuJVh.mjs → retired-settings-keys-C_MtEB25.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as streamCatalogCapability, At as motionCapability, Jt as snapshotCapability, Pn as EventCategory, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, an as errMsg, b as MOTION_CLOSE_AFTER_MS, bn as createEvent, dn as DeviceFeature, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, i as BaseDevice, on as buildFfmpegArgs, p as DeclaredDevices, pn as DeviceType, sn as BaseAddon, wn as hydrateSchema } from "../../dist-
|
|
1
|
+
import { $t as streamCatalogCapability, At as motionCapability, Jt as snapshotCapability, Pn as EventCategory, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, an as errMsg, b as MOTION_CLOSE_AFTER_MS, bn as createEvent, dn as DeviceFeature, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, i as BaseDevice, on as buildFfmpegArgs, p as DeclaredDevices, pn as DeviceType, sn as BaseAddon, wn as hydrateSchema } from "../../dist-BYnnRAYv.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { FfmpegProcess } from "@camstack/types/node";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -255,9 +255,17 @@ var GridInstanceSchema = z.object({
|
|
|
255
255
|
cameraStableId: z.string().min(1).max(256),
|
|
256
256
|
name: z.string().min(1).max(160),
|
|
257
257
|
enabled: z.boolean(),
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
/**
|
|
259
|
+
* NO canvas size. It is DERIVED, per profile, from the cells and the sources'
|
|
260
|
+
* own dimensions (`grid-canvas-derivation.ts`) — the two numbers that used to
|
|
261
|
+
* live here were typed into the editor and silently decided both how much the
|
|
262
|
+
* composite cost and how sharp it was.
|
|
263
|
+
*
|
|
264
|
+
* Rows written before that change still carry `width`/`height`; zod drops
|
|
265
|
+
* unknown keys, so they are ignored rather than migrated. Nothing reads them,
|
|
266
|
+
* and a migration that rewrote every row to delete two fields would be a
|
|
267
|
+
* write with no reader.
|
|
268
|
+
*/
|
|
261
269
|
fps: z.number().int().min(1).max(60),
|
|
262
270
|
/**
|
|
263
271
|
* Empty is the NORMAL freshly-created state: a grid is created from the
|
|
@@ -320,10 +328,7 @@ function gridPlanFor(instance, encode) {
|
|
|
320
328
|
cell: cell.cell
|
|
321
329
|
};
|
|
322
330
|
});
|
|
323
|
-
const canvas = encode.canvas
|
|
324
|
-
width: instance.width,
|
|
325
|
-
height: instance.height
|
|
326
|
-
};
|
|
331
|
+
const canvas = encode.canvas;
|
|
327
332
|
return {
|
|
328
333
|
layout: {
|
|
329
334
|
width: canvas.width,
|
|
@@ -921,92 +926,239 @@ var GridLastFrameFileStore = class {
|
|
|
921
926
|
}
|
|
922
927
|
};
|
|
923
928
|
//#endregion
|
|
924
|
-
//#region src/builtins/camera-grid/grid-
|
|
929
|
+
//#region src/builtins/camera-grid/grid-canvas.ts
|
|
930
|
+
/** Bytes one yuv420p frame of this size occupies. */
|
|
931
|
+
function yuv420pSize(width, height) {
|
|
932
|
+
return width * height + 2 * (width / 2) * (height / 2);
|
|
933
|
+
}
|
|
925
934
|
/**
|
|
926
|
-
*
|
|
935
|
+
* Round DOWN to even.
|
|
927
936
|
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* detour and it is `high` again, so nothing an operator assigned is disturbed.
|
|
937
|
+
* Down and not nearest: a tile that rounded UP would claim a row of samples the
|
|
938
|
+
* decoder was never asked to produce, and the copy would either overrun or be
|
|
939
|
+
* refused at the moment the picture is wanted.
|
|
932
940
|
*/
|
|
933
|
-
|
|
941
|
+
function evenDown(value) {
|
|
942
|
+
return Math.max(0, Math.floor(value / 2) * 2);
|
|
943
|
+
}
|
|
944
|
+
var GridCanvas = class {
|
|
945
|
+
width;
|
|
946
|
+
height;
|
|
947
|
+
buffer;
|
|
948
|
+
lumaBytes;
|
|
949
|
+
chromaBytes;
|
|
950
|
+
constructor(width, height) {
|
|
951
|
+
this.width = width;
|
|
952
|
+
this.height = height;
|
|
953
|
+
this.lumaBytes = width * height;
|
|
954
|
+
this.chromaBytes = width / 2 * (height / 2);
|
|
955
|
+
this.buffer = Buffer.alloc(yuv420pSize(width, height));
|
|
956
|
+
this.clear();
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Legible black — Y 0, chroma 128 (neutral).
|
|
960
|
+
*
|
|
961
|
+
* `Buffer.alloc` already zeroes, and zeroed CHROMA is not black: it is a
|
|
962
|
+
* strong green. A cell nobody has filled yet has to look like an empty cell,
|
|
963
|
+
* not like a fault.
|
|
964
|
+
*/
|
|
965
|
+
clear() {
|
|
966
|
+
this.buffer.fill(0, 0, this.lumaBytes);
|
|
967
|
+
this.buffer.fill(128, this.lumaBytes);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Copy one decoded tile in at `origin`.
|
|
971
|
+
*
|
|
972
|
+
* Returns false — never throws, never writes a partial tile — when the tile
|
|
973
|
+
* does not fit, is the wrong size for the dimensions it claims, or sits on an
|
|
974
|
+
* odd boundary. This runs on the output tick, so a bad frame from one source
|
|
975
|
+
* must cost that source's rectangle and nothing else.
|
|
976
|
+
*/
|
|
977
|
+
blit(tile, size, origin) {
|
|
978
|
+
if (!isEven(size.width) || !isEven(size.height)) return false;
|
|
979
|
+
if (!isEven(origin.x) || !isEven(origin.y)) return false;
|
|
980
|
+
if (size.width <= 0 || size.height <= 0) return false;
|
|
981
|
+
if (origin.x < 0 || origin.y < 0) return false;
|
|
982
|
+
if (origin.x + size.width > this.width) return false;
|
|
983
|
+
if (origin.y + size.height > this.height) return false;
|
|
984
|
+
if (tile.length !== yuv420pSize(size.width, size.height)) return false;
|
|
985
|
+
for (let row = 0; row < size.height; row += 1) {
|
|
986
|
+
const from = row * size.width;
|
|
987
|
+
const to = (origin.y + row) * this.width + origin.x;
|
|
988
|
+
tile.copy(this.buffer, to, from, from + size.width);
|
|
989
|
+
}
|
|
990
|
+
const tileLuma = size.width * size.height;
|
|
991
|
+
const tileChroma = size.width / 2 * (size.height / 2);
|
|
992
|
+
const halfW = size.width / 2;
|
|
993
|
+
const halfH = size.height / 2;
|
|
994
|
+
const canvasHalfW = this.width / 2;
|
|
995
|
+
for (let plane = 0; plane < 2; plane += 1) {
|
|
996
|
+
const tileBase = tileLuma + plane * tileChroma;
|
|
997
|
+
const canvasBase = this.lumaBytes + plane * this.chromaBytes;
|
|
998
|
+
for (let row = 0; row < halfH; row += 1) {
|
|
999
|
+
const from = tileBase + row * halfW;
|
|
1000
|
+
const to = canvasBase + (origin.y / 2 + row) * canvasHalfW + origin.x / 2;
|
|
1001
|
+
tile.copy(this.buffer, to, from, from + halfW);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
function isEven(value) {
|
|
1008
|
+
return Number.isInteger(value) && value % 2 === 0;
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/builtins/camera-grid/grid-canvas-derivation.ts
|
|
934
1012
|
/**
|
|
935
|
-
* The
|
|
1013
|
+
* The canvas is a RESULT, not a field.
|
|
1014
|
+
*
|
|
1015
|
+
* ## Why the operator stopped typing it
|
|
1016
|
+
*
|
|
1017
|
+
* A grid's resolution used to be two numbers in the editor, beside FPS. They
|
|
1018
|
+
* looked harmless and were not: that number silently decided how much the
|
|
1019
|
+
* composite cost and how sharp it was. An operator typing 1920x1080 was, without
|
|
1020
|
+
* being told, committing the hub to reading two 4 MP streams — or, with the
|
|
1021
|
+
* rule before it, to upscaling a 640x360 sub-stream by 3x. Neither is a thing
|
|
1022
|
+
* anyone can be expected to work out from a text field.
|
|
1023
|
+
*
|
|
1024
|
+
* ## What it is derived from
|
|
1025
|
+
*
|
|
1026
|
+
* The operator draws two rectangles per camera — which part of the source to
|
|
1027
|
+
* take, and where it lands — and both are fractions, so nothing is pixels yet.
|
|
1028
|
+
* Exactly one number is missing: how many pixels the whole canvas is worth.
|
|
936
1029
|
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
1030
|
+
* A cell has `sourceResolution x crop` REAL pixels behind it. Showing them 1:1
|
|
1031
|
+
* neither invents nor discards anything, so the cell should be exactly that
|
|
1032
|
+
* many pixels — and since the cell is a fraction `f` of the canvas, the canvas
|
|
1033
|
+
* it implies is `real / f`. Each cell proposes one, and the LARGEST wins: the
|
|
1034
|
+
* hungriest cell is served 1:1 and the others scale down by a hair, never up.
|
|
1035
|
+
*
|
|
1036
|
+
* Gaps change nothing. Cells need not cover the canvas, and an uncovered part
|
|
1037
|
+
* is background — it has no pixels to propose.
|
|
1038
|
+
*
|
|
1039
|
+
* ## One per profile
|
|
1040
|
+
*
|
|
1041
|
+
* A source is a different size at each profile, so each profile derives its own
|
|
1042
|
+
* canvas: a grid's `low` composes its sources' `low` onto the canvas their
|
|
1043
|
+
* `low` implies. That is what makes the three outputs 1:1 with what they are
|
|
1044
|
+
* made of, and what lets a viewer's adaptive pick a rung without the grid
|
|
1045
|
+
* needing to know it exists.
|
|
942
1046
|
*/
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* What an encoder will take. Not a taste: `libx264` refuses beyond 8192 in
|
|
1049
|
+
* either axis, and a canvas that large is a composition nobody can decode
|
|
1050
|
+
* anyway. Reached only by a cell that shows a sliver of a source at full
|
|
1051
|
+
* canvas width, which is a layout, not a mistake — so it is CLAMPED rather
|
|
1052
|
+
* than refused, and clamped in both axes together so the picture the operator
|
|
1053
|
+
* drew keeps its shape.
|
|
1054
|
+
*/
|
|
1055
|
+
var MAX_CANVAS_WIDTH = 7680;
|
|
1056
|
+
var MAX_CANVAS_HEIGHT = 4320;
|
|
1057
|
+
/** Below this a composition is not a cheaper picture, it is no picture. */
|
|
1058
|
+
var MIN_CANVAS_EDGE = 160;
|
|
1059
|
+
/**
|
|
1060
|
+
* The canvas these cells imply, or `null` when they imply none.
|
|
1061
|
+
*
|
|
1062
|
+
* `null` for a grid with no cells — the normal freshly-created state — and for
|
|
1063
|
+
* a layout whose every rectangle is degenerate. Both are "there is no picture
|
|
1064
|
+
* to size", which the caller turns into "this profile is not on offer" rather
|
|
1065
|
+
* than into a guess.
|
|
1066
|
+
*/
|
|
1067
|
+
function deriveGridCanvas(cells) {
|
|
1068
|
+
let width = 0;
|
|
1069
|
+
let height = 0;
|
|
1070
|
+
for (const entry of cells) {
|
|
1071
|
+
if (entry.cell.width <= 0 || entry.cell.height <= 0) continue;
|
|
1072
|
+
if (entry.sourceSize.width <= 0 || entry.sourceSize.height <= 0) continue;
|
|
1073
|
+
const realWidth = entry.sourceSize.width * entry.source.width;
|
|
1074
|
+
const realHeight = entry.sourceSize.height * entry.source.height;
|
|
1075
|
+
if (realWidth <= 0 || realHeight <= 0) continue;
|
|
1076
|
+
width = Math.max(width, realWidth / entry.cell.width);
|
|
1077
|
+
height = Math.max(height, realHeight / entry.cell.height);
|
|
1078
|
+
}
|
|
1079
|
+
if (width <= 0 || height <= 0) return null;
|
|
1080
|
+
const overshoot = Math.max(width / MAX_CANVAS_WIDTH, height / MAX_CANVAS_HEIGHT, 1);
|
|
1081
|
+
const factor = Math.max(MIN_CANVAS_EDGE / width, MIN_CANVAS_EDGE / height, 1) / overshoot;
|
|
960
1082
|
return {
|
|
961
|
-
width:
|
|
962
|
-
height:
|
|
1083
|
+
width: Math.max(2, evenDown(width * factor)),
|
|
1084
|
+
height: Math.max(2, evenDown(height * factor))
|
|
963
1085
|
};
|
|
964
1086
|
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/builtins/camera-grid/grid-profiles.ts
|
|
965
1089
|
/**
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
* Soft beats absent: a camera whose best stream cannot fill its cell still
|
|
969
|
-
* composes, at the best it has. Refusing would take the whole grid down over
|
|
970
|
-
* one cell being drawn larger than its source.
|
|
1090
|
+
* One id per profile, for every grid, forever.
|
|
971
1091
|
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
1092
|
+
* D519's reason is what keeps `high`'s id bare: a newly-spelled id mints a
|
|
1093
|
+
* second cam-stream and orphans whatever profile assignment the operator had
|
|
1094
|
+
* made. `grid` was `high` on the live hub before any of this and still is.
|
|
975
1095
|
*/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1096
|
+
var GRID_CAM_STREAM_ID = "grid";
|
|
1097
|
+
/** Every profile a grid can compose, in descending quality. */
|
|
1098
|
+
var GRID_PROFILES = [
|
|
1099
|
+
"high",
|
|
1100
|
+
"mid",
|
|
1101
|
+
"low"
|
|
1102
|
+
];
|
|
1103
|
+
/** The cam-stream id a grid publishes for one profile. */
|
|
1104
|
+
function gridCamStreamId(profile) {
|
|
1105
|
+
return profile === "high" ? GRID_CAM_STREAM_ID : `${GRID_CAM_STREAM_ID}-${profile}`;
|
|
985
1106
|
}
|
|
986
|
-
/**
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1107
|
+
/**
|
|
1108
|
+
* What this grid can honestly publish, per profile, with the canvas or the
|
|
1109
|
+
* refusal.
|
|
1110
|
+
*
|
|
1111
|
+
* Every profile gets a row whether it is offered or not: an absent row says
|
|
1112
|
+
* nothing, and the caller logging a refusal needs the reason more than the
|
|
1113
|
+
* caller publishing an offer needs the offer.
|
|
1114
|
+
*/
|
|
1115
|
+
function gridProfilePlans(input) {
|
|
1116
|
+
const sourceDeviceIds = [...new Set(input.cells.map((cell) => cell.deviceId))];
|
|
1117
|
+
const servedBy = /* @__PURE__ */ new Map();
|
|
1118
|
+
for (const deviceId of sourceDeviceIds) servedBy.set(deviceId, input.profilesServedBy(deviceId));
|
|
1119
|
+
return GRID_PROFILES.map((profile) => {
|
|
1120
|
+
if (sourceDeviceIds.length === 0) return {
|
|
1121
|
+
profile,
|
|
1122
|
+
offered: false,
|
|
1123
|
+
missingSources: []
|
|
1124
|
+
};
|
|
1125
|
+
const sizeBySource = /* @__PURE__ */ new Map();
|
|
1126
|
+
const missingSources = [];
|
|
1127
|
+
for (const deviceId of sourceDeviceIds) {
|
|
1128
|
+
const match = (servedBy.get(deviceId) ?? null)?.find((entry) => entry.profile === profile);
|
|
1129
|
+
if (match === void 0 || match.width <= 0 || match.height <= 0) {
|
|
1130
|
+
missingSources.push(deviceId);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
sizeBySource.set(deviceId, {
|
|
1134
|
+
width: match.width,
|
|
1135
|
+
height: match.height
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
if (missingSources.length > 0) return {
|
|
1139
|
+
profile,
|
|
1140
|
+
offered: false,
|
|
1141
|
+
missingSources
|
|
1142
|
+
};
|
|
1143
|
+
const canvas = deriveGridCanvas(input.cells.map((cell) => ({
|
|
1144
|
+
source: cell.source,
|
|
1145
|
+
cell: cell.cell,
|
|
1146
|
+
sourceSize: sizeBySource.get(cell.deviceId) ?? {
|
|
1147
|
+
width: 0,
|
|
1148
|
+
height: 0
|
|
1149
|
+
}
|
|
1150
|
+
})));
|
|
1151
|
+
if (canvas === null) return {
|
|
1152
|
+
profile,
|
|
1153
|
+
offered: false,
|
|
1154
|
+
missingSources: []
|
|
1155
|
+
};
|
|
1156
|
+
return {
|
|
1157
|
+
profile,
|
|
1158
|
+
offered: true,
|
|
1159
|
+
canvas
|
|
1160
|
+
};
|
|
1161
|
+
});
|
|
1010
1162
|
}
|
|
1011
1163
|
//#endregion
|
|
1012
1164
|
//#region src/builtins/camera-grid/grid-sentry-sources.ts
|
|
@@ -1171,15 +1323,24 @@ var PATH_PREFIX = "/grid/";
|
|
|
1171
1323
|
var PATH_SUFFIX = GRID_WIRE_PATH_SUFFIX;
|
|
1172
1324
|
/** How long a reader may refuse to drain before it is cut. */
|
|
1173
1325
|
var SLOW_READER_MS = 5e3;
|
|
1174
|
-
|
|
1326
|
+
function asProfile(raw) {
|
|
1327
|
+
return GRID_PROFILES.find((profile) => profile === raw) ?? null;
|
|
1328
|
+
}
|
|
1329
|
+
/** `/grid/<instanceId>/<profile>.flv`, or `null` when it is anything else. */
|
|
1175
1330
|
function parseGridStreamPath(url) {
|
|
1176
1331
|
const path = (url ?? "").split("?")[0] ?? "";
|
|
1177
1332
|
if (!path.startsWith(PATH_PREFIX) || !path.endsWith(PATH_SUFFIX)) return null;
|
|
1178
1333
|
const raw = path.slice(6, path.length - PATH_SUFFIX.length);
|
|
1179
|
-
|
|
1334
|
+
const at = raw.lastIndexOf("/");
|
|
1335
|
+
if (at <= 0) return null;
|
|
1336
|
+
const profile = asProfile(raw.slice(at + 1));
|
|
1337
|
+
if (profile === null) return null;
|
|
1180
1338
|
try {
|
|
1181
|
-
const instanceId = decodeURIComponent(raw);
|
|
1182
|
-
return instanceId.length > 0 ? {
|
|
1339
|
+
const instanceId = decodeURIComponent(raw.slice(0, at));
|
|
1340
|
+
return instanceId.length > 0 ? {
|
|
1341
|
+
instanceId,
|
|
1342
|
+
profile
|
|
1343
|
+
} : null;
|
|
1183
1344
|
} catch {
|
|
1184
1345
|
return null;
|
|
1185
1346
|
}
|
|
@@ -1218,13 +1379,16 @@ var GridStreamRelay = class {
|
|
|
1218
1379
|
streamUrl(request) {
|
|
1219
1380
|
if (!this.baseUrl) throw new Error("camera-grid: the stream relay is not started");
|
|
1220
1381
|
const instance = encodeURIComponent(request.instanceId);
|
|
1221
|
-
return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
|
|
1382
|
+
return `${this.baseUrl}${PATH_PREFIX}${instance}/${request.profile}${PATH_SUFFIX}`;
|
|
1222
1383
|
}
|
|
1223
1384
|
async serve(request, res) {
|
|
1224
|
-
const { instanceId } = request;
|
|
1385
|
+
const { instanceId, profile } = request;
|
|
1225
1386
|
const session = this.deps.sessionFor(request);
|
|
1226
1387
|
if (session === null) {
|
|
1227
|
-
this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
|
|
1388
|
+
this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
|
|
1389
|
+
instanceId,
|
|
1390
|
+
profile
|
|
1391
|
+
} });
|
|
1228
1392
|
res.writeHead(404).end();
|
|
1229
1393
|
return;
|
|
1230
1394
|
}
|
|
@@ -1258,6 +1422,7 @@ var GridStreamRelay = class {
|
|
|
1258
1422
|
tags: { deviceId: session.deviceId },
|
|
1259
1423
|
meta: {
|
|
1260
1424
|
instanceId,
|
|
1425
|
+
profile,
|
|
1261
1426
|
stalledForMs: SLOW_READER_MS
|
|
1262
1427
|
}
|
|
1263
1428
|
});
|
|
@@ -1270,6 +1435,7 @@ var GridStreamRelay = class {
|
|
|
1270
1435
|
tags: { deviceId: session.deviceId },
|
|
1271
1436
|
meta: {
|
|
1272
1437
|
instanceId,
|
|
1438
|
+
profile,
|
|
1273
1439
|
reason
|
|
1274
1440
|
}
|
|
1275
1441
|
});
|
|
@@ -1282,6 +1448,7 @@ var GridStreamRelay = class {
|
|
|
1282
1448
|
tags: { deviceId: session.deviceId },
|
|
1283
1449
|
meta: {
|
|
1284
1450
|
instanceId,
|
|
1451
|
+
profile,
|
|
1285
1452
|
error: errMsg(error)
|
|
1286
1453
|
}
|
|
1287
1454
|
});
|
|
@@ -1317,13 +1484,13 @@ var GridStreamRelay = class {
|
|
|
1317
1484
|
* scrub never reads. The Dreame robot's one 720p relay landed exactly there and
|
|
1318
1485
|
* its footage was written nowhere anything looked.
|
|
1319
1486
|
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1487
|
+
* With several compositions published the hint does a second job: it says which
|
|
1488
|
+
* slot each one is FOR. Ranked by pixels a grid's `mid` would take the `high`
|
|
1489
|
+
* slot on any grid whose `high` was not offered.
|
|
1323
1490
|
*/
|
|
1324
1491
|
function gridStreamDescriptor(input) {
|
|
1325
1492
|
return {
|
|
1326
|
-
camStreamId:
|
|
1493
|
+
camStreamId: gridCamStreamId(input.profile),
|
|
1327
1494
|
kind: GRID_WIRE_STREAM_KIND,
|
|
1328
1495
|
url: input.url,
|
|
1329
1496
|
codec: "h264",
|
|
@@ -1332,8 +1499,8 @@ function gridStreamDescriptor(input) {
|
|
|
1332
1499
|
height: input.height
|
|
1333
1500
|
},
|
|
1334
1501
|
fps: input.fps,
|
|
1335
|
-
label:
|
|
1336
|
-
profileHint:
|
|
1502
|
+
label: `Grid composition (${input.profile})`,
|
|
1503
|
+
profileHint: input.profile,
|
|
1337
1504
|
autoEligible: true
|
|
1338
1505
|
};
|
|
1339
1506
|
}
|
|
@@ -1836,88 +2003,6 @@ async function silenceAnalysisFor(deps, deviceId) {
|
|
|
1836
2003
|
if (failures.length > 0) throw new Error(`camera grid ${deviceId}: could not switch off ${failures.length} analyzer(s) — it will run at full detection cost (${failures.join("; ")})`);
|
|
1837
2004
|
}
|
|
1838
2005
|
//#endregion
|
|
1839
|
-
//#region src/builtins/camera-grid/grid-canvas.ts
|
|
1840
|
-
/** Bytes one yuv420p frame of this size occupies. */
|
|
1841
|
-
function yuv420pSize(width, height) {
|
|
1842
|
-
return width * height + 2 * (width / 2) * (height / 2);
|
|
1843
|
-
}
|
|
1844
|
-
/**
|
|
1845
|
-
* Round DOWN to even.
|
|
1846
|
-
*
|
|
1847
|
-
* Down and not nearest: a tile that rounded UP would claim a row of samples the
|
|
1848
|
-
* decoder was never asked to produce, and the copy would either overrun or be
|
|
1849
|
-
* refused at the moment the picture is wanted.
|
|
1850
|
-
*/
|
|
1851
|
-
function evenDown(value) {
|
|
1852
|
-
return Math.max(0, Math.floor(value / 2) * 2);
|
|
1853
|
-
}
|
|
1854
|
-
var GridCanvas = class {
|
|
1855
|
-
width;
|
|
1856
|
-
height;
|
|
1857
|
-
buffer;
|
|
1858
|
-
lumaBytes;
|
|
1859
|
-
chromaBytes;
|
|
1860
|
-
constructor(width, height) {
|
|
1861
|
-
this.width = width;
|
|
1862
|
-
this.height = height;
|
|
1863
|
-
this.lumaBytes = width * height;
|
|
1864
|
-
this.chromaBytes = width / 2 * (height / 2);
|
|
1865
|
-
this.buffer = Buffer.alloc(yuv420pSize(width, height));
|
|
1866
|
-
this.clear();
|
|
1867
|
-
}
|
|
1868
|
-
/**
|
|
1869
|
-
* Legible black — Y 0, chroma 128 (neutral).
|
|
1870
|
-
*
|
|
1871
|
-
* `Buffer.alloc` already zeroes, and zeroed CHROMA is not black: it is a
|
|
1872
|
-
* strong green. A cell nobody has filled yet has to look like an empty cell,
|
|
1873
|
-
* not like a fault.
|
|
1874
|
-
*/
|
|
1875
|
-
clear() {
|
|
1876
|
-
this.buffer.fill(0, 0, this.lumaBytes);
|
|
1877
|
-
this.buffer.fill(128, this.lumaBytes);
|
|
1878
|
-
}
|
|
1879
|
-
/**
|
|
1880
|
-
* Copy one decoded tile in at `origin`.
|
|
1881
|
-
*
|
|
1882
|
-
* Returns false — never throws, never writes a partial tile — when the tile
|
|
1883
|
-
* does not fit, is the wrong size for the dimensions it claims, or sits on an
|
|
1884
|
-
* odd boundary. This runs on the output tick, so a bad frame from one source
|
|
1885
|
-
* must cost that source's rectangle and nothing else.
|
|
1886
|
-
*/
|
|
1887
|
-
blit(tile, size, origin) {
|
|
1888
|
-
if (!isEven(size.width) || !isEven(size.height)) return false;
|
|
1889
|
-
if (!isEven(origin.x) || !isEven(origin.y)) return false;
|
|
1890
|
-
if (size.width <= 0 || size.height <= 0) return false;
|
|
1891
|
-
if (origin.x < 0 || origin.y < 0) return false;
|
|
1892
|
-
if (origin.x + size.width > this.width) return false;
|
|
1893
|
-
if (origin.y + size.height > this.height) return false;
|
|
1894
|
-
if (tile.length !== yuv420pSize(size.width, size.height)) return false;
|
|
1895
|
-
for (let row = 0; row < size.height; row += 1) {
|
|
1896
|
-
const from = row * size.width;
|
|
1897
|
-
const to = (origin.y + row) * this.width + origin.x;
|
|
1898
|
-
tile.copy(this.buffer, to, from, from + size.width);
|
|
1899
|
-
}
|
|
1900
|
-
const tileLuma = size.width * size.height;
|
|
1901
|
-
const tileChroma = size.width / 2 * (size.height / 2);
|
|
1902
|
-
const halfW = size.width / 2;
|
|
1903
|
-
const halfH = size.height / 2;
|
|
1904
|
-
const canvasHalfW = this.width / 2;
|
|
1905
|
-
for (let plane = 0; plane < 2; plane += 1) {
|
|
1906
|
-
const tileBase = tileLuma + plane * tileChroma;
|
|
1907
|
-
const canvasBase = this.lumaBytes + plane * this.chromaBytes;
|
|
1908
|
-
for (let row = 0; row < halfH; row += 1) {
|
|
1909
|
-
const from = tileBase + row * halfW;
|
|
1910
|
-
const to = canvasBase + (origin.y / 2 + row) * canvasHalfW + origin.x / 2;
|
|
1911
|
-
tile.copy(this.buffer, to, from, from + halfW);
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
return true;
|
|
1915
|
-
}
|
|
1916
|
-
};
|
|
1917
|
-
function isEven(value) {
|
|
1918
|
-
return Number.isInteger(value) && value % 2 === 0;
|
|
1919
|
-
}
|
|
1920
|
-
//#endregion
|
|
1921
2006
|
//#region src/builtins/camera-grid/grid-tile-feed.ts
|
|
1922
2007
|
/**
|
|
1923
2008
|
* One source's decoded tiles, of which only the LATEST is ever wanted.
|
|
@@ -2419,18 +2504,26 @@ var DEFAULTS = {
|
|
|
2419
2504
|
cameraGrids: [],
|
|
2420
2505
|
cameraGridTombstones: []
|
|
2421
2506
|
};
|
|
2507
|
+
/** One composition per (grid, profile) — a grid composes each separately. */
|
|
2508
|
+
function sessionKey(instanceId, profile) {
|
|
2509
|
+
return `${instanceId}::${profile}`;
|
|
2510
|
+
}
|
|
2422
2511
|
/**
|
|
2423
|
-
* One grid's composition
|
|
2512
|
+
* One grid's composition of one profile.
|
|
2424
2513
|
*
|
|
2425
|
-
*
|
|
2426
|
-
*
|
|
2427
|
-
*
|
|
2514
|
+
* The profile is back in the key after a spell with a single output. What made
|
|
2515
|
+
* the per-profile shape painful was never the count: it was that a rebuild
|
|
2516
|
+
* meant a blank screen until the slowest source produced a key frame. An output
|
|
2517
|
+
* clock (D530) shows the picture at once and fills the cells in, so a rung the
|
|
2518
|
+
* adaptive can stand on costs what it is worth.
|
|
2428
2519
|
*/
|
|
2429
2520
|
var GridSessionEntry = class {
|
|
2430
2521
|
instanceId;
|
|
2522
|
+
profile;
|
|
2431
2523
|
session;
|
|
2432
|
-
constructor(instanceId, session) {
|
|
2524
|
+
constructor(instanceId, profile, session) {
|
|
2433
2525
|
this.instanceId = instanceId;
|
|
2526
|
+
this.profile = profile;
|
|
2434
2527
|
this.session = session;
|
|
2435
2528
|
}
|
|
2436
2529
|
isRunning() {
|
|
@@ -2454,7 +2547,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2454
2547
|
* (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
|
|
2455
2548
|
* — that only changes when an operator changes it.
|
|
2456
2549
|
*/
|
|
2457
|
-
|
|
2550
|
+
plansByInstanceId = /* @__PURE__ */ new Map();
|
|
2458
2551
|
activity = null;
|
|
2459
2552
|
snapshots = null;
|
|
2460
2553
|
frameSampler = null;
|
|
@@ -2687,9 +2780,9 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2687
2780
|
if (instance.cells.filter((cell) => cell.deviceId === sourceDeviceId).length === 0) continue;
|
|
2688
2781
|
targets.push({
|
|
2689
2782
|
deviceId,
|
|
2690
|
-
canvas: {
|
|
2691
|
-
width:
|
|
2692
|
-
height:
|
|
2783
|
+
canvas: this.canvasFor(instanceId) ?? {
|
|
2784
|
+
width: 0,
|
|
2785
|
+
height: 0
|
|
2693
2786
|
},
|
|
2694
2787
|
cellsForSource: (id) => instance.cells.filter((cell) => cell.deviceId === id).map((cell) => ({
|
|
2695
2788
|
source: cell.source,
|
|
@@ -2732,11 +2825,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2732
2825
|
servedBy.set(slot.deviceId, profiles);
|
|
2733
2826
|
}
|
|
2734
2827
|
for (const instance of instances) {
|
|
2735
|
-
const
|
|
2736
|
-
canvas: {
|
|
2737
|
-
width: instance.width,
|
|
2738
|
-
height: instance.height
|
|
2739
|
-
},
|
|
2828
|
+
const plans = gridProfilePlans({
|
|
2740
2829
|
cells: instance.cells.map((cell) => ({
|
|
2741
2830
|
deviceId: cell.deviceId,
|
|
2742
2831
|
source: cell.source,
|
|
@@ -2744,9 +2833,9 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2744
2833
|
})),
|
|
2745
2834
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
2746
2835
|
});
|
|
2747
|
-
const previous = this.
|
|
2748
|
-
this.
|
|
2749
|
-
this.logSourcePlan(instance,
|
|
2836
|
+
const previous = this.plansByInstanceId.get(instance.id);
|
|
2837
|
+
this.plansByInstanceId.set(instance.id, plans);
|
|
2838
|
+
this.logSourcePlan(instance, plans, previous);
|
|
2750
2839
|
}
|
|
2751
2840
|
}
|
|
2752
2841
|
/**
|
|
@@ -2762,48 +2851,65 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2762
2851
|
* Logged on the EDGE: a grid that cannot be composed would otherwise write a
|
|
2763
2852
|
* line a minute for ever.
|
|
2764
2853
|
*/
|
|
2765
|
-
logSourcePlan(instance,
|
|
2766
|
-
const shape = (value) => JSON.stringify(
|
|
2767
|
-
|
|
2768
|
-
[...value.profileBySource],
|
|
2769
|
-
value.missingSources
|
|
2770
|
-
]);
|
|
2771
|
-
if (previous !== void 0 && shape(previous) === shape(plan)) return;
|
|
2854
|
+
logSourcePlan(instance, plans, previous) {
|
|
2855
|
+
const shape = (value) => JSON.stringify(value);
|
|
2856
|
+
if (previous !== void 0 && shape(previous) === shape(plans)) return;
|
|
2772
2857
|
const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2858
|
+
for (const row of plans) {
|
|
2859
|
+
if (row.offered || row.missingSources.length === 0) continue;
|
|
2860
|
+
this.ctx.logger.info("camera grid: a profile is NOT on offer", {
|
|
2861
|
+
tags: { deviceId },
|
|
2862
|
+
meta: {
|
|
2863
|
+
instanceId: instance.id,
|
|
2864
|
+
profile: row.profile,
|
|
2865
|
+
missingSources: [...row.missingSources]
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
this.ctx.logger.info("camera grid: published profiles", {
|
|
2781
2870
|
tags: { deviceId },
|
|
2782
2871
|
meta: {
|
|
2783
2872
|
instanceId: instance.id,
|
|
2784
|
-
offered:
|
|
2785
|
-
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
2873
|
+
offered: Object.fromEntries(plans.filter((row) => row.offered).map((row) => [row.profile, row.offered ? `${String(row.canvas.width)}x${String(row.canvas.height)}` : ""]))
|
|
2786
2874
|
}
|
|
2787
2875
|
});
|
|
2788
2876
|
}
|
|
2789
|
-
|
|
2790
|
-
return this.
|
|
2877
|
+
plansFor(instanceId) {
|
|
2878
|
+
return this.plansByInstanceId.get(instanceId) ?? null;
|
|
2879
|
+
}
|
|
2880
|
+
planFor(instanceId, profile) {
|
|
2881
|
+
return this.plansFor(instanceId)?.find((row) => row.profile === profile) ?? null;
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* The canvas of the BEST profile this grid offers.
|
|
2885
|
+
*
|
|
2886
|
+
* Used where a single geometry is needed for something that is not a
|
|
2887
|
+
* composition — the detection boxes a grid forwards (`grid-activity`), which
|
|
2888
|
+
* are normalized against whatever picture the viewer is looking at. The best
|
|
2889
|
+
* offered one is the honest default; a grid offering nothing has no geometry
|
|
2890
|
+
* and says so.
|
|
2891
|
+
*/
|
|
2892
|
+
canvasFor(instanceId) {
|
|
2893
|
+
for (const row of this.plansFor(instanceId) ?? []) if (row.offered) return row.canvas;
|
|
2894
|
+
return null;
|
|
2791
2895
|
}
|
|
2792
2896
|
ensureSessions(instanceId, deviceId) {
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
const
|
|
2796
|
-
if (
|
|
2797
|
-
this.sessions.
|
|
2798
|
-
stale
|
|
2897
|
+
for (const profile of GRID_PROFILES) {
|
|
2898
|
+
const key = sessionKey(instanceId, profile);
|
|
2899
|
+
const plan = this.planFor(instanceId, profile);
|
|
2900
|
+
if (plan === null || !plan.offered) {
|
|
2901
|
+
const stale = this.sessions.get(key);
|
|
2902
|
+
if (stale) {
|
|
2903
|
+
this.sessions.delete(key);
|
|
2904
|
+
stale.session.dispose();
|
|
2905
|
+
}
|
|
2906
|
+
continue;
|
|
2799
2907
|
}
|
|
2800
|
-
|
|
2908
|
+
if (this.sessions.has(key)) continue;
|
|
2909
|
+
this.sessions.set(key, new GridSessionEntry(instanceId, profile, this.makeSession(instanceId, deviceId, profile)));
|
|
2801
2910
|
}
|
|
2802
|
-
if (this.sessions.has(instanceId)) return;
|
|
2803
|
-
const session = this.makeSession(instanceId, deviceId);
|
|
2804
|
-
this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
|
|
2805
2911
|
}
|
|
2806
|
-
makeSession(instanceId, deviceId) {
|
|
2912
|
+
makeSession(instanceId, deviceId, profile) {
|
|
2807
2913
|
const childContext = {
|
|
2808
2914
|
binaryPath: this.ffmpegBinaryPath,
|
|
2809
2915
|
logger: this.ctx.logger,
|
|
@@ -2814,8 +2920,6 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2814
2920
|
deps: {
|
|
2815
2921
|
logger: this.ctx.logger,
|
|
2816
2922
|
acquireSource: async (sourceDeviceId) => {
|
|
2817
|
-
const profile = this.planFor(instanceId)?.profileBySource.get(sourceDeviceId);
|
|
2818
|
-
if (profile === void 0) throw new Error(`camera-grid: no stream is assigned for source ${String(sourceDeviceId)}`);
|
|
2819
2923
|
const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
|
|
2820
2924
|
deviceId: sourceDeviceId,
|
|
2821
2925
|
video: "copy",
|
|
@@ -2862,10 +2966,12 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2862
2966
|
},
|
|
2863
2967
|
resolvePlan: () => {
|
|
2864
2968
|
const instance = this.instanceById(instanceId);
|
|
2865
|
-
|
|
2969
|
+
const plan = this.planFor(instanceId, profile);
|
|
2970
|
+
if (!instance || plan === null || !plan.offered) return null;
|
|
2866
2971
|
return gridPlanFor(instance, {
|
|
2867
2972
|
encoder: "libx264",
|
|
2868
|
-
decodeHwAccel: null
|
|
2973
|
+
decodeHwAccel: null,
|
|
2974
|
+
canvas: plan.canvas
|
|
2869
2975
|
});
|
|
2870
2976
|
}
|
|
2871
2977
|
});
|
|
@@ -2873,14 +2979,18 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2873
2979
|
}
|
|
2874
2980
|
/** The session a dial is asking for, or `null` - which the relay answers 404 to. */
|
|
2875
2981
|
sessionFor(request) {
|
|
2876
|
-
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
2982
|
+
return this.sessions.get(sessionKey(request.instanceId, request.profile))?.session ?? null;
|
|
2877
2983
|
}
|
|
2878
|
-
/** This grid's
|
|
2984
|
+
/** This grid's sessions, in DESCENDING quality — the snapshot takes the first running one. */
|
|
2879
2985
|
sessionsForDevice(deviceId) {
|
|
2880
2986
|
const instanceId = this.instanceIdForDevice(deviceId);
|
|
2881
2987
|
if (instanceId === null) return [];
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2988
|
+
const entries = [];
|
|
2989
|
+
for (const profile of GRID_PROFILES) {
|
|
2990
|
+
const entry = this.sessions.get(sessionKey(instanceId, profile));
|
|
2991
|
+
if (entry) entries.push(entry);
|
|
2992
|
+
}
|
|
2993
|
+
return entries;
|
|
2884
2994
|
}
|
|
2885
2995
|
/** A grid that is gone stops immediately - it must not keep N sources dialled. */
|
|
2886
2996
|
async retireWithdrawnSessions(instances) {
|
|
@@ -2898,7 +3008,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2898
3008
|
if (live.has(instanceId)) continue;
|
|
2899
3009
|
this.activity?.forget(this.deviceIdByInstanceId.get(instanceId) ?? 0);
|
|
2900
3010
|
this.deviceIdByInstanceId.delete(instanceId);
|
|
2901
|
-
this.
|
|
3011
|
+
this.plansByInstanceId.delete(instanceId);
|
|
2902
3012
|
}
|
|
2903
3013
|
}
|
|
2904
3014
|
/**
|
|
@@ -2946,15 +3056,18 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2946
3056
|
descriptorsFor(instanceId) {
|
|
2947
3057
|
const instance = this.instanceById(instanceId);
|
|
2948
3058
|
const relay = this.relay;
|
|
2949
|
-
const
|
|
2950
|
-
if (!instance || !relay ||
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
width:
|
|
2954
|
-
height:
|
|
3059
|
+
const plans = this.plansFor(instanceId);
|
|
3060
|
+
if (!instance || !relay || plans === null) return null;
|
|
3061
|
+
return plans.filter((row) => row.offered).map((row) => gridStreamDescriptor({
|
|
3062
|
+
profile: row.profile,
|
|
3063
|
+
width: row.offered ? row.canvas.width : 0,
|
|
3064
|
+
height: row.offered ? row.canvas.height : 0,
|
|
2955
3065
|
fps: instance.fps,
|
|
2956
|
-
url: relay.streamUrl({
|
|
2957
|
-
|
|
3066
|
+
url: relay.streamUrl({
|
|
3067
|
+
instanceId,
|
|
3068
|
+
profile: row.profile
|
|
3069
|
+
})
|
|
3070
|
+
}));
|
|
2958
3071
|
}
|
|
2959
3072
|
instanceIdForDevice(deviceId) {
|
|
2960
3073
|
for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
|
|
@@ -2971,23 +3084,18 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2971
3084
|
instanceId: instance.id,
|
|
2972
3085
|
deviceId,
|
|
2973
3086
|
name: instance.name,
|
|
2974
|
-
width: instance.width,
|
|
2975
|
-
height: instance.height,
|
|
2976
3087
|
fps: instance.fps,
|
|
2977
3088
|
cells: instance.cells.map((cell) => ({
|
|
2978
3089
|
deviceId: cell.deviceId,
|
|
2979
3090
|
source: cell.source,
|
|
2980
3091
|
cell: cell.cell
|
|
2981
3092
|
})),
|
|
2982
|
-
profiles: (() => {
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
missingSources: [...plan.missingSources]
|
|
2989
|
-
}];
|
|
2990
|
-
})()
|
|
3093
|
+
profiles: (this.plansByInstanceId.get(instance.id) ?? []).map((row) => ({
|
|
3094
|
+
profile: row.profile,
|
|
3095
|
+
offered: row.offered,
|
|
3096
|
+
missingSources: row.offered ? [] : [...row.missingSources],
|
|
3097
|
+
canvas: row.offered ? `${String(row.canvas.width)}x${String(row.canvas.height)}` : ""
|
|
3098
|
+
}))
|
|
2991
3099
|
};
|
|
2992
3100
|
}
|
|
2993
3101
|
async saveGridLayout(input) {
|
|
@@ -2996,8 +3104,6 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2996
3104
|
const updated = {
|
|
2997
3105
|
...instance,
|
|
2998
3106
|
...input.name !== void 0 ? { name: input.name.trim() } : {},
|
|
2999
|
-
...input.width !== void 0 ? { width: input.width } : {},
|
|
3000
|
-
...input.height !== void 0 ? { height: input.height } : {},
|
|
3001
3107
|
...input.fps !== void 0 ? { fps: input.fps } : {},
|
|
3002
3108
|
cells: input.cells.map((cell) => ({ ...cell }))
|
|
3003
3109
|
};
|
|
@@ -3006,7 +3112,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
3006
3112
|
meta: {
|
|
3007
3113
|
cells: updated.cells.length,
|
|
3008
3114
|
sources: [...new Set(updated.cells.map((cell) => cell.deviceId))],
|
|
3009
|
-
|
|
3115
|
+
fps: updated.fps
|
|
3010
3116
|
}
|
|
3011
3117
|
});
|
|
3012
3118
|
await this.updateGlobalSettings({ cameraGrids: this.config.cameraGrids.map((row) => row["id"] === instance.id ? {
|
|
@@ -3343,4 +3449,4 @@ async function startGridChild(ctx, plan, sources, onEnded) {
|
|
|
3343
3449
|
};
|
|
3344
3450
|
}
|
|
3345
3451
|
//#endregion
|
|
3346
|
-
export { CameraGridAddon, CameraGridAddon as default, GRID_CAM_STREAM_ID,
|
|
3452
|
+
export { CameraGridAddon, CameraGridAddon as default, GRID_CAM_STREAM_ID, GRID_PROFILES, GRID_SILENCED_CAP_NAMES, GridCameraDevice, GridFrameSampler, GridLastFrameFileStore, GridSnapshotSource, GridStreamRelay, GridStreamSession, buildGridCameraDeclarations, buildGridFilterGraph, deriveGridCanvas, gridCamStreamId, gridCameraReconciliationIndex, gridPlanFor, gridProfilePlans, gridSnapshotAnswerFor, gridSourceDial, gridStreamDescriptor, gridStreamInvocation, installGridCameraRuntime, newGridCameraStableId, normalizeGridRows, parseGridStreamPath, readGridInstances, silenceAnalysisFor, startGridChild };
|