@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
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-DWNlc-PG.js");
|
|
7
7
|
let zod = require("zod");
|
|
8
8
|
let _camstack_types_node = require("@camstack/types/node");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -260,9 +260,17 @@ var GridInstanceSchema = zod.z.object({
|
|
|
260
260
|
cameraStableId: zod.z.string().min(1).max(256),
|
|
261
261
|
name: zod.z.string().min(1).max(160),
|
|
262
262
|
enabled: zod.z.boolean(),
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
/**
|
|
264
|
+
* NO canvas size. It is DERIVED, per profile, from the cells and the sources'
|
|
265
|
+
* own dimensions (`grid-canvas-derivation.ts`) — the two numbers that used to
|
|
266
|
+
* live here were typed into the editor and silently decided both how much the
|
|
267
|
+
* composite cost and how sharp it was.
|
|
268
|
+
*
|
|
269
|
+
* Rows written before that change still carry `width`/`height`; zod drops
|
|
270
|
+
* unknown keys, so they are ignored rather than migrated. Nothing reads them,
|
|
271
|
+
* and a migration that rewrote every row to delete two fields would be a
|
|
272
|
+
* write with no reader.
|
|
273
|
+
*/
|
|
266
274
|
fps: zod.z.number().int().min(1).max(60),
|
|
267
275
|
/**
|
|
268
276
|
* Empty is the NORMAL freshly-created state: a grid is created from the
|
|
@@ -325,10 +333,7 @@ function gridPlanFor(instance, encode) {
|
|
|
325
333
|
cell: cell.cell
|
|
326
334
|
};
|
|
327
335
|
});
|
|
328
|
-
const canvas = encode.canvas
|
|
329
|
-
width: instance.width,
|
|
330
|
-
height: instance.height
|
|
331
|
-
};
|
|
336
|
+
const canvas = encode.canvas;
|
|
332
337
|
return {
|
|
333
338
|
layout: {
|
|
334
339
|
width: canvas.width,
|
|
@@ -926,92 +931,239 @@ var GridLastFrameFileStore = class {
|
|
|
926
931
|
}
|
|
927
932
|
};
|
|
928
933
|
//#endregion
|
|
929
|
-
//#region src/builtins/camera-grid/grid-
|
|
934
|
+
//#region src/builtins/camera-grid/grid-canvas.ts
|
|
935
|
+
/** Bytes one yuv420p frame of this size occupies. */
|
|
936
|
+
function yuv420pSize(width, height) {
|
|
937
|
+
return width * height + 2 * (width / 2) * (height / 2);
|
|
938
|
+
}
|
|
930
939
|
/**
|
|
931
|
-
*
|
|
940
|
+
* Round DOWN to even.
|
|
932
941
|
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* detour and it is `high` again, so nothing an operator assigned is disturbed.
|
|
942
|
+
* Down and not nearest: a tile that rounded UP would claim a row of samples the
|
|
943
|
+
* decoder was never asked to produce, and the copy would either overrun or be
|
|
944
|
+
* refused at the moment the picture is wanted.
|
|
937
945
|
*/
|
|
938
|
-
|
|
946
|
+
function evenDown(value) {
|
|
947
|
+
return Math.max(0, Math.floor(value / 2) * 2);
|
|
948
|
+
}
|
|
949
|
+
var GridCanvas = class {
|
|
950
|
+
width;
|
|
951
|
+
height;
|
|
952
|
+
buffer;
|
|
953
|
+
lumaBytes;
|
|
954
|
+
chromaBytes;
|
|
955
|
+
constructor(width, height) {
|
|
956
|
+
this.width = width;
|
|
957
|
+
this.height = height;
|
|
958
|
+
this.lumaBytes = width * height;
|
|
959
|
+
this.chromaBytes = width / 2 * (height / 2);
|
|
960
|
+
this.buffer = Buffer.alloc(yuv420pSize(width, height));
|
|
961
|
+
this.clear();
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Legible black — Y 0, chroma 128 (neutral).
|
|
965
|
+
*
|
|
966
|
+
* `Buffer.alloc` already zeroes, and zeroed CHROMA is not black: it is a
|
|
967
|
+
* strong green. A cell nobody has filled yet has to look like an empty cell,
|
|
968
|
+
* not like a fault.
|
|
969
|
+
*/
|
|
970
|
+
clear() {
|
|
971
|
+
this.buffer.fill(0, 0, this.lumaBytes);
|
|
972
|
+
this.buffer.fill(128, this.lumaBytes);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Copy one decoded tile in at `origin`.
|
|
976
|
+
*
|
|
977
|
+
* Returns false — never throws, never writes a partial tile — when the tile
|
|
978
|
+
* does not fit, is the wrong size for the dimensions it claims, or sits on an
|
|
979
|
+
* odd boundary. This runs on the output tick, so a bad frame from one source
|
|
980
|
+
* must cost that source's rectangle and nothing else.
|
|
981
|
+
*/
|
|
982
|
+
blit(tile, size, origin) {
|
|
983
|
+
if (!isEven(size.width) || !isEven(size.height)) return false;
|
|
984
|
+
if (!isEven(origin.x) || !isEven(origin.y)) return false;
|
|
985
|
+
if (size.width <= 0 || size.height <= 0) return false;
|
|
986
|
+
if (origin.x < 0 || origin.y < 0) return false;
|
|
987
|
+
if (origin.x + size.width > this.width) return false;
|
|
988
|
+
if (origin.y + size.height > this.height) return false;
|
|
989
|
+
if (tile.length !== yuv420pSize(size.width, size.height)) return false;
|
|
990
|
+
for (let row = 0; row < size.height; row += 1) {
|
|
991
|
+
const from = row * size.width;
|
|
992
|
+
const to = (origin.y + row) * this.width + origin.x;
|
|
993
|
+
tile.copy(this.buffer, to, from, from + size.width);
|
|
994
|
+
}
|
|
995
|
+
const tileLuma = size.width * size.height;
|
|
996
|
+
const tileChroma = size.width / 2 * (size.height / 2);
|
|
997
|
+
const halfW = size.width / 2;
|
|
998
|
+
const halfH = size.height / 2;
|
|
999
|
+
const canvasHalfW = this.width / 2;
|
|
1000
|
+
for (let plane = 0; plane < 2; plane += 1) {
|
|
1001
|
+
const tileBase = tileLuma + plane * tileChroma;
|
|
1002
|
+
const canvasBase = this.lumaBytes + plane * this.chromaBytes;
|
|
1003
|
+
for (let row = 0; row < halfH; row += 1) {
|
|
1004
|
+
const from = tileBase + row * halfW;
|
|
1005
|
+
const to = canvasBase + (origin.y / 2 + row) * canvasHalfW + origin.x / 2;
|
|
1006
|
+
tile.copy(this.buffer, to, from, from + halfW);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
function isEven(value) {
|
|
1013
|
+
return Number.isInteger(value) && value % 2 === 0;
|
|
1014
|
+
}
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/builtins/camera-grid/grid-canvas-derivation.ts
|
|
939
1017
|
/**
|
|
940
|
-
* The
|
|
1018
|
+
* The canvas is a RESULT, not a field.
|
|
1019
|
+
*
|
|
1020
|
+
* ## Why the operator stopped typing it
|
|
1021
|
+
*
|
|
1022
|
+
* A grid's resolution used to be two numbers in the editor, beside FPS. They
|
|
1023
|
+
* looked harmless and were not: that number silently decided how much the
|
|
1024
|
+
* composite cost and how sharp it was. An operator typing 1920x1080 was, without
|
|
1025
|
+
* being told, committing the hub to reading two 4 MP streams — or, with the
|
|
1026
|
+
* rule before it, to upscaling a 640x360 sub-stream by 3x. Neither is a thing
|
|
1027
|
+
* anyone can be expected to work out from a text field.
|
|
1028
|
+
*
|
|
1029
|
+
* ## What it is derived from
|
|
1030
|
+
*
|
|
1031
|
+
* The operator draws two rectangles per camera — which part of the source to
|
|
1032
|
+
* take, and where it lands — and both are fractions, so nothing is pixels yet.
|
|
1033
|
+
* Exactly one number is missing: how many pixels the whole canvas is worth.
|
|
941
1034
|
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
1035
|
+
* A cell has `sourceResolution x crop` REAL pixels behind it. Showing them 1:1
|
|
1036
|
+
* neither invents nor discards anything, so the cell should be exactly that
|
|
1037
|
+
* many pixels — and since the cell is a fraction `f` of the canvas, the canvas
|
|
1038
|
+
* it implies is `real / f`. Each cell proposes one, and the LARGEST wins: the
|
|
1039
|
+
* hungriest cell is served 1:1 and the others scale down by a hair, never up.
|
|
1040
|
+
*
|
|
1041
|
+
* Gaps change nothing. Cells need not cover the canvas, and an uncovered part
|
|
1042
|
+
* is background — it has no pixels to propose.
|
|
1043
|
+
*
|
|
1044
|
+
* ## One per profile
|
|
1045
|
+
*
|
|
1046
|
+
* A source is a different size at each profile, so each profile derives its own
|
|
1047
|
+
* canvas: a grid's `low` composes its sources' `low` onto the canvas their
|
|
1048
|
+
* `low` implies. That is what makes the three outputs 1:1 with what they are
|
|
1049
|
+
* made of, and what lets a viewer's adaptive pick a rung without the grid
|
|
1050
|
+
* needing to know it exists.
|
|
947
1051
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1052
|
+
/**
|
|
1053
|
+
* What an encoder will take. Not a taste: `libx264` refuses beyond 8192 in
|
|
1054
|
+
* either axis, and a canvas that large is a composition nobody can decode
|
|
1055
|
+
* anyway. Reached only by a cell that shows a sliver of a source at full
|
|
1056
|
+
* canvas width, which is a layout, not a mistake — so it is CLAMPED rather
|
|
1057
|
+
* than refused, and clamped in both axes together so the picture the operator
|
|
1058
|
+
* drew keeps its shape.
|
|
1059
|
+
*/
|
|
1060
|
+
var MAX_CANVAS_WIDTH = 7680;
|
|
1061
|
+
var MAX_CANVAS_HEIGHT = 4320;
|
|
1062
|
+
/** Below this a composition is not a cheaper picture, it is no picture. */
|
|
1063
|
+
var MIN_CANVAS_EDGE = 160;
|
|
1064
|
+
/**
|
|
1065
|
+
* The canvas these cells imply, or `null` when they imply none.
|
|
1066
|
+
*
|
|
1067
|
+
* `null` for a grid with no cells — the normal freshly-created state — and for
|
|
1068
|
+
* a layout whose every rectangle is degenerate. Both are "there is no picture
|
|
1069
|
+
* to size", which the caller turns into "this profile is not on offer" rather
|
|
1070
|
+
* than into a guess.
|
|
1071
|
+
*/
|
|
1072
|
+
function deriveGridCanvas(cells) {
|
|
1073
|
+
let width = 0;
|
|
1074
|
+
let height = 0;
|
|
1075
|
+
for (const entry of cells) {
|
|
1076
|
+
if (entry.cell.width <= 0 || entry.cell.height <= 0) continue;
|
|
1077
|
+
if (entry.sourceSize.width <= 0 || entry.sourceSize.height <= 0) continue;
|
|
1078
|
+
const realWidth = entry.sourceSize.width * entry.source.width;
|
|
1079
|
+
const realHeight = entry.sourceSize.height * entry.source.height;
|
|
1080
|
+
if (realWidth <= 0 || realHeight <= 0) continue;
|
|
1081
|
+
width = Math.max(width, realWidth / entry.cell.width);
|
|
1082
|
+
height = Math.max(height, realHeight / entry.cell.height);
|
|
1083
|
+
}
|
|
1084
|
+
if (width <= 0 || height <= 0) return null;
|
|
1085
|
+
const overshoot = Math.max(width / MAX_CANVAS_WIDTH, height / MAX_CANVAS_HEIGHT, 1);
|
|
1086
|
+
const factor = Math.max(MIN_CANVAS_EDGE / width, MIN_CANVAS_EDGE / height, 1) / overshoot;
|
|
965
1087
|
return {
|
|
966
|
-
width:
|
|
967
|
-
height:
|
|
1088
|
+
width: Math.max(2, evenDown(width * factor)),
|
|
1089
|
+
height: Math.max(2, evenDown(height * factor))
|
|
968
1090
|
};
|
|
969
1091
|
}
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/builtins/camera-grid/grid-profiles.ts
|
|
970
1094
|
/**
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
* Soft beats absent: a camera whose best stream cannot fill its cell still
|
|
974
|
-
* composes, at the best it has. Refusing would take the whole grid down over
|
|
975
|
-
* one cell being drawn larger than its source.
|
|
1095
|
+
* One id per profile, for every grid, forever.
|
|
976
1096
|
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
1097
|
+
* D519's reason is what keeps `high`'s id bare: a newly-spelled id mints a
|
|
1098
|
+
* second cam-stream and orphans whatever profile assignment the operator had
|
|
1099
|
+
* made. `grid` was `high` on the live hub before any of this and still is.
|
|
980
1100
|
*/
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1101
|
+
var GRID_CAM_STREAM_ID = "grid";
|
|
1102
|
+
/** Every profile a grid can compose, in descending quality. */
|
|
1103
|
+
var GRID_PROFILES = [
|
|
1104
|
+
"high",
|
|
1105
|
+
"mid",
|
|
1106
|
+
"low"
|
|
1107
|
+
];
|
|
1108
|
+
/** The cam-stream id a grid publishes for one profile. */
|
|
1109
|
+
function gridCamStreamId(profile) {
|
|
1110
|
+
return profile === "high" ? GRID_CAM_STREAM_ID : `${GRID_CAM_STREAM_ID}-${profile}`;
|
|
990
1111
|
}
|
|
991
|
-
/**
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* What this grid can honestly publish, per profile, with the canvas or the
|
|
1114
|
+
* refusal.
|
|
1115
|
+
*
|
|
1116
|
+
* Every profile gets a row whether it is offered or not: an absent row says
|
|
1117
|
+
* nothing, and the caller logging a refusal needs the reason more than the
|
|
1118
|
+
* caller publishing an offer needs the offer.
|
|
1119
|
+
*/
|
|
1120
|
+
function gridProfilePlans(input) {
|
|
1121
|
+
const sourceDeviceIds = [...new Set(input.cells.map((cell) => cell.deviceId))];
|
|
1122
|
+
const servedBy = /* @__PURE__ */ new Map();
|
|
1123
|
+
for (const deviceId of sourceDeviceIds) servedBy.set(deviceId, input.profilesServedBy(deviceId));
|
|
1124
|
+
return GRID_PROFILES.map((profile) => {
|
|
1125
|
+
if (sourceDeviceIds.length === 0) return {
|
|
1126
|
+
profile,
|
|
1127
|
+
offered: false,
|
|
1128
|
+
missingSources: []
|
|
1129
|
+
};
|
|
1130
|
+
const sizeBySource = /* @__PURE__ */ new Map();
|
|
1131
|
+
const missingSources = [];
|
|
1132
|
+
for (const deviceId of sourceDeviceIds) {
|
|
1133
|
+
const match = (servedBy.get(deviceId) ?? null)?.find((entry) => entry.profile === profile);
|
|
1134
|
+
if (match === void 0 || match.width <= 0 || match.height <= 0) {
|
|
1135
|
+
missingSources.push(deviceId);
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
sizeBySource.set(deviceId, {
|
|
1139
|
+
width: match.width,
|
|
1140
|
+
height: match.height
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
if (missingSources.length > 0) return {
|
|
1144
|
+
profile,
|
|
1145
|
+
offered: false,
|
|
1146
|
+
missingSources
|
|
1147
|
+
};
|
|
1148
|
+
const canvas = deriveGridCanvas(input.cells.map((cell) => ({
|
|
1149
|
+
source: cell.source,
|
|
1150
|
+
cell: cell.cell,
|
|
1151
|
+
sourceSize: sizeBySource.get(cell.deviceId) ?? {
|
|
1152
|
+
width: 0,
|
|
1153
|
+
height: 0
|
|
1154
|
+
}
|
|
1155
|
+
})));
|
|
1156
|
+
if (canvas === null) return {
|
|
1157
|
+
profile,
|
|
1158
|
+
offered: false,
|
|
1159
|
+
missingSources: []
|
|
1160
|
+
};
|
|
1161
|
+
return {
|
|
1162
|
+
profile,
|
|
1163
|
+
offered: true,
|
|
1164
|
+
canvas
|
|
1165
|
+
};
|
|
1166
|
+
});
|
|
1015
1167
|
}
|
|
1016
1168
|
//#endregion
|
|
1017
1169
|
//#region src/builtins/camera-grid/grid-sentry-sources.ts
|
|
@@ -1176,15 +1328,24 @@ var PATH_PREFIX = "/grid/";
|
|
|
1176
1328
|
var PATH_SUFFIX = GRID_WIRE_PATH_SUFFIX;
|
|
1177
1329
|
/** How long a reader may refuse to drain before it is cut. */
|
|
1178
1330
|
var SLOW_READER_MS = 5e3;
|
|
1179
|
-
|
|
1331
|
+
function asProfile(raw) {
|
|
1332
|
+
return GRID_PROFILES.find((profile) => profile === raw) ?? null;
|
|
1333
|
+
}
|
|
1334
|
+
/** `/grid/<instanceId>/<profile>.flv`, or `null` when it is anything else. */
|
|
1180
1335
|
function parseGridStreamPath(url) {
|
|
1181
1336
|
const path = (url ?? "").split("?")[0] ?? "";
|
|
1182
1337
|
if (!path.startsWith(PATH_PREFIX) || !path.endsWith(PATH_SUFFIX)) return null;
|
|
1183
1338
|
const raw = path.slice(6, path.length - PATH_SUFFIX.length);
|
|
1184
|
-
|
|
1339
|
+
const at = raw.lastIndexOf("/");
|
|
1340
|
+
if (at <= 0) return null;
|
|
1341
|
+
const profile = asProfile(raw.slice(at + 1));
|
|
1342
|
+
if (profile === null) return null;
|
|
1185
1343
|
try {
|
|
1186
|
-
const instanceId = decodeURIComponent(raw);
|
|
1187
|
-
return instanceId.length > 0 ? {
|
|
1344
|
+
const instanceId = decodeURIComponent(raw.slice(0, at));
|
|
1345
|
+
return instanceId.length > 0 ? {
|
|
1346
|
+
instanceId,
|
|
1347
|
+
profile
|
|
1348
|
+
} : null;
|
|
1188
1349
|
} catch {
|
|
1189
1350
|
return null;
|
|
1190
1351
|
}
|
|
@@ -1223,13 +1384,16 @@ var GridStreamRelay = class {
|
|
|
1223
1384
|
streamUrl(request) {
|
|
1224
1385
|
if (!this.baseUrl) throw new Error("camera-grid: the stream relay is not started");
|
|
1225
1386
|
const instance = encodeURIComponent(request.instanceId);
|
|
1226
|
-
return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
|
|
1387
|
+
return `${this.baseUrl}${PATH_PREFIX}${instance}/${request.profile}${PATH_SUFFIX}`;
|
|
1227
1388
|
}
|
|
1228
1389
|
async serve(request, res) {
|
|
1229
|
-
const { instanceId } = request;
|
|
1390
|
+
const { instanceId, profile } = request;
|
|
1230
1391
|
const session = this.deps.sessionFor(request);
|
|
1231
1392
|
if (session === null) {
|
|
1232
|
-
this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
|
|
1393
|
+
this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
|
|
1394
|
+
instanceId,
|
|
1395
|
+
profile
|
|
1396
|
+
} });
|
|
1233
1397
|
res.writeHead(404).end();
|
|
1234
1398
|
return;
|
|
1235
1399
|
}
|
|
@@ -1263,6 +1427,7 @@ var GridStreamRelay = class {
|
|
|
1263
1427
|
tags: { deviceId: session.deviceId },
|
|
1264
1428
|
meta: {
|
|
1265
1429
|
instanceId,
|
|
1430
|
+
profile,
|
|
1266
1431
|
stalledForMs: SLOW_READER_MS
|
|
1267
1432
|
}
|
|
1268
1433
|
});
|
|
@@ -1275,6 +1440,7 @@ var GridStreamRelay = class {
|
|
|
1275
1440
|
tags: { deviceId: session.deviceId },
|
|
1276
1441
|
meta: {
|
|
1277
1442
|
instanceId,
|
|
1443
|
+
profile,
|
|
1278
1444
|
reason
|
|
1279
1445
|
}
|
|
1280
1446
|
});
|
|
@@ -1287,6 +1453,7 @@ var GridStreamRelay = class {
|
|
|
1287
1453
|
tags: { deviceId: session.deviceId },
|
|
1288
1454
|
meta: {
|
|
1289
1455
|
instanceId,
|
|
1456
|
+
profile,
|
|
1290
1457
|
error: require_dist.errMsg(error)
|
|
1291
1458
|
}
|
|
1292
1459
|
});
|
|
@@ -1322,13 +1489,13 @@ var GridStreamRelay = class {
|
|
|
1322
1489
|
* scrub never reads. The Dreame robot's one 720p relay landed exactly there and
|
|
1323
1490
|
* its footage was written nowhere anything looked.
|
|
1324
1491
|
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1492
|
+
* With several compositions published the hint does a second job: it says which
|
|
1493
|
+
* slot each one is FOR. Ranked by pixels a grid's `mid` would take the `high`
|
|
1494
|
+
* slot on any grid whose `high` was not offered.
|
|
1328
1495
|
*/
|
|
1329
1496
|
function gridStreamDescriptor(input) {
|
|
1330
1497
|
return {
|
|
1331
|
-
camStreamId:
|
|
1498
|
+
camStreamId: gridCamStreamId(input.profile),
|
|
1332
1499
|
kind: GRID_WIRE_STREAM_KIND,
|
|
1333
1500
|
url: input.url,
|
|
1334
1501
|
codec: "h264",
|
|
@@ -1337,8 +1504,8 @@ function gridStreamDescriptor(input) {
|
|
|
1337
1504
|
height: input.height
|
|
1338
1505
|
},
|
|
1339
1506
|
fps: input.fps,
|
|
1340
|
-
label:
|
|
1341
|
-
profileHint:
|
|
1507
|
+
label: `Grid composition (${input.profile})`,
|
|
1508
|
+
profileHint: input.profile,
|
|
1342
1509
|
autoEligible: true
|
|
1343
1510
|
};
|
|
1344
1511
|
}
|
|
@@ -1841,88 +2008,6 @@ async function silenceAnalysisFor(deps, deviceId) {
|
|
|
1841
2008
|
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("; ")})`);
|
|
1842
2009
|
}
|
|
1843
2010
|
//#endregion
|
|
1844
|
-
//#region src/builtins/camera-grid/grid-canvas.ts
|
|
1845
|
-
/** Bytes one yuv420p frame of this size occupies. */
|
|
1846
|
-
function yuv420pSize(width, height) {
|
|
1847
|
-
return width * height + 2 * (width / 2) * (height / 2);
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* Round DOWN to even.
|
|
1851
|
-
*
|
|
1852
|
-
* Down and not nearest: a tile that rounded UP would claim a row of samples the
|
|
1853
|
-
* decoder was never asked to produce, and the copy would either overrun or be
|
|
1854
|
-
* refused at the moment the picture is wanted.
|
|
1855
|
-
*/
|
|
1856
|
-
function evenDown(value) {
|
|
1857
|
-
return Math.max(0, Math.floor(value / 2) * 2);
|
|
1858
|
-
}
|
|
1859
|
-
var GridCanvas = class {
|
|
1860
|
-
width;
|
|
1861
|
-
height;
|
|
1862
|
-
buffer;
|
|
1863
|
-
lumaBytes;
|
|
1864
|
-
chromaBytes;
|
|
1865
|
-
constructor(width, height) {
|
|
1866
|
-
this.width = width;
|
|
1867
|
-
this.height = height;
|
|
1868
|
-
this.lumaBytes = width * height;
|
|
1869
|
-
this.chromaBytes = width / 2 * (height / 2);
|
|
1870
|
-
this.buffer = Buffer.alloc(yuv420pSize(width, height));
|
|
1871
|
-
this.clear();
|
|
1872
|
-
}
|
|
1873
|
-
/**
|
|
1874
|
-
* Legible black — Y 0, chroma 128 (neutral).
|
|
1875
|
-
*
|
|
1876
|
-
* `Buffer.alloc` already zeroes, and zeroed CHROMA is not black: it is a
|
|
1877
|
-
* strong green. A cell nobody has filled yet has to look like an empty cell,
|
|
1878
|
-
* not like a fault.
|
|
1879
|
-
*/
|
|
1880
|
-
clear() {
|
|
1881
|
-
this.buffer.fill(0, 0, this.lumaBytes);
|
|
1882
|
-
this.buffer.fill(128, this.lumaBytes);
|
|
1883
|
-
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Copy one decoded tile in at `origin`.
|
|
1886
|
-
*
|
|
1887
|
-
* Returns false — never throws, never writes a partial tile — when the tile
|
|
1888
|
-
* does not fit, is the wrong size for the dimensions it claims, or sits on an
|
|
1889
|
-
* odd boundary. This runs on the output tick, so a bad frame from one source
|
|
1890
|
-
* must cost that source's rectangle and nothing else.
|
|
1891
|
-
*/
|
|
1892
|
-
blit(tile, size, origin) {
|
|
1893
|
-
if (!isEven(size.width) || !isEven(size.height)) return false;
|
|
1894
|
-
if (!isEven(origin.x) || !isEven(origin.y)) return false;
|
|
1895
|
-
if (size.width <= 0 || size.height <= 0) return false;
|
|
1896
|
-
if (origin.x < 0 || origin.y < 0) return false;
|
|
1897
|
-
if (origin.x + size.width > this.width) return false;
|
|
1898
|
-
if (origin.y + size.height > this.height) return false;
|
|
1899
|
-
if (tile.length !== yuv420pSize(size.width, size.height)) return false;
|
|
1900
|
-
for (let row = 0; row < size.height; row += 1) {
|
|
1901
|
-
const from = row * size.width;
|
|
1902
|
-
const to = (origin.y + row) * this.width + origin.x;
|
|
1903
|
-
tile.copy(this.buffer, to, from, from + size.width);
|
|
1904
|
-
}
|
|
1905
|
-
const tileLuma = size.width * size.height;
|
|
1906
|
-
const tileChroma = size.width / 2 * (size.height / 2);
|
|
1907
|
-
const halfW = size.width / 2;
|
|
1908
|
-
const halfH = size.height / 2;
|
|
1909
|
-
const canvasHalfW = this.width / 2;
|
|
1910
|
-
for (let plane = 0; plane < 2; plane += 1) {
|
|
1911
|
-
const tileBase = tileLuma + plane * tileChroma;
|
|
1912
|
-
const canvasBase = this.lumaBytes + plane * this.chromaBytes;
|
|
1913
|
-
for (let row = 0; row < halfH; row += 1) {
|
|
1914
|
-
const from = tileBase + row * halfW;
|
|
1915
|
-
const to = canvasBase + (origin.y / 2 + row) * canvasHalfW + origin.x / 2;
|
|
1916
|
-
tile.copy(this.buffer, to, from, from + halfW);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
return true;
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
function isEven(value) {
|
|
1923
|
-
return Number.isInteger(value) && value % 2 === 0;
|
|
1924
|
-
}
|
|
1925
|
-
//#endregion
|
|
1926
2011
|
//#region src/builtins/camera-grid/grid-tile-feed.ts
|
|
1927
2012
|
/**
|
|
1928
2013
|
* One source's decoded tiles, of which only the LATEST is ever wanted.
|
|
@@ -2424,18 +2509,26 @@ var DEFAULTS = {
|
|
|
2424
2509
|
cameraGrids: [],
|
|
2425
2510
|
cameraGridTombstones: []
|
|
2426
2511
|
};
|
|
2512
|
+
/** One composition per (grid, profile) — a grid composes each separately. */
|
|
2513
|
+
function sessionKey(instanceId, profile) {
|
|
2514
|
+
return `${instanceId}::${profile}`;
|
|
2515
|
+
}
|
|
2427
2516
|
/**
|
|
2428
|
-
* One grid's composition
|
|
2517
|
+
* One grid's composition of one profile.
|
|
2429
2518
|
*
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2519
|
+
* The profile is back in the key after a spell with a single output. What made
|
|
2520
|
+
* the per-profile shape painful was never the count: it was that a rebuild
|
|
2521
|
+
* meant a blank screen until the slowest source produced a key frame. An output
|
|
2522
|
+
* clock (D530) shows the picture at once and fills the cells in, so a rung the
|
|
2523
|
+
* adaptive can stand on costs what it is worth.
|
|
2433
2524
|
*/
|
|
2434
2525
|
var GridSessionEntry = class {
|
|
2435
2526
|
instanceId;
|
|
2527
|
+
profile;
|
|
2436
2528
|
session;
|
|
2437
|
-
constructor(instanceId, session) {
|
|
2529
|
+
constructor(instanceId, profile, session) {
|
|
2438
2530
|
this.instanceId = instanceId;
|
|
2531
|
+
this.profile = profile;
|
|
2439
2532
|
this.session = session;
|
|
2440
2533
|
}
|
|
2441
2534
|
isRunning() {
|
|
@@ -2459,7 +2552,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2459
2552
|
* (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
|
|
2460
2553
|
* — that only changes when an operator changes it.
|
|
2461
2554
|
*/
|
|
2462
|
-
|
|
2555
|
+
plansByInstanceId = /* @__PURE__ */ new Map();
|
|
2463
2556
|
activity = null;
|
|
2464
2557
|
snapshots = null;
|
|
2465
2558
|
frameSampler = null;
|
|
@@ -2692,9 +2785,9 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2692
2785
|
if (instance.cells.filter((cell) => cell.deviceId === sourceDeviceId).length === 0) continue;
|
|
2693
2786
|
targets.push({
|
|
2694
2787
|
deviceId,
|
|
2695
|
-
canvas: {
|
|
2696
|
-
width:
|
|
2697
|
-
height:
|
|
2788
|
+
canvas: this.canvasFor(instanceId) ?? {
|
|
2789
|
+
width: 0,
|
|
2790
|
+
height: 0
|
|
2698
2791
|
},
|
|
2699
2792
|
cellsForSource: (id) => instance.cells.filter((cell) => cell.deviceId === id).map((cell) => ({
|
|
2700
2793
|
source: cell.source,
|
|
@@ -2737,11 +2830,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2737
2830
|
servedBy.set(slot.deviceId, profiles);
|
|
2738
2831
|
}
|
|
2739
2832
|
for (const instance of instances) {
|
|
2740
|
-
const
|
|
2741
|
-
canvas: {
|
|
2742
|
-
width: instance.width,
|
|
2743
|
-
height: instance.height
|
|
2744
|
-
},
|
|
2833
|
+
const plans = gridProfilePlans({
|
|
2745
2834
|
cells: instance.cells.map((cell) => ({
|
|
2746
2835
|
deviceId: cell.deviceId,
|
|
2747
2836
|
source: cell.source,
|
|
@@ -2749,9 +2838,9 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2749
2838
|
})),
|
|
2750
2839
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
2751
2840
|
});
|
|
2752
|
-
const previous = this.
|
|
2753
|
-
this.
|
|
2754
|
-
this.logSourcePlan(instance,
|
|
2841
|
+
const previous = this.plansByInstanceId.get(instance.id);
|
|
2842
|
+
this.plansByInstanceId.set(instance.id, plans);
|
|
2843
|
+
this.logSourcePlan(instance, plans, previous);
|
|
2755
2844
|
}
|
|
2756
2845
|
}
|
|
2757
2846
|
/**
|
|
@@ -2767,48 +2856,65 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2767
2856
|
* Logged on the EDGE: a grid that cannot be composed would otherwise write a
|
|
2768
2857
|
* line a minute for ever.
|
|
2769
2858
|
*/
|
|
2770
|
-
logSourcePlan(instance,
|
|
2771
|
-
const shape = (value) => JSON.stringify(
|
|
2772
|
-
|
|
2773
|
-
[...value.profileBySource],
|
|
2774
|
-
value.missingSources
|
|
2775
|
-
]);
|
|
2776
|
-
if (previous !== void 0 && shape(previous) === shape(plan)) return;
|
|
2859
|
+
logSourcePlan(instance, plans, previous) {
|
|
2860
|
+
const shape = (value) => JSON.stringify(value);
|
|
2861
|
+
if (previous !== void 0 && shape(previous) === shape(plans)) return;
|
|
2777
2862
|
const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2863
|
+
for (const row of plans) {
|
|
2864
|
+
if (row.offered || row.missingSources.length === 0) continue;
|
|
2865
|
+
this.ctx.logger.info("camera grid: a profile is NOT on offer", {
|
|
2866
|
+
tags: { deviceId },
|
|
2867
|
+
meta: {
|
|
2868
|
+
instanceId: instance.id,
|
|
2869
|
+
profile: row.profile,
|
|
2870
|
+
missingSources: [...row.missingSources]
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
this.ctx.logger.info("camera grid: published profiles", {
|
|
2786
2875
|
tags: { deviceId },
|
|
2787
2876
|
meta: {
|
|
2788
2877
|
instanceId: instance.id,
|
|
2789
|
-
offered:
|
|
2790
|
-
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
2878
|
+
offered: Object.fromEntries(plans.filter((row) => row.offered).map((row) => [row.profile, row.offered ? `${String(row.canvas.width)}x${String(row.canvas.height)}` : ""]))
|
|
2791
2879
|
}
|
|
2792
2880
|
});
|
|
2793
2881
|
}
|
|
2794
|
-
|
|
2795
|
-
return this.
|
|
2882
|
+
plansFor(instanceId) {
|
|
2883
|
+
return this.plansByInstanceId.get(instanceId) ?? null;
|
|
2884
|
+
}
|
|
2885
|
+
planFor(instanceId, profile) {
|
|
2886
|
+
return this.plansFor(instanceId)?.find((row) => row.profile === profile) ?? null;
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* The canvas of the BEST profile this grid offers.
|
|
2890
|
+
*
|
|
2891
|
+
* Used where a single geometry is needed for something that is not a
|
|
2892
|
+
* composition — the detection boxes a grid forwards (`grid-activity`), which
|
|
2893
|
+
* are normalized against whatever picture the viewer is looking at. The best
|
|
2894
|
+
* offered one is the honest default; a grid offering nothing has no geometry
|
|
2895
|
+
* and says so.
|
|
2896
|
+
*/
|
|
2897
|
+
canvasFor(instanceId) {
|
|
2898
|
+
for (const row of this.plansFor(instanceId) ?? []) if (row.offered) return row.canvas;
|
|
2899
|
+
return null;
|
|
2796
2900
|
}
|
|
2797
2901
|
ensureSessions(instanceId, deviceId) {
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
const
|
|
2801
|
-
if (
|
|
2802
|
-
this.sessions.
|
|
2803
|
-
stale
|
|
2902
|
+
for (const profile of GRID_PROFILES) {
|
|
2903
|
+
const key = sessionKey(instanceId, profile);
|
|
2904
|
+
const plan = this.planFor(instanceId, profile);
|
|
2905
|
+
if (plan === null || !plan.offered) {
|
|
2906
|
+
const stale = this.sessions.get(key);
|
|
2907
|
+
if (stale) {
|
|
2908
|
+
this.sessions.delete(key);
|
|
2909
|
+
stale.session.dispose();
|
|
2910
|
+
}
|
|
2911
|
+
continue;
|
|
2804
2912
|
}
|
|
2805
|
-
|
|
2913
|
+
if (this.sessions.has(key)) continue;
|
|
2914
|
+
this.sessions.set(key, new GridSessionEntry(instanceId, profile, this.makeSession(instanceId, deviceId, profile)));
|
|
2806
2915
|
}
|
|
2807
|
-
if (this.sessions.has(instanceId)) return;
|
|
2808
|
-
const session = this.makeSession(instanceId, deviceId);
|
|
2809
|
-
this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
|
|
2810
2916
|
}
|
|
2811
|
-
makeSession(instanceId, deviceId) {
|
|
2917
|
+
makeSession(instanceId, deviceId, profile) {
|
|
2812
2918
|
const childContext = {
|
|
2813
2919
|
binaryPath: this.ffmpegBinaryPath,
|
|
2814
2920
|
logger: this.ctx.logger,
|
|
@@ -2819,8 +2925,6 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2819
2925
|
deps: {
|
|
2820
2926
|
logger: this.ctx.logger,
|
|
2821
2927
|
acquireSource: async (sourceDeviceId) => {
|
|
2822
|
-
const profile = this.planFor(instanceId)?.profileBySource.get(sourceDeviceId);
|
|
2823
|
-
if (profile === void 0) throw new Error(`camera-grid: no stream is assigned for source ${String(sourceDeviceId)}`);
|
|
2824
2928
|
const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
|
|
2825
2929
|
deviceId: sourceDeviceId,
|
|
2826
2930
|
video: "copy",
|
|
@@ -2867,10 +2971,12 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2867
2971
|
},
|
|
2868
2972
|
resolvePlan: () => {
|
|
2869
2973
|
const instance = this.instanceById(instanceId);
|
|
2870
|
-
|
|
2974
|
+
const plan = this.planFor(instanceId, profile);
|
|
2975
|
+
if (!instance || plan === null || !plan.offered) return null;
|
|
2871
2976
|
return gridPlanFor(instance, {
|
|
2872
2977
|
encoder: "libx264",
|
|
2873
|
-
decodeHwAccel: null
|
|
2978
|
+
decodeHwAccel: null,
|
|
2979
|
+
canvas: plan.canvas
|
|
2874
2980
|
});
|
|
2875
2981
|
}
|
|
2876
2982
|
});
|
|
@@ -2878,14 +2984,18 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2878
2984
|
}
|
|
2879
2985
|
/** The session a dial is asking for, or `null` - which the relay answers 404 to. */
|
|
2880
2986
|
sessionFor(request) {
|
|
2881
|
-
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
2987
|
+
return this.sessions.get(sessionKey(request.instanceId, request.profile))?.session ?? null;
|
|
2882
2988
|
}
|
|
2883
|
-
/** This grid's
|
|
2989
|
+
/** This grid's sessions, in DESCENDING quality — the snapshot takes the first running one. */
|
|
2884
2990
|
sessionsForDevice(deviceId) {
|
|
2885
2991
|
const instanceId = this.instanceIdForDevice(deviceId);
|
|
2886
2992
|
if (instanceId === null) return [];
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2993
|
+
const entries = [];
|
|
2994
|
+
for (const profile of GRID_PROFILES) {
|
|
2995
|
+
const entry = this.sessions.get(sessionKey(instanceId, profile));
|
|
2996
|
+
if (entry) entries.push(entry);
|
|
2997
|
+
}
|
|
2998
|
+
return entries;
|
|
2889
2999
|
}
|
|
2890
3000
|
/** A grid that is gone stops immediately - it must not keep N sources dialled. */
|
|
2891
3001
|
async retireWithdrawnSessions(instances) {
|
|
@@ -2903,7 +3013,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2903
3013
|
if (live.has(instanceId)) continue;
|
|
2904
3014
|
this.activity?.forget(this.deviceIdByInstanceId.get(instanceId) ?? 0);
|
|
2905
3015
|
this.deviceIdByInstanceId.delete(instanceId);
|
|
2906
|
-
this.
|
|
3016
|
+
this.plansByInstanceId.delete(instanceId);
|
|
2907
3017
|
}
|
|
2908
3018
|
}
|
|
2909
3019
|
/**
|
|
@@ -2951,15 +3061,18 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2951
3061
|
descriptorsFor(instanceId) {
|
|
2952
3062
|
const instance = this.instanceById(instanceId);
|
|
2953
3063
|
const relay = this.relay;
|
|
2954
|
-
const
|
|
2955
|
-
if (!instance || !relay ||
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
width:
|
|
2959
|
-
height:
|
|
3064
|
+
const plans = this.plansFor(instanceId);
|
|
3065
|
+
if (!instance || !relay || plans === null) return null;
|
|
3066
|
+
return plans.filter((row) => row.offered).map((row) => gridStreamDescriptor({
|
|
3067
|
+
profile: row.profile,
|
|
3068
|
+
width: row.offered ? row.canvas.width : 0,
|
|
3069
|
+
height: row.offered ? row.canvas.height : 0,
|
|
2960
3070
|
fps: instance.fps,
|
|
2961
|
-
url: relay.streamUrl({
|
|
2962
|
-
|
|
3071
|
+
url: relay.streamUrl({
|
|
3072
|
+
instanceId,
|
|
3073
|
+
profile: row.profile
|
|
3074
|
+
})
|
|
3075
|
+
}));
|
|
2963
3076
|
}
|
|
2964
3077
|
instanceIdForDevice(deviceId) {
|
|
2965
3078
|
for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
|
|
@@ -2976,23 +3089,18 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2976
3089
|
instanceId: instance.id,
|
|
2977
3090
|
deviceId,
|
|
2978
3091
|
name: instance.name,
|
|
2979
|
-
width: instance.width,
|
|
2980
|
-
height: instance.height,
|
|
2981
3092
|
fps: instance.fps,
|
|
2982
3093
|
cells: instance.cells.map((cell) => ({
|
|
2983
3094
|
deviceId: cell.deviceId,
|
|
2984
3095
|
source: cell.source,
|
|
2985
3096
|
cell: cell.cell
|
|
2986
3097
|
})),
|
|
2987
|
-
profiles: (() => {
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
missingSources: [...plan.missingSources]
|
|
2994
|
-
}];
|
|
2995
|
-
})()
|
|
3098
|
+
profiles: (this.plansByInstanceId.get(instance.id) ?? []).map((row) => ({
|
|
3099
|
+
profile: row.profile,
|
|
3100
|
+
offered: row.offered,
|
|
3101
|
+
missingSources: row.offered ? [] : [...row.missingSources],
|
|
3102
|
+
canvas: row.offered ? `${String(row.canvas.width)}x${String(row.canvas.height)}` : ""
|
|
3103
|
+
}))
|
|
2996
3104
|
};
|
|
2997
3105
|
}
|
|
2998
3106
|
async saveGridLayout(input) {
|
|
@@ -3001,8 +3109,6 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
3001
3109
|
const updated = {
|
|
3002
3110
|
...instance,
|
|
3003
3111
|
...input.name !== void 0 ? { name: input.name.trim() } : {},
|
|
3004
|
-
...input.width !== void 0 ? { width: input.width } : {},
|
|
3005
|
-
...input.height !== void 0 ? { height: input.height } : {},
|
|
3006
3112
|
...input.fps !== void 0 ? { fps: input.fps } : {},
|
|
3007
3113
|
cells: input.cells.map((cell) => ({ ...cell }))
|
|
3008
3114
|
};
|
|
@@ -3011,7 +3117,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
3011
3117
|
meta: {
|
|
3012
3118
|
cells: updated.cells.length,
|
|
3013
3119
|
sources: [...new Set(updated.cells.map((cell) => cell.deviceId))],
|
|
3014
|
-
|
|
3120
|
+
fps: updated.fps
|
|
3015
3121
|
}
|
|
3016
3122
|
});
|
|
3017
3123
|
await this.updateGlobalSettings({ cameraGrids: this.config.cameraGrids.map((row) => row["id"] === instance.id ? {
|
|
@@ -3350,7 +3456,7 @@ async function startGridChild(ctx, plan, sources, onEnded) {
|
|
|
3350
3456
|
//#endregion
|
|
3351
3457
|
exports.CameraGridAddon = CameraGridAddon;
|
|
3352
3458
|
exports.GRID_CAM_STREAM_ID = GRID_CAM_STREAM_ID;
|
|
3353
|
-
exports.
|
|
3459
|
+
exports.GRID_PROFILES = GRID_PROFILES;
|
|
3354
3460
|
exports.GRID_SILENCED_CAP_NAMES = GRID_SILENCED_CAP_NAMES;
|
|
3355
3461
|
exports.GridCameraDevice = GridCameraDevice;
|
|
3356
3462
|
exports.GridFrameSampler = GridFrameSampler;
|
|
@@ -3361,15 +3467,16 @@ exports.GridStreamSession = GridStreamSession;
|
|
|
3361
3467
|
exports.buildGridCameraDeclarations = buildGridCameraDeclarations;
|
|
3362
3468
|
exports.buildGridFilterGraph = buildGridFilterGraph;
|
|
3363
3469
|
exports.default = CameraGridAddon;
|
|
3470
|
+
exports.deriveGridCanvas = deriveGridCanvas;
|
|
3471
|
+
exports.gridCamStreamId = gridCamStreamId;
|
|
3364
3472
|
exports.gridCameraReconciliationIndex = gridCameraReconciliationIndex;
|
|
3365
3473
|
exports.gridPlanFor = gridPlanFor;
|
|
3474
|
+
exports.gridProfilePlans = gridProfilePlans;
|
|
3366
3475
|
exports.gridSnapshotAnswerFor = gridSnapshotAnswerFor;
|
|
3367
3476
|
exports.gridSourceDial = gridSourceDial;
|
|
3368
|
-
exports.gridSourceProfiles = gridSourceProfiles;
|
|
3369
3477
|
exports.gridStreamDescriptor = gridStreamDescriptor;
|
|
3370
3478
|
exports.gridStreamInvocation = gridStreamInvocation;
|
|
3371
3479
|
exports.installGridCameraRuntime = installGridCameraRuntime;
|
|
3372
|
-
exports.lowestServedProfile = lowestServedProfile;
|
|
3373
3480
|
exports.newGridCameraStableId = newGridCameraStableId;
|
|
3374
3481
|
exports.normalizeGridRows = normalizeGridRows;
|
|
3375
3482
|
exports.parseGridStreamPath = parseGridStreamPath;
|