@camstack/system 1.2.267 → 1.2.269
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-compositor.d.ts +8 -0
- package/dist/builtins/camera-grid/grid-instances.d.ts +5 -2
- package/dist/builtins/camera-grid/grid-plan.d.ts +8 -6
- package/dist/builtins/camera-grid/grid-profiles.d.ts +42 -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/grid-tile-feed.d.ts +12 -0
- package/dist/builtins/camera-grid/index.d.ts +3 -2
- package/dist/builtins/camera-grid/index.js +438 -265
- package/dist/builtins/camera-grid/index.mjs +435 -263
- 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-BbPssgI6.js → dist-BTpfLiUX.js} +31 -6
- package/dist/{dist-Ck0rjYOQ.mjs → dist-CqmImT7i.mjs} +31 -6
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-BZpDuJVh.mjs → retired-settings-keys-C8nV-AUi.mjs} +1 -1
- package/dist/{retired-settings-keys-Dg_69cOY.js → retired-settings-keys-D_DIuT_N.js} +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-BTpfLiUX.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,11 +260,36 @@ 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
|
/**
|
|
276
|
+
* Which profiles this grid PUBLISHES, of the ones it could serve.
|
|
277
|
+
*
|
|
278
|
+
* Default `mid` + `low`, and the default is the finding: a grid's `high` is
|
|
279
|
+
* composed of its sources' `high`, so on this fleet it is a 4K canvas built
|
|
280
|
+
* from 4K decodes — a thing to opt into, not to be handed by a viewer's
|
|
281
|
+
* adaptive climbing to the top rung it can see.
|
|
282
|
+
*
|
|
283
|
+
* Absent on rows written before this, which is why the default lives in the
|
|
284
|
+
* schema rather than at the call site: an old grid starts publishing the two
|
|
285
|
+
* cheap rungs without anyone touching it.
|
|
286
|
+
*/
|
|
287
|
+
publishedProfiles: zod.z.array(zod.z.enum([
|
|
288
|
+
"high",
|
|
289
|
+
"mid",
|
|
290
|
+
"low"
|
|
291
|
+
])).max(3).default(["mid", "low"]),
|
|
292
|
+
/**
|
|
268
293
|
* Empty is the NORMAL freshly-created state: a grid is created from the
|
|
269
294
|
* integration page with a name only, and laid out afterwards in the camera's
|
|
270
295
|
* own settings. A grid with no cells declares its camera and emits nothing.
|
|
@@ -325,10 +350,7 @@ function gridPlanFor(instance, encode) {
|
|
|
325
350
|
cell: cell.cell
|
|
326
351
|
};
|
|
327
352
|
});
|
|
328
|
-
const canvas = encode.canvas
|
|
329
|
-
width: instance.width,
|
|
330
|
-
height: instance.height
|
|
331
|
-
};
|
|
353
|
+
const canvas = encode.canvas;
|
|
332
354
|
return {
|
|
333
355
|
layout: {
|
|
334
356
|
width: canvas.width,
|
|
@@ -926,92 +948,240 @@ var GridLastFrameFileStore = class {
|
|
|
926
948
|
}
|
|
927
949
|
};
|
|
928
950
|
//#endregion
|
|
929
|
-
//#region src/builtins/camera-grid/grid-
|
|
951
|
+
//#region src/builtins/camera-grid/grid-canvas.ts
|
|
952
|
+
/** Bytes one yuv420p frame of this size occupies. */
|
|
953
|
+
function yuv420pSize(width, height) {
|
|
954
|
+
return width * height + 2 * (width / 2) * (height / 2);
|
|
955
|
+
}
|
|
930
956
|
/**
|
|
931
|
-
*
|
|
957
|
+
* Round DOWN to even.
|
|
932
958
|
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* detour and it is `high` again, so nothing an operator assigned is disturbed.
|
|
959
|
+
* Down and not nearest: a tile that rounded UP would claim a row of samples the
|
|
960
|
+
* decoder was never asked to produce, and the copy would either overrun or be
|
|
961
|
+
* refused at the moment the picture is wanted.
|
|
937
962
|
*/
|
|
938
|
-
|
|
963
|
+
function evenDown(value) {
|
|
964
|
+
return Math.max(0, Math.floor(value / 2) * 2);
|
|
965
|
+
}
|
|
966
|
+
var GridCanvas = class {
|
|
967
|
+
width;
|
|
968
|
+
height;
|
|
969
|
+
buffer;
|
|
970
|
+
lumaBytes;
|
|
971
|
+
chromaBytes;
|
|
972
|
+
constructor(width, height) {
|
|
973
|
+
this.width = width;
|
|
974
|
+
this.height = height;
|
|
975
|
+
this.lumaBytes = width * height;
|
|
976
|
+
this.chromaBytes = width / 2 * (height / 2);
|
|
977
|
+
this.buffer = Buffer.alloc(yuv420pSize(width, height));
|
|
978
|
+
this.clear();
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Legible black — Y 0, chroma 128 (neutral).
|
|
982
|
+
*
|
|
983
|
+
* `Buffer.alloc` already zeroes, and zeroed CHROMA is not black: it is a
|
|
984
|
+
* strong green. A cell nobody has filled yet has to look like an empty cell,
|
|
985
|
+
* not like a fault.
|
|
986
|
+
*/
|
|
987
|
+
clear() {
|
|
988
|
+
this.buffer.fill(0, 0, this.lumaBytes);
|
|
989
|
+
this.buffer.fill(128, this.lumaBytes);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Copy one decoded tile in at `origin`.
|
|
993
|
+
*
|
|
994
|
+
* Returns false — never throws, never writes a partial tile — when the tile
|
|
995
|
+
* does not fit, is the wrong size for the dimensions it claims, or sits on an
|
|
996
|
+
* odd boundary. This runs on the output tick, so a bad frame from one source
|
|
997
|
+
* must cost that source's rectangle and nothing else.
|
|
998
|
+
*/
|
|
999
|
+
blit(tile, size, origin) {
|
|
1000
|
+
if (!isEven(size.width) || !isEven(size.height)) return false;
|
|
1001
|
+
if (!isEven(origin.x) || !isEven(origin.y)) return false;
|
|
1002
|
+
if (size.width <= 0 || size.height <= 0) return false;
|
|
1003
|
+
if (origin.x < 0 || origin.y < 0) return false;
|
|
1004
|
+
if (origin.x + size.width > this.width) return false;
|
|
1005
|
+
if (origin.y + size.height > this.height) return false;
|
|
1006
|
+
if (tile.length !== yuv420pSize(size.width, size.height)) return false;
|
|
1007
|
+
for (let row = 0; row < size.height; row += 1) {
|
|
1008
|
+
const from = row * size.width;
|
|
1009
|
+
const to = (origin.y + row) * this.width + origin.x;
|
|
1010
|
+
tile.copy(this.buffer, to, from, from + size.width);
|
|
1011
|
+
}
|
|
1012
|
+
const tileLuma = size.width * size.height;
|
|
1013
|
+
const tileChroma = size.width / 2 * (size.height / 2);
|
|
1014
|
+
const halfW = size.width / 2;
|
|
1015
|
+
const halfH = size.height / 2;
|
|
1016
|
+
const canvasHalfW = this.width / 2;
|
|
1017
|
+
for (let plane = 0; plane < 2; plane += 1) {
|
|
1018
|
+
const tileBase = tileLuma + plane * tileChroma;
|
|
1019
|
+
const canvasBase = this.lumaBytes + plane * this.chromaBytes;
|
|
1020
|
+
for (let row = 0; row < halfH; row += 1) {
|
|
1021
|
+
const from = tileBase + row * halfW;
|
|
1022
|
+
const to = canvasBase + (origin.y / 2 + row) * canvasHalfW + origin.x / 2;
|
|
1023
|
+
tile.copy(this.buffer, to, from, from + halfW);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
function isEven(value) {
|
|
1030
|
+
return Number.isInteger(value) && value % 2 === 0;
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/builtins/camera-grid/grid-canvas-derivation.ts
|
|
939
1034
|
/**
|
|
940
|
-
* The
|
|
1035
|
+
* The canvas is a RESULT, not a field.
|
|
1036
|
+
*
|
|
1037
|
+
* ## Why the operator stopped typing it
|
|
1038
|
+
*
|
|
1039
|
+
* A grid's resolution used to be two numbers in the editor, beside FPS. They
|
|
1040
|
+
* looked harmless and were not: that number silently decided how much the
|
|
1041
|
+
* composite cost and how sharp it was. An operator typing 1920x1080 was, without
|
|
1042
|
+
* being told, committing the hub to reading two 4 MP streams — or, with the
|
|
1043
|
+
* rule before it, to upscaling a 640x360 sub-stream by 3x. Neither is a thing
|
|
1044
|
+
* anyone can be expected to work out from a text field.
|
|
1045
|
+
*
|
|
1046
|
+
* ## What it is derived from
|
|
1047
|
+
*
|
|
1048
|
+
* The operator draws two rectangles per camera — which part of the source to
|
|
1049
|
+
* take, and where it lands — and both are fractions, so nothing is pixels yet.
|
|
1050
|
+
* Exactly one number is missing: how many pixels the whole canvas is worth.
|
|
1051
|
+
*
|
|
1052
|
+
* A cell has `sourceResolution x crop` REAL pixels behind it. Showing them 1:1
|
|
1053
|
+
* neither invents nor discards anything, so the cell should be exactly that
|
|
1054
|
+
* many pixels — and since the cell is a fraction `f` of the canvas, the canvas
|
|
1055
|
+
* it implies is `real / f`. Each cell proposes one, and the LARGEST wins: the
|
|
1056
|
+
* hungriest cell is served 1:1 and the others scale down by a hair, never up.
|
|
1057
|
+
*
|
|
1058
|
+
* Gaps change nothing. Cells need not cover the canvas, and an uncovered part
|
|
1059
|
+
* is background — it has no pixels to propose.
|
|
1060
|
+
*
|
|
1061
|
+
* ## One per profile
|
|
941
1062
|
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
1063
|
+
* A source is a different size at each profile, so each profile derives its own
|
|
1064
|
+
* canvas: a grid's `low` composes its sources' `low` onto the canvas their
|
|
1065
|
+
* `low` implies. That is what makes the three outputs 1:1 with what they are
|
|
1066
|
+
* made of, and what lets a viewer's adaptive pick a rung without the grid
|
|
1067
|
+
* needing to know it exists.
|
|
947
1068
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1069
|
+
/**
|
|
1070
|
+
* What an encoder will take. Not a taste: `libx264` refuses beyond 8192 in
|
|
1071
|
+
* either axis, and a canvas that large is a composition nobody can decode
|
|
1072
|
+
* anyway. Reached only by a cell that shows a sliver of a source at full
|
|
1073
|
+
* canvas width, which is a layout, not a mistake — so it is CLAMPED rather
|
|
1074
|
+
* than refused, and clamped in both axes together so the picture the operator
|
|
1075
|
+
* drew keeps its shape.
|
|
1076
|
+
*/
|
|
1077
|
+
var MAX_CANVAS_WIDTH = 7680;
|
|
1078
|
+
var MAX_CANVAS_HEIGHT = 4320;
|
|
1079
|
+
/** Below this a composition is not a cheaper picture, it is no picture. */
|
|
1080
|
+
var MIN_CANVAS_EDGE = 160;
|
|
1081
|
+
/**
|
|
1082
|
+
* The canvas these cells imply, or `null` when they imply none.
|
|
1083
|
+
*
|
|
1084
|
+
* `null` for a grid with no cells — the normal freshly-created state — and for
|
|
1085
|
+
* a layout whose every rectangle is degenerate. Both are "there is no picture
|
|
1086
|
+
* to size", which the caller turns into "this profile is not on offer" rather
|
|
1087
|
+
* than into a guess.
|
|
1088
|
+
*/
|
|
1089
|
+
function deriveGridCanvas(cells) {
|
|
1090
|
+
let width = 0;
|
|
1091
|
+
let height = 0;
|
|
1092
|
+
for (const entry of cells) {
|
|
1093
|
+
if (entry.cell.width <= 0 || entry.cell.height <= 0) continue;
|
|
1094
|
+
if (entry.sourceSize.width <= 0 || entry.sourceSize.height <= 0) continue;
|
|
1095
|
+
const realWidth = entry.sourceSize.width * entry.source.width;
|
|
1096
|
+
const realHeight = entry.sourceSize.height * entry.source.height;
|
|
1097
|
+
if (realWidth <= 0 || realHeight <= 0) continue;
|
|
1098
|
+
width = Math.max(width, realWidth / entry.cell.width);
|
|
1099
|
+
height = Math.max(height, realHeight / entry.cell.height);
|
|
1100
|
+
}
|
|
1101
|
+
if (width <= 0 || height <= 0) return null;
|
|
1102
|
+
const overshoot = Math.max(width / MAX_CANVAS_WIDTH, height / MAX_CANVAS_HEIGHT, 1);
|
|
1103
|
+
const factor = Math.max(MIN_CANVAS_EDGE / width, MIN_CANVAS_EDGE / height, 1) / overshoot;
|
|
965
1104
|
return {
|
|
966
|
-
width:
|
|
967
|
-
height:
|
|
1105
|
+
width: Math.max(2, evenDown(width * factor)),
|
|
1106
|
+
height: Math.max(2, evenDown(height * factor))
|
|
968
1107
|
};
|
|
969
1108
|
}
|
|
1109
|
+
//#endregion
|
|
1110
|
+
//#region src/builtins/camera-grid/grid-profiles.ts
|
|
970
1111
|
/**
|
|
971
|
-
*
|
|
1112
|
+
* One id per profile, for every grid, forever.
|
|
972
1113
|
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
* A stream that states NO size cannot be compared, so it is ordered as cheapest
|
|
978
|
-
* rather than discarded — an unmeasurable stream is still a stream, and the
|
|
979
|
-
* picture is what the operator asked for.
|
|
1114
|
+
* D519's reason is what keeps `high`'s id bare: a newly-spelled id mints a
|
|
1115
|
+
* second cam-stream and orphans whatever profile assignment the operator had
|
|
1116
|
+
* made. `grid` was `high` on the live hub before any of this and still is.
|
|
980
1117
|
*/
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1118
|
+
var GRID_CAM_STREAM_ID = "grid";
|
|
1119
|
+
/** Every profile a grid can compose, in descending quality. */
|
|
1120
|
+
var GRID_PROFILES = [
|
|
1121
|
+
"high",
|
|
1122
|
+
"mid",
|
|
1123
|
+
"low"
|
|
1124
|
+
];
|
|
1125
|
+
/** The cam-stream id a grid publishes for one profile. */
|
|
1126
|
+
function gridCamStreamId(profile) {
|
|
1127
|
+
return profile === "high" ? GRID_CAM_STREAM_ID : `${GRID_CAM_STREAM_ID}-${profile}`;
|
|
990
1128
|
}
|
|
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
|
-
|
|
1129
|
+
/**
|
|
1130
|
+
* What this grid can honestly publish, per profile, with the canvas or the
|
|
1131
|
+
* refusal.
|
|
1132
|
+
*
|
|
1133
|
+
* Every profile gets a row whether it is offered or not: an absent row says
|
|
1134
|
+
* nothing, and the caller logging a refusal needs the reason more than the
|
|
1135
|
+
* caller publishing an offer needs the offer.
|
|
1136
|
+
*/
|
|
1137
|
+
function gridProfilePlans(input) {
|
|
1138
|
+
const sourceDeviceIds = [...new Set(input.cells.map((cell) => cell.deviceId))];
|
|
1139
|
+
const servedBy = /* @__PURE__ */ new Map();
|
|
1140
|
+
for (const deviceId of sourceDeviceIds) servedBy.set(deviceId, input.profilesServedBy(deviceId));
|
|
1141
|
+
return GRID_PROFILES.map((profile) => {
|
|
1142
|
+
if (sourceDeviceIds.length === 0) return {
|
|
1143
|
+
profile,
|
|
1144
|
+
offered: false,
|
|
1145
|
+
missingSources: []
|
|
1146
|
+
};
|
|
1147
|
+
const sizeBySource = /* @__PURE__ */ new Map();
|
|
1148
|
+
const missingSources = [];
|
|
1149
|
+
for (const deviceId of sourceDeviceIds) {
|
|
1150
|
+
const match = (servedBy.get(deviceId) ?? null)?.find((entry) => entry.profile === profile);
|
|
1151
|
+
if (match === void 0 || match.width <= 0 || match.height <= 0) {
|
|
1152
|
+
missingSources.push(deviceId);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
sizeBySource.set(deviceId, {
|
|
1156
|
+
width: match.width,
|
|
1157
|
+
height: match.height
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (missingSources.length > 0) return {
|
|
1161
|
+
profile,
|
|
1162
|
+
offered: false,
|
|
1163
|
+
missingSources
|
|
1164
|
+
};
|
|
1165
|
+
const canvas = deriveGridCanvas(input.cells.map((cell) => ({
|
|
1166
|
+
source: cell.source,
|
|
1167
|
+
cell: cell.cell,
|
|
1168
|
+
sourceSize: sizeBySource.get(cell.deviceId) ?? {
|
|
1169
|
+
width: 0,
|
|
1170
|
+
height: 0
|
|
1171
|
+
}
|
|
1172
|
+
})));
|
|
1173
|
+
if (canvas === null) return {
|
|
1174
|
+
profile,
|
|
1175
|
+
offered: false,
|
|
1176
|
+
missingSources: []
|
|
1177
|
+
};
|
|
1178
|
+
return {
|
|
1179
|
+
profile,
|
|
1180
|
+
offered: true,
|
|
1181
|
+
canvas,
|
|
1182
|
+
published: input.publishedProfiles.includes(profile)
|
|
1183
|
+
};
|
|
1184
|
+
});
|
|
1015
1185
|
}
|
|
1016
1186
|
//#endregion
|
|
1017
1187
|
//#region src/builtins/camera-grid/grid-sentry-sources.ts
|
|
@@ -1176,15 +1346,24 @@ var PATH_PREFIX = "/grid/";
|
|
|
1176
1346
|
var PATH_SUFFIX = GRID_WIRE_PATH_SUFFIX;
|
|
1177
1347
|
/** How long a reader may refuse to drain before it is cut. */
|
|
1178
1348
|
var SLOW_READER_MS = 5e3;
|
|
1179
|
-
|
|
1349
|
+
function asProfile(raw) {
|
|
1350
|
+
return GRID_PROFILES.find((profile) => profile === raw) ?? null;
|
|
1351
|
+
}
|
|
1352
|
+
/** `/grid/<instanceId>/<profile>.flv`, or `null` when it is anything else. */
|
|
1180
1353
|
function parseGridStreamPath(url) {
|
|
1181
1354
|
const path = (url ?? "").split("?")[0] ?? "";
|
|
1182
1355
|
if (!path.startsWith(PATH_PREFIX) || !path.endsWith(PATH_SUFFIX)) return null;
|
|
1183
1356
|
const raw = path.slice(6, path.length - PATH_SUFFIX.length);
|
|
1184
|
-
|
|
1357
|
+
const at = raw.lastIndexOf("/");
|
|
1358
|
+
if (at <= 0) return null;
|
|
1359
|
+
const profile = asProfile(raw.slice(at + 1));
|
|
1360
|
+
if (profile === null) return null;
|
|
1185
1361
|
try {
|
|
1186
|
-
const instanceId = decodeURIComponent(raw);
|
|
1187
|
-
return instanceId.length > 0 ? {
|
|
1362
|
+
const instanceId = decodeURIComponent(raw.slice(0, at));
|
|
1363
|
+
return instanceId.length > 0 ? {
|
|
1364
|
+
instanceId,
|
|
1365
|
+
profile
|
|
1366
|
+
} : null;
|
|
1188
1367
|
} catch {
|
|
1189
1368
|
return null;
|
|
1190
1369
|
}
|
|
@@ -1223,13 +1402,16 @@ var GridStreamRelay = class {
|
|
|
1223
1402
|
streamUrl(request) {
|
|
1224
1403
|
if (!this.baseUrl) throw new Error("camera-grid: the stream relay is not started");
|
|
1225
1404
|
const instance = encodeURIComponent(request.instanceId);
|
|
1226
|
-
return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
|
|
1405
|
+
return `${this.baseUrl}${PATH_PREFIX}${instance}/${request.profile}${PATH_SUFFIX}`;
|
|
1227
1406
|
}
|
|
1228
1407
|
async serve(request, res) {
|
|
1229
|
-
const { instanceId } = request;
|
|
1408
|
+
const { instanceId, profile } = request;
|
|
1230
1409
|
const session = this.deps.sessionFor(request);
|
|
1231
1410
|
if (session === null) {
|
|
1232
|
-
this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
|
|
1411
|
+
this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
|
|
1412
|
+
instanceId,
|
|
1413
|
+
profile
|
|
1414
|
+
} });
|
|
1233
1415
|
res.writeHead(404).end();
|
|
1234
1416
|
return;
|
|
1235
1417
|
}
|
|
@@ -1263,6 +1445,7 @@ var GridStreamRelay = class {
|
|
|
1263
1445
|
tags: { deviceId: session.deviceId },
|
|
1264
1446
|
meta: {
|
|
1265
1447
|
instanceId,
|
|
1448
|
+
profile,
|
|
1266
1449
|
stalledForMs: SLOW_READER_MS
|
|
1267
1450
|
}
|
|
1268
1451
|
});
|
|
@@ -1275,6 +1458,7 @@ var GridStreamRelay = class {
|
|
|
1275
1458
|
tags: { deviceId: session.deviceId },
|
|
1276
1459
|
meta: {
|
|
1277
1460
|
instanceId,
|
|
1461
|
+
profile,
|
|
1278
1462
|
reason
|
|
1279
1463
|
}
|
|
1280
1464
|
});
|
|
@@ -1287,6 +1471,7 @@ var GridStreamRelay = class {
|
|
|
1287
1471
|
tags: { deviceId: session.deviceId },
|
|
1288
1472
|
meta: {
|
|
1289
1473
|
instanceId,
|
|
1474
|
+
profile,
|
|
1290
1475
|
error: require_dist.errMsg(error)
|
|
1291
1476
|
}
|
|
1292
1477
|
});
|
|
@@ -1322,13 +1507,13 @@ var GridStreamRelay = class {
|
|
|
1322
1507
|
* scrub never reads. The Dreame robot's one 720p relay landed exactly there and
|
|
1323
1508
|
* its footage was written nowhere anything looked.
|
|
1324
1509
|
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1510
|
+
* With several compositions published the hint does a second job: it says which
|
|
1511
|
+
* slot each one is FOR. Ranked by pixels a grid's `mid` would take the `high`
|
|
1512
|
+
* slot on any grid whose `high` was not offered.
|
|
1328
1513
|
*/
|
|
1329
1514
|
function gridStreamDescriptor(input) {
|
|
1330
1515
|
return {
|
|
1331
|
-
camStreamId:
|
|
1516
|
+
camStreamId: gridCamStreamId(input.profile),
|
|
1332
1517
|
kind: GRID_WIRE_STREAM_KIND,
|
|
1333
1518
|
url: input.url,
|
|
1334
1519
|
codec: "h264",
|
|
@@ -1337,8 +1522,8 @@ function gridStreamDescriptor(input) {
|
|
|
1337
1522
|
height: input.height
|
|
1338
1523
|
},
|
|
1339
1524
|
fps: input.fps,
|
|
1340
|
-
label:
|
|
1341
|
-
profileHint:
|
|
1525
|
+
label: `Grid composition (${input.profile})`,
|
|
1526
|
+
profileHint: input.profile,
|
|
1342
1527
|
autoEligible: true
|
|
1343
1528
|
};
|
|
1344
1529
|
}
|
|
@@ -1841,88 +2026,6 @@ async function silenceAnalysisFor(deps, deviceId) {
|
|
|
1841
2026
|
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
2027
|
}
|
|
1843
2028
|
//#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
2029
|
//#region src/builtins/camera-grid/grid-tile-feed.ts
|
|
1927
2030
|
/**
|
|
1928
2031
|
* One source's decoded tiles, of which only the LATEST is ever wanted.
|
|
@@ -1974,6 +2077,20 @@ var GridTileFeed = class {
|
|
|
1974
2077
|
hasFresh() {
|
|
1975
2078
|
return this.fresh;
|
|
1976
2079
|
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Forget the half frame, keep the picture.
|
|
2082
|
+
*
|
|
2083
|
+
* Raw video has no framing: the feed cuts on a fixed length, so bytes from a
|
|
2084
|
+
* NEW decoder concatenated onto a partial frame from the old one would be
|
|
2085
|
+
* misaligned for ever — every frame after it sheared, with nothing to
|
|
2086
|
+
* re-sync on. Called whenever a tile's decoder is replaced.
|
|
2087
|
+
*
|
|
2088
|
+
* The newest frame is kept deliberately: the cell holds its last picture
|
|
2089
|
+
* while its decoder comes back, instead of blinking to black.
|
|
2090
|
+
*/
|
|
2091
|
+
resetPartial() {
|
|
2092
|
+
this.partial = Buffer.alloc(0);
|
|
2093
|
+
}
|
|
1977
2094
|
/** For the bound in the spec: one frame plus at most a partial one. */
|
|
1978
2095
|
bufferedBytes() {
|
|
1979
2096
|
return (this.newest?.length ?? 0) + this.partial.length;
|
|
@@ -2057,6 +2174,16 @@ var GridCompositor = class {
|
|
|
2057
2174
|
}
|
|
2058
2175
|
return this.deps.write(this.canvas.buffer);
|
|
2059
2176
|
}
|
|
2177
|
+
/**
|
|
2178
|
+
* A tile's decoder was replaced: drop its half frame, keep its picture.
|
|
2179
|
+
*
|
|
2180
|
+
* Without this the new decoder's bytes would be appended to the old one's
|
|
2181
|
+
* partial frame and every frame after would be sheared — raw video has no
|
|
2182
|
+
* framing to re-sync on.
|
|
2183
|
+
*/
|
|
2184
|
+
resetTile(tileId) {
|
|
2185
|
+
this.placed.get(tileId)?.feed.resetPartial();
|
|
2186
|
+
}
|
|
2060
2187
|
/** After this, a tick emits nothing — a dead child must not keep writing. */
|
|
2061
2188
|
close() {
|
|
2062
2189
|
this.closed = true;
|
|
@@ -2243,13 +2370,22 @@ function gridCanvasEncoderInvocation(input) {
|
|
|
2243
2370
|
* is how a stalled encoder becomes an OOM, and a dropped frame in a live
|
|
2244
2371
|
* composite costs nothing anybody can see.
|
|
2245
2372
|
*
|
|
2246
|
-
* ##
|
|
2373
|
+
* ## A tile comes back; only the ENCODER ends the composition
|
|
2374
|
+
*
|
|
2375
|
+
* Every child exiting used to end it, on the reasoning that a frozen tile is a
|
|
2376
|
+
* nicer picture and a worse fact. In practice it produced neither: a 4K source
|
|
2377
|
+
* ended mid-run (`code=0` — the input simply finished), the composition tore
|
|
2378
|
+
* down, the consumer re-dialled, and the grid served nothing but restarts. That
|
|
2379
|
+
* is what the operator saw as the `auto` rung being stuck.
|
|
2247
2380
|
*
|
|
2248
|
-
*
|
|
2249
|
-
*
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
2381
|
+
* A canvas makes the better answer cheap: while a decoder comes back, its cell
|
|
2382
|
+
* holds the last frame it produced and every other cell carries on. The bound
|
|
2383
|
+
* (`TILE_MAX_RESTARTS`) keeps a source that is genuinely gone from respawning
|
|
2384
|
+
* for ever — and when it is reached, the composition does end, with the tile
|
|
2385
|
+
* named.
|
|
2386
|
+
*
|
|
2387
|
+
* The encoder is different: nothing downstream of it exists without it, so its
|
|
2388
|
+
* exit is the composition's exit, first time.
|
|
2253
2389
|
*/
|
|
2254
2390
|
/**
|
|
2255
2391
|
* The encoder must produce its first bytes quickly, because the tick feeds it a
|
|
@@ -2263,6 +2399,15 @@ var ENCODER_FIRST_DATA_MS = 5e3;
|
|
|
2263
2399
|
* already on screen.
|
|
2264
2400
|
*/
|
|
2265
2401
|
var DECODER_FIRST_DATA_MS = 15e3;
|
|
2402
|
+
/**
|
|
2403
|
+
* How many times a tile's decoder may come back before the composition gives up
|
|
2404
|
+
* on it.
|
|
2405
|
+
*
|
|
2406
|
+
* The bound exists so a source that is genuinely gone still ends the
|
|
2407
|
+
* composition rather than respawning for ever; below it, a decoder that dies is
|
|
2408
|
+
* simply replaced while its cell holds the last frame it produced.
|
|
2409
|
+
*/
|
|
2410
|
+
var TILE_MAX_RESTARTS = 5;
|
|
2266
2411
|
async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
2267
2412
|
const tilePlan = buildGridTilePlan({
|
|
2268
2413
|
canvas: {
|
|
@@ -2372,14 +2517,17 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2372
2517
|
height: tile.size.height
|
|
2373
2518
|
},
|
|
2374
2519
|
firstDataTimeoutMs: DECODER_FIRST_DATA_MS,
|
|
2375
|
-
maxRestarts:
|
|
2520
|
+
maxRestarts: TILE_MAX_RESTARTS,
|
|
2376
2521
|
onChild: (child) => {
|
|
2377
2522
|
child.stdout?.on("data", (chunk) => {
|
|
2378
2523
|
compositor.onTile(tile.id, chunk);
|
|
2379
2524
|
});
|
|
2380
2525
|
child.stderr?.on("data", () => {});
|
|
2381
2526
|
},
|
|
2382
|
-
onExit: (
|
|
2527
|
+
onExit: () => {
|
|
2528
|
+
compositor.resetTile(tile.id);
|
|
2529
|
+
},
|
|
2530
|
+
onGiveUp: (error) => end(`tile ${tile.id}: ${error.message}`)
|
|
2383
2531
|
});
|
|
2384
2532
|
decoders.push(decoder);
|
|
2385
2533
|
await decoder.start();
|
|
@@ -2424,18 +2572,26 @@ var DEFAULTS = {
|
|
|
2424
2572
|
cameraGrids: [],
|
|
2425
2573
|
cameraGridTombstones: []
|
|
2426
2574
|
};
|
|
2575
|
+
/** One composition per (grid, profile) — a grid composes each separately. */
|
|
2576
|
+
function sessionKey(instanceId, profile) {
|
|
2577
|
+
return `${instanceId}::${profile}`;
|
|
2578
|
+
}
|
|
2427
2579
|
/**
|
|
2428
|
-
* One grid's composition
|
|
2580
|
+
* One grid's composition of one profile.
|
|
2429
2581
|
*
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2582
|
+
* The profile is back in the key after a spell with a single output. What made
|
|
2583
|
+
* the per-profile shape painful was never the count: it was that a rebuild
|
|
2584
|
+
* meant a blank screen until the slowest source produced a key frame. An output
|
|
2585
|
+
* clock (D530) shows the picture at once and fills the cells in, so a rung the
|
|
2586
|
+
* adaptive can stand on costs what it is worth.
|
|
2433
2587
|
*/
|
|
2434
2588
|
var GridSessionEntry = class {
|
|
2435
2589
|
instanceId;
|
|
2590
|
+
profile;
|
|
2436
2591
|
session;
|
|
2437
|
-
constructor(instanceId, session) {
|
|
2592
|
+
constructor(instanceId, profile, session) {
|
|
2438
2593
|
this.instanceId = instanceId;
|
|
2594
|
+
this.profile = profile;
|
|
2439
2595
|
this.session = session;
|
|
2440
2596
|
}
|
|
2441
2597
|
isRunning() {
|
|
@@ -2459,7 +2615,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2459
2615
|
* (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
|
|
2460
2616
|
* — that only changes when an operator changes it.
|
|
2461
2617
|
*/
|
|
2462
|
-
|
|
2618
|
+
plansByInstanceId = /* @__PURE__ */ new Map();
|
|
2463
2619
|
activity = null;
|
|
2464
2620
|
snapshots = null;
|
|
2465
2621
|
frameSampler = null;
|
|
@@ -2692,9 +2848,9 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2692
2848
|
if (instance.cells.filter((cell) => cell.deviceId === sourceDeviceId).length === 0) continue;
|
|
2693
2849
|
targets.push({
|
|
2694
2850
|
deviceId,
|
|
2695
|
-
canvas: {
|
|
2696
|
-
width:
|
|
2697
|
-
height:
|
|
2851
|
+
canvas: this.canvasFor(instanceId) ?? {
|
|
2852
|
+
width: 0,
|
|
2853
|
+
height: 0
|
|
2698
2854
|
},
|
|
2699
2855
|
cellsForSource: (id) => instance.cells.filter((cell) => cell.deviceId === id).map((cell) => ({
|
|
2700
2856
|
source: cell.source,
|
|
@@ -2737,11 +2893,8 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2737
2893
|
servedBy.set(slot.deviceId, profiles);
|
|
2738
2894
|
}
|
|
2739
2895
|
for (const instance of instances) {
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
width: instance.width,
|
|
2743
|
-
height: instance.height
|
|
2744
|
-
},
|
|
2896
|
+
const plans = gridProfilePlans({
|
|
2897
|
+
publishedProfiles: instance.publishedProfiles,
|
|
2745
2898
|
cells: instance.cells.map((cell) => ({
|
|
2746
2899
|
deviceId: cell.deviceId,
|
|
2747
2900
|
source: cell.source,
|
|
@@ -2749,9 +2902,9 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2749
2902
|
})),
|
|
2750
2903
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
2751
2904
|
});
|
|
2752
|
-
const previous = this.
|
|
2753
|
-
this.
|
|
2754
|
-
this.logSourcePlan(instance,
|
|
2905
|
+
const previous = this.plansByInstanceId.get(instance.id);
|
|
2906
|
+
this.plansByInstanceId.set(instance.id, plans);
|
|
2907
|
+
this.logSourcePlan(instance, plans, previous);
|
|
2755
2908
|
}
|
|
2756
2909
|
}
|
|
2757
2910
|
/**
|
|
@@ -2767,48 +2920,65 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2767
2920
|
* Logged on the EDGE: a grid that cannot be composed would otherwise write a
|
|
2768
2921
|
* line a minute for ever.
|
|
2769
2922
|
*/
|
|
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;
|
|
2923
|
+
logSourcePlan(instance, plans, previous) {
|
|
2924
|
+
const shape = (value) => JSON.stringify(value);
|
|
2925
|
+
if (previous !== void 0 && shape(previous) === shape(plans)) return;
|
|
2777
2926
|
const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2927
|
+
for (const row of plans) {
|
|
2928
|
+
if (row.offered || row.missingSources.length === 0) continue;
|
|
2929
|
+
this.ctx.logger.info("camera grid: a profile is NOT on offer", {
|
|
2930
|
+
tags: { deviceId },
|
|
2931
|
+
meta: {
|
|
2932
|
+
instanceId: instance.id,
|
|
2933
|
+
profile: row.profile,
|
|
2934
|
+
missingSources: [...row.missingSources]
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
this.ctx.logger.info("camera grid: published profiles", {
|
|
2786
2939
|
tags: { deviceId },
|
|
2787
2940
|
meta: {
|
|
2788
2941
|
instanceId: instance.id,
|
|
2789
|
-
offered:
|
|
2790
|
-
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
2942
|
+
offered: Object.fromEntries(plans.filter((row) => row.offered && row.published).map((row) => [row.profile, row.offered ? `${String(row.canvas.width)}x${String(row.canvas.height)}` : ""]))
|
|
2791
2943
|
}
|
|
2792
2944
|
});
|
|
2793
2945
|
}
|
|
2794
|
-
|
|
2795
|
-
return this.
|
|
2946
|
+
plansFor(instanceId) {
|
|
2947
|
+
return this.plansByInstanceId.get(instanceId) ?? null;
|
|
2948
|
+
}
|
|
2949
|
+
planFor(instanceId, profile) {
|
|
2950
|
+
return this.plansFor(instanceId)?.find((row) => row.profile === profile) ?? null;
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* The canvas of the BEST profile this grid offers.
|
|
2954
|
+
*
|
|
2955
|
+
* Used where a single geometry is needed for something that is not a
|
|
2956
|
+
* composition — the detection boxes a grid forwards (`grid-activity`), which
|
|
2957
|
+
* are normalized against whatever picture the viewer is looking at. The best
|
|
2958
|
+
* offered one is the honest default; a grid offering nothing has no geometry
|
|
2959
|
+
* and says so.
|
|
2960
|
+
*/
|
|
2961
|
+
canvasFor(instanceId) {
|
|
2962
|
+
for (const row of this.plansFor(instanceId) ?? []) if (row.offered) return row.canvas;
|
|
2963
|
+
return null;
|
|
2796
2964
|
}
|
|
2797
2965
|
ensureSessions(instanceId, deviceId) {
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
const
|
|
2801
|
-
if (
|
|
2802
|
-
this.sessions.
|
|
2803
|
-
stale
|
|
2966
|
+
for (const profile of GRID_PROFILES) {
|
|
2967
|
+
const key = sessionKey(instanceId, profile);
|
|
2968
|
+
const plan = this.planFor(instanceId, profile);
|
|
2969
|
+
if (plan === null || !plan.offered || !plan.published) {
|
|
2970
|
+
const stale = this.sessions.get(key);
|
|
2971
|
+
if (stale) {
|
|
2972
|
+
this.sessions.delete(key);
|
|
2973
|
+
stale.session.dispose();
|
|
2974
|
+
}
|
|
2975
|
+
continue;
|
|
2804
2976
|
}
|
|
2805
|
-
|
|
2977
|
+
if (this.sessions.has(key)) continue;
|
|
2978
|
+
this.sessions.set(key, new GridSessionEntry(instanceId, profile, this.makeSession(instanceId, deviceId, profile)));
|
|
2806
2979
|
}
|
|
2807
|
-
if (this.sessions.has(instanceId)) return;
|
|
2808
|
-
const session = this.makeSession(instanceId, deviceId);
|
|
2809
|
-
this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
|
|
2810
2980
|
}
|
|
2811
|
-
makeSession(instanceId, deviceId) {
|
|
2981
|
+
makeSession(instanceId, deviceId, profile) {
|
|
2812
2982
|
const childContext = {
|
|
2813
2983
|
binaryPath: this.ffmpegBinaryPath,
|
|
2814
2984
|
logger: this.ctx.logger,
|
|
@@ -2819,8 +2989,6 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2819
2989
|
deps: {
|
|
2820
2990
|
logger: this.ctx.logger,
|
|
2821
2991
|
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
2992
|
const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
|
|
2825
2993
|
deviceId: sourceDeviceId,
|
|
2826
2994
|
video: "copy",
|
|
@@ -2867,10 +3035,12 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2867
3035
|
},
|
|
2868
3036
|
resolvePlan: () => {
|
|
2869
3037
|
const instance = this.instanceById(instanceId);
|
|
2870
|
-
|
|
3038
|
+
const plan = this.planFor(instanceId, profile);
|
|
3039
|
+
if (!instance || plan === null || !plan.offered || !plan.published) return null;
|
|
2871
3040
|
return gridPlanFor(instance, {
|
|
2872
3041
|
encoder: "libx264",
|
|
2873
|
-
decodeHwAccel: null
|
|
3042
|
+
decodeHwAccel: null,
|
|
3043
|
+
canvas: plan.canvas
|
|
2874
3044
|
});
|
|
2875
3045
|
}
|
|
2876
3046
|
});
|
|
@@ -2878,14 +3048,18 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2878
3048
|
}
|
|
2879
3049
|
/** The session a dial is asking for, or `null` - which the relay answers 404 to. */
|
|
2880
3050
|
sessionFor(request) {
|
|
2881
|
-
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
3051
|
+
return this.sessions.get(sessionKey(request.instanceId, request.profile))?.session ?? null;
|
|
2882
3052
|
}
|
|
2883
|
-
/** This grid's
|
|
3053
|
+
/** This grid's sessions, in DESCENDING quality — the snapshot takes the first running one. */
|
|
2884
3054
|
sessionsForDevice(deviceId) {
|
|
2885
3055
|
const instanceId = this.instanceIdForDevice(deviceId);
|
|
2886
3056
|
if (instanceId === null) return [];
|
|
2887
|
-
const
|
|
2888
|
-
|
|
3057
|
+
const entries = [];
|
|
3058
|
+
for (const profile of GRID_PROFILES) {
|
|
3059
|
+
const entry = this.sessions.get(sessionKey(instanceId, profile));
|
|
3060
|
+
if (entry) entries.push(entry);
|
|
3061
|
+
}
|
|
3062
|
+
return entries;
|
|
2889
3063
|
}
|
|
2890
3064
|
/** A grid that is gone stops immediately - it must not keep N sources dialled. */
|
|
2891
3065
|
async retireWithdrawnSessions(instances) {
|
|
@@ -2903,7 +3077,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2903
3077
|
if (live.has(instanceId)) continue;
|
|
2904
3078
|
this.activity?.forget(this.deviceIdByInstanceId.get(instanceId) ?? 0);
|
|
2905
3079
|
this.deviceIdByInstanceId.delete(instanceId);
|
|
2906
|
-
this.
|
|
3080
|
+
this.plansByInstanceId.delete(instanceId);
|
|
2907
3081
|
}
|
|
2908
3082
|
}
|
|
2909
3083
|
/**
|
|
@@ -2951,15 +3125,18 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2951
3125
|
descriptorsFor(instanceId) {
|
|
2952
3126
|
const instance = this.instanceById(instanceId);
|
|
2953
3127
|
const relay = this.relay;
|
|
2954
|
-
const
|
|
2955
|
-
if (!instance || !relay ||
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
width:
|
|
2959
|
-
height:
|
|
3128
|
+
const plans = this.plansFor(instanceId);
|
|
3129
|
+
if (!instance || !relay || plans === null) return null;
|
|
3130
|
+
return plans.filter((row) => row.offered && row.published).map((row) => gridStreamDescriptor({
|
|
3131
|
+
profile: row.profile,
|
|
3132
|
+
width: row.offered ? row.canvas.width : 0,
|
|
3133
|
+
height: row.offered ? row.canvas.height : 0,
|
|
2960
3134
|
fps: instance.fps,
|
|
2961
|
-
url: relay.streamUrl({
|
|
2962
|
-
|
|
3135
|
+
url: relay.streamUrl({
|
|
3136
|
+
instanceId,
|
|
3137
|
+
profile: row.profile
|
|
3138
|
+
})
|
|
3139
|
+
}));
|
|
2963
3140
|
}
|
|
2964
3141
|
instanceIdForDevice(deviceId) {
|
|
2965
3142
|
for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
|
|
@@ -2976,23 +3153,19 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2976
3153
|
instanceId: instance.id,
|
|
2977
3154
|
deviceId,
|
|
2978
3155
|
name: instance.name,
|
|
2979
|
-
width: instance.width,
|
|
2980
|
-
height: instance.height,
|
|
2981
3156
|
fps: instance.fps,
|
|
2982
3157
|
cells: instance.cells.map((cell) => ({
|
|
2983
3158
|
deviceId: cell.deviceId,
|
|
2984
3159
|
source: cell.source,
|
|
2985
3160
|
cell: cell.cell
|
|
2986
3161
|
})),
|
|
2987
|
-
profiles: (() => {
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
}];
|
|
2995
|
-
})()
|
|
3162
|
+
profiles: (this.plansByInstanceId.get(instance.id) ?? []).map((row) => ({
|
|
3163
|
+
profile: row.profile,
|
|
3164
|
+
offered: row.offered,
|
|
3165
|
+
published: row.offered && row.published,
|
|
3166
|
+
missingSources: row.offered ? [] : [...row.missingSources],
|
|
3167
|
+
canvas: row.offered ? `${String(row.canvas.width)}x${String(row.canvas.height)}` : ""
|
|
3168
|
+
}))
|
|
2996
3169
|
};
|
|
2997
3170
|
}
|
|
2998
3171
|
async saveGridLayout(input) {
|
|
@@ -3001,9 +3174,8 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
3001
3174
|
const updated = {
|
|
3002
3175
|
...instance,
|
|
3003
3176
|
...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
3177
|
...input.fps !== void 0 ? { fps: input.fps } : {},
|
|
3178
|
+
...input.publishedProfiles !== void 0 ? { publishedProfiles: [...input.publishedProfiles] } : {},
|
|
3007
3179
|
cells: input.cells.map((cell) => ({ ...cell }))
|
|
3008
3180
|
};
|
|
3009
3181
|
this.ctx.logger.info("camera grid: layout saved", {
|
|
@@ -3011,7 +3183,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
3011
3183
|
meta: {
|
|
3012
3184
|
cells: updated.cells.length,
|
|
3013
3185
|
sources: [...new Set(updated.cells.map((cell) => cell.deviceId))],
|
|
3014
|
-
|
|
3186
|
+
fps: updated.fps
|
|
3015
3187
|
}
|
|
3016
3188
|
});
|
|
3017
3189
|
await this.updateGlobalSettings({ cameraGrids: this.config.cameraGrids.map((row) => row["id"] === instance.id ? {
|
|
@@ -3350,7 +3522,7 @@ async function startGridChild(ctx, plan, sources, onEnded) {
|
|
|
3350
3522
|
//#endregion
|
|
3351
3523
|
exports.CameraGridAddon = CameraGridAddon;
|
|
3352
3524
|
exports.GRID_CAM_STREAM_ID = GRID_CAM_STREAM_ID;
|
|
3353
|
-
exports.
|
|
3525
|
+
exports.GRID_PROFILES = GRID_PROFILES;
|
|
3354
3526
|
exports.GRID_SILENCED_CAP_NAMES = GRID_SILENCED_CAP_NAMES;
|
|
3355
3527
|
exports.GridCameraDevice = GridCameraDevice;
|
|
3356
3528
|
exports.GridFrameSampler = GridFrameSampler;
|
|
@@ -3361,15 +3533,16 @@ exports.GridStreamSession = GridStreamSession;
|
|
|
3361
3533
|
exports.buildGridCameraDeclarations = buildGridCameraDeclarations;
|
|
3362
3534
|
exports.buildGridFilterGraph = buildGridFilterGraph;
|
|
3363
3535
|
exports.default = CameraGridAddon;
|
|
3536
|
+
exports.deriveGridCanvas = deriveGridCanvas;
|
|
3537
|
+
exports.gridCamStreamId = gridCamStreamId;
|
|
3364
3538
|
exports.gridCameraReconciliationIndex = gridCameraReconciliationIndex;
|
|
3365
3539
|
exports.gridPlanFor = gridPlanFor;
|
|
3540
|
+
exports.gridProfilePlans = gridProfilePlans;
|
|
3366
3541
|
exports.gridSnapshotAnswerFor = gridSnapshotAnswerFor;
|
|
3367
3542
|
exports.gridSourceDial = gridSourceDial;
|
|
3368
|
-
exports.gridSourceProfiles = gridSourceProfiles;
|
|
3369
3543
|
exports.gridStreamDescriptor = gridStreamDescriptor;
|
|
3370
3544
|
exports.gridStreamInvocation = gridStreamInvocation;
|
|
3371
3545
|
exports.installGridCameraRuntime = installGridCameraRuntime;
|
|
3372
|
-
exports.lowestServedProfile = lowestServedProfile;
|
|
3373
3546
|
exports.newGridCameraStableId = newGridCameraStableId;
|
|
3374
3547
|
exports.normalizeGridRows = normalizeGridRows;
|
|
3375
3548
|
exports.parseGridStreamPath = parseGridStreamPath;
|