@camstack/system 1.2.272 → 1.2.274
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 -0
- package/dist/builtins/camera-grid/grid-canvas-seed.d.ts +62 -0
- package/dist/builtins/camera-grid/grid-canvas.d.ts +4 -2
- package/dist/builtins/camera-grid/grid-child.d.ts +10 -0
- package/dist/builtins/camera-grid/grid-compositor.d.ts +10 -0
- package/dist/builtins/camera-grid/index.js +321 -10
- package/dist/builtins/camera-grid/index.mjs +321 -10
- 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/cgroup-pids.d.ts +112 -0
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +1 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +209 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +209 -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-_YWIPtUH.js → dist-C_4Xt0Ya.js} +209 -11
- package/dist/{dist-Diz59Dhi.mjs → dist-DchKwh_m.mjs} +209 -11
- package/dist/index.js +12 -5
- package/dist/index.mjs +12 -5
- package/dist/kernel/moleculer/process-service.d.ts +54 -0
- package/dist/{retired-settings-keys-C2ocoH_l.js → retired-settings-keys-6wZNp7lr.js} +1 -1
- package/dist/{retired-settings-keys-C_Hg5d37.mjs → retired-settings-keys-DPSHUHMu.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
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-C_4Xt0Ya.js");
|
|
7
7
|
const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { P as addonPagesCapability, Pn as EventCategory, an as errMsg, sn as BaseAddon } from "../../dist-
|
|
1
|
+
import { P as addonPagesCapability, Pn as EventCategory, an as errMsg, sn as BaseAddon } from "../../dist-DchKwh_m.mjs";
|
|
2
2
|
import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-C_4Xt0Ya.js");
|
|
7
7
|
const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as addonWidgetsCapability, Pn as EventCategory, an as errMsg, sn as BaseAddon } from "../../dist-
|
|
1
|
+
import { I as addonWidgetsCapability, Pn as EventCategory, an as errMsg, sn as BaseAddon } from "../../dist-DchKwh_m.mjs";
|
|
2
2
|
import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -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-C_4Xt0Ya.js");
|
|
7
7
|
//#region src/builtins/alerts/prune-policy.ts
|
|
8
8
|
/**
|
|
9
9
|
* Ids older than `cutoffMs`, oldest first.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as alertsCapability, Pn as EventCategory, an as errMsg, bn as createEvent, n as AlertSchema, sn as BaseAddon } from "../../dist-
|
|
1
|
+
import { L as alertsCapability, Pn as EventCategory, an as errMsg, bn as createEvent, n as AlertSchema, sn as BaseAddon } from "../../dist-DchKwh_m.mjs";
|
|
2
2
|
//#region src/builtins/alerts/prune-policy.ts
|
|
3
3
|
/**
|
|
4
4
|
* Ids older than `cutoffMs`, oldest first.
|
|
@@ -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-C_4Xt0Ya.js");
|
|
7
7
|
//#region src/builtins/autotrack/autotrack-config.ts
|
|
8
8
|
var DEFAULT_AUTOTRACK_LOOP_CONFIG = {
|
|
9
9
|
deadZoneFraction: .5,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as bareAddonId, En as isEvent, It as ptzAutotrackCapability, Pn as EventCategory, an as errMsg, h as FIRST_LEVEL_MACRO_CLASSES, sn as BaseAddon } from "../../dist-
|
|
1
|
+
import { B as bareAddonId, En as isEvent, It as ptzAutotrackCapability, Pn as EventCategory, an as errMsg, h as FIRST_LEVEL_MACRO_CLASSES, sn as BaseAddon } from "../../dist-DchKwh_m.mjs";
|
|
2
2
|
//#region src/builtins/autotrack/autotrack-config.ts
|
|
3
3
|
var DEFAULT_AUTOTRACK_LOOP_CONFIG = {
|
|
4
4
|
deadZoneFraction: .5,
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-C_4Xt0Ya.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let buffer = require("buffer");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Dt as mayWriteToLocation, Pn as EventCategory, Zt as storageOccupancyCapability, sn as BaseAddon, z as backupCapability } from "../../dist-
|
|
1
|
+
import { Dt as mayWriteToLocation, Pn as EventCategory, Zt as storageOccupancyCapability, sn as BaseAddon, z as backupCapability } from "../../dist-DchKwh_m.mjs";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { Buffer as Buffer$1 } from "buffer";
|
|
4
4
|
import * as fsp from "node:fs/promises";
|
|
@@ -100,6 +100,19 @@ export declare class CameraGridAddon extends BaseAddon<CameraGridAddonConfig> {
|
|
|
100
100
|
*/
|
|
101
101
|
private canvasFor;
|
|
102
102
|
private ensureSessions;
|
|
103
|
+
/**
|
|
104
|
+
* The source cameras' names, for the cards the canvas is seeded with.
|
|
105
|
+
*
|
|
106
|
+
* Asked of the device manager, which OWNS the name, at the moment the seed is
|
|
107
|
+
* drawn — never kept here. A consumer that mirrored this would be stale on
|
|
108
|
+
* exactly the write an operator makes (D224), for a label.
|
|
109
|
+
*
|
|
110
|
+
* A device that cannot be read is simply absent from the map, and its card
|
|
111
|
+
* carries the device id instead. One unreadable camera never costs the other
|
|
112
|
+
* cards their names, which is why these are settled rather than awaited as
|
|
113
|
+
* one batch.
|
|
114
|
+
*/
|
|
115
|
+
private sourceNames;
|
|
103
116
|
private makeSession;
|
|
104
117
|
/** The session a dial is asking for, or `null` - which the relay answers 404 to. */
|
|
105
118
|
private sessionFor;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PixelOrigin, PixelSize } from './grid-canvas.js';
|
|
2
|
+
import { GridTilePlan } from './grid-tile-plan.js';
|
|
3
|
+
/**
|
|
4
|
+
* The word a card carries. Deliberately not a fault: nothing has arrived YET,
|
|
5
|
+
* which is true of a camera that is slow and of one that is broken, and this
|
|
6
|
+
* placeholder knows which it is for neither.
|
|
7
|
+
*/
|
|
8
|
+
export declare const GRID_SEED_WAITING_LABEL = "Waiting";
|
|
9
|
+
export interface GridSeedCell {
|
|
10
|
+
readonly origin: PixelOrigin;
|
|
11
|
+
readonly size: PixelSize;
|
|
12
|
+
/** What the card is labelled with — a camera name, or its id. */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
}
|
|
15
|
+
export interface GridSeedSpec {
|
|
16
|
+
readonly canvas: PixelSize;
|
|
17
|
+
readonly cells: readonly GridSeedCell[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The label for one source.
|
|
21
|
+
*
|
|
22
|
+
* A name we could not read is not a reason to paint an anonymous rectangle: the
|
|
23
|
+
* DEVICE ID is what every log line about that tile carries, so it is the one
|
|
24
|
+
* label an operator can act on. A blank or whitespace name is treated as no
|
|
25
|
+
* name for the same reason.
|
|
26
|
+
*/
|
|
27
|
+
export declare function gridSeedName(names: ReadonlyMap<number, string>, deviceId: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* The seed a tile plan implies — pure, and the plan is the ONLY input to the
|
|
30
|
+
* geometry. A card is its tile's rectangle, never a recomputed one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildGridSeedSpec(plan: GridTilePlan, names: ReadonlyMap<number, string>): GridSeedSpec;
|
|
33
|
+
/**
|
|
34
|
+
* The document the seed is rasterized from.
|
|
35
|
+
*
|
|
36
|
+
* No full-bleed background: the uncovered part of a canvas is NOT a cell — the
|
|
37
|
+
* cells need not cover it (`grid-canvas-derivation.ts`) — and painting a card's
|
|
38
|
+
* treatment over it would say "waiting" about a tile that does not exist. It
|
|
39
|
+
* stays the canvas' own black.
|
|
40
|
+
*
|
|
41
|
+
* Each card clips its own text. A camera named for a whole street would
|
|
42
|
+
* otherwise spill across the cell next to it, and a label on the wrong tile is
|
|
43
|
+
* worse than a truncated one.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildGridSeedSvg(spec: GridSeedSpec): string;
|
|
46
|
+
/**
|
|
47
|
+
* Packed RGB → yuv420p, once.
|
|
48
|
+
*
|
|
49
|
+
* Chroma is the average of each 2×2 block's colour, converted once per block —
|
|
50
|
+
* which is what 4:2:0 means, and what keeps a card's border from fringing.
|
|
51
|
+
* Sizes must be even; the canvas refuses anything else, so a caller that got
|
|
52
|
+
* here with an odd size has a bug the blit would hide.
|
|
53
|
+
*/
|
|
54
|
+
export declare function rgbToYuv420p(rgb: Buffer, size: PixelSize): Buffer;
|
|
55
|
+
/**
|
|
56
|
+
* Render the seed as one yuv420p frame the canvas can take whole.
|
|
57
|
+
*
|
|
58
|
+
* Rejects on failure rather than answering black: the caller decides, and it
|
|
59
|
+
* decides to carry on without a seed — a grid that would not start because its
|
|
60
|
+
* placeholder would not draw is a worse grid than one that starts black.
|
|
61
|
+
*/
|
|
62
|
+
export declare function renderGridSeed(spec: GridSeedSpec): Promise<Buffer>;
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* A canvas has no pairing to get wrong. When the output tick fires, each
|
|
22
22
|
* rectangle holds whatever arrived last from its source, and the OUTPUT owns
|
|
23
23
|
* the clock. The gap is zero by construction rather than by tuning, and a cell
|
|
24
|
-
* that has produced nothing yet
|
|
25
|
-
*
|
|
24
|
+
* that has produced nothing yet costs nothing — so the picture appears at once
|
|
25
|
+
* instead of waiting for the slowest source's first key frame. What such a cell
|
|
26
|
+
* shows is the seed the compositor blits in at construction
|
|
27
|
+
* (`grid-canvas-seed.ts`); `clear()` below is what it shows without one.
|
|
26
28
|
*
|
|
27
29
|
* ## Why yuv420p, and why everything here must be even
|
|
28
30
|
*
|
|
@@ -5,5 +5,15 @@ export interface GridChildContext {
|
|
|
5
5
|
readonly logger: IScopedLogger;
|
|
6
6
|
/** The COMPOSITE's device id — every line the primitive emits is tagged with it. */
|
|
7
7
|
readonly deviceId: number;
|
|
8
|
+
/**
|
|
9
|
+
* The SOURCE cameras' names, for the cards the canvas is seeded with
|
|
10
|
+
* (`grid-canvas-seed.ts`). Read at the point of use rather than mirrored:
|
|
11
|
+
* this addon does not own those devices, and a name it cached would be the
|
|
12
|
+
* second copy of a fact the device manager already owns (D224).
|
|
13
|
+
*
|
|
14
|
+
* Optional, and a failure is not one: a card with no name carries the device
|
|
15
|
+
* id, which is what every log line about that tile carries anyway.
|
|
16
|
+
*/
|
|
17
|
+
readonly resolveSourceNames?: (deviceIds: readonly number[]) => Promise<ReadonlyMap<number, string>>;
|
|
8
18
|
}
|
|
9
19
|
export declare function startGridChild(ctx: GridChildContext, plan: GridStreamPlan, sources: readonly GridSourceHandle[], onEnded: (reason: string) => void): Promise<GridStreamChild>;
|
|
@@ -7,6 +7,16 @@ export interface GridCompositorDeps {
|
|
|
7
7
|
* 345 KB each is how a stalled encoder becomes an OOM.
|
|
8
8
|
*/
|
|
9
9
|
readonly write: (frame: Buffer) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The picture the canvas carries before any tile has arrived — one card per
|
|
12
|
+
* cell, naming its camera (`grid-canvas-seed.ts`). `null` ⇒ black, which is
|
|
13
|
+
* what this was before the seed and what it falls back to when the seed
|
|
14
|
+
* cannot be drawn.
|
|
15
|
+
*
|
|
16
|
+
* Blitted ONCE, here, so the very first tick already carries it: the tick
|
|
17
|
+
* serves every cell and must never wait on a render.
|
|
18
|
+
*/
|
|
19
|
+
readonly seed: Buffer | null;
|
|
10
20
|
}
|
|
11
21
|
export declare class GridCompositor {
|
|
12
22
|
private readonly deps;
|
|
@@ -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-C_4Xt0Ya.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");
|
|
@@ -2026,6 +2026,200 @@ async function silenceAnalysisFor(deps, deviceId) {
|
|
|
2026
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("; ")})`);
|
|
2027
2027
|
}
|
|
2028
2028
|
//#endregion
|
|
2029
|
+
//#region src/builtins/camera-grid/grid-canvas-seed.ts
|
|
2030
|
+
/**
|
|
2031
|
+
* What the canvas SAYS before a single camera has delivered a frame.
|
|
2032
|
+
*
|
|
2033
|
+
* ## Why black was not good enough
|
|
2034
|
+
*
|
|
2035
|
+
* The compositor emits from its very first tick (`grid-compositor.ts`) — that
|
|
2036
|
+
* is the whole point of a canvas, and it is what removed the ~9 s cold start.
|
|
2037
|
+
* But what it emitted until now was black, and black is the same picture for
|
|
2038
|
+
* four different situations: a grid with no cells, a grid whose sources are all
|
|
2039
|
+
* broken, a grid whose sources are merely slow, and a viewer that connected to
|
|
2040
|
+
* the wrong camera. An operator cannot tell them apart, and the one they
|
|
2041
|
+
* actually hit — sources still opening — is the one that fixes itself.
|
|
2042
|
+
*
|
|
2043
|
+
* The seed paints the LAYOUT instead: one card per cell, at the cell's own
|
|
2044
|
+
* rectangle, carrying the camera's name and the word `Waiting`. Three states
|
|
2045
|
+
* become three pictures — "no cards" is an empty grid, "a card still there" is
|
|
2046
|
+
* a tile that has not arrived, and "a picture" is a tile that has.
|
|
2047
|
+
*
|
|
2048
|
+
* It is honest about what it is. It is not a stand-in picture of the camera,
|
|
2049
|
+
* and it never carries a state it has not been told (a tile whose source is
|
|
2050
|
+
* genuinely broken is a matter for the tile's own decoder and its logs — this
|
|
2051
|
+
* says only "nothing has arrived here yet", which is true of both).
|
|
2052
|
+
*
|
|
2053
|
+
* ## Drawn ONCE, before the tick exists
|
|
2054
|
+
*
|
|
2055
|
+
* The seed is a full-canvas frame blitted in at the compositor's construction,
|
|
2056
|
+
* so the first tick already carries it and the tick never waits on anything.
|
|
2057
|
+
* Rendering it is the only asynchronous part, and it happens BEFORE the
|
|
2058
|
+
* compositor is built — never on the tick, which serves every cell.
|
|
2059
|
+
*
|
|
2060
|
+
* ## Why SVG → sharp, and why the conversion is here
|
|
2061
|
+
*
|
|
2062
|
+
* `snapshot-courtesy.ts` already renders text this way and states the reason:
|
|
2063
|
+
* shelling out to ffmpeg's `drawtext` was 52 ms against sharp's 4.8 ms, and an
|
|
2064
|
+
* SVG names a font FAMILY that fontconfig resolves instead of a font PATH that
|
|
2065
|
+
* has to be probed. This is that pattern, not a second one.
|
|
2066
|
+
*
|
|
2067
|
+
* What is different is the output. The canvas is yuv420p — that is what makes a
|
|
2068
|
+
* blit a strided memcpy — and sharp has no yuv output, so the raster is
|
|
2069
|
+
* converted once, here, by `rgbToYuv420p`. Once per composition start, over a
|
|
2070
|
+
* frame that is already being allocated; never per tick.
|
|
2071
|
+
*
|
|
2072
|
+
* ## Even, like everything else that touches the canvas
|
|
2073
|
+
*
|
|
2074
|
+
* The cards are the TILES' own rectangles, which `buildGridTilePlan` has
|
|
2075
|
+
* already snapped to even pixels — yuv420p subsamples chroma 2×2, and an odd
|
|
2076
|
+
* rectangle tints a tile rather than breaking it. Nothing here re-derives a
|
|
2077
|
+
* rectangle; it copies the one the decoder was sized against, so a card cannot
|
|
2078
|
+
* land anywhere other than exactly where its picture will.
|
|
2079
|
+
*/
|
|
2080
|
+
/**
|
|
2081
|
+
* The word a card carries. Deliberately not a fault: nothing has arrived YET,
|
|
2082
|
+
* which is true of a camera that is slow and of one that is broken, and this
|
|
2083
|
+
* placeholder knows which it is for neither.
|
|
2084
|
+
*/
|
|
2085
|
+
var GRID_SEED_WAITING_LABEL = "Waiting";
|
|
2086
|
+
/**
|
|
2087
|
+
* Family names, not paths — librsvg resolves them through fontconfig, and the
|
|
2088
|
+
* trailing generics keep a host without DejaVu rendering something legible
|
|
2089
|
+
* rather than nothing. Same stack as the courtesy frame, for the same reason.
|
|
2090
|
+
*/
|
|
2091
|
+
var SEED_FONT_STACK = "DejaVu Sans,DejaVu Sans Mono,Helvetica,Arial,sans-serif";
|
|
2092
|
+
/** The card. Dark enough to read as "empty", lit enough to read as "a cell". */
|
|
2093
|
+
var CARD_FILL = "#1b1b21";
|
|
2094
|
+
var CARD_BORDER = "#3f3f4c";
|
|
2095
|
+
var CARD_NAME = "#e8e8ee";
|
|
2096
|
+
var CARD_STATE = "#8f8f9e";
|
|
2097
|
+
/**
|
|
2098
|
+
* The label for one source.
|
|
2099
|
+
*
|
|
2100
|
+
* A name we could not read is not a reason to paint an anonymous rectangle: the
|
|
2101
|
+
* DEVICE ID is what every log line about that tile carries, so it is the one
|
|
2102
|
+
* label an operator can act on. A blank or whitespace name is treated as no
|
|
2103
|
+
* name for the same reason.
|
|
2104
|
+
*/
|
|
2105
|
+
function gridSeedName(names, deviceId) {
|
|
2106
|
+
const name = names.get(deviceId)?.trim();
|
|
2107
|
+
return name === void 0 || name.length === 0 ? `Camera ${String(deviceId)}` : name;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* The seed a tile plan implies — pure, and the plan is the ONLY input to the
|
|
2111
|
+
* geometry. A card is its tile's rectangle, never a recomputed one.
|
|
2112
|
+
*/
|
|
2113
|
+
function buildGridSeedSpec(plan, names) {
|
|
2114
|
+
return {
|
|
2115
|
+
canvas: plan.canvas,
|
|
2116
|
+
cells: plan.tiles.map((tile) => ({
|
|
2117
|
+
origin: tile.origin,
|
|
2118
|
+
size: tile.size,
|
|
2119
|
+
name: gridSeedName(names, tile.deviceId)
|
|
2120
|
+
}))
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
/** XML escaping for text placed inside an SVG `<text>` node. */
|
|
2124
|
+
function escapeSeedText(value) {
|
|
2125
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* The document the seed is rasterized from.
|
|
2129
|
+
*
|
|
2130
|
+
* No full-bleed background: the uncovered part of a canvas is NOT a cell — the
|
|
2131
|
+
* cells need not cover it (`grid-canvas-derivation.ts`) — and painting a card's
|
|
2132
|
+
* treatment over it would say "waiting" about a tile that does not exist. It
|
|
2133
|
+
* stays the canvas' own black.
|
|
2134
|
+
*
|
|
2135
|
+
* Each card clips its own text. A camera named for a whole street would
|
|
2136
|
+
* otherwise spill across the cell next to it, and a label on the wrong tile is
|
|
2137
|
+
* worse than a truncated one.
|
|
2138
|
+
*/
|
|
2139
|
+
function buildGridSeedSvg(spec) {
|
|
2140
|
+
const parts = [`<svg xmlns="http://www.w3.org/2000/svg" width="${String(spec.canvas.width)}" height="${String(spec.canvas.height)}">`];
|
|
2141
|
+
for (const [index, cell] of spec.cells.entries()) {
|
|
2142
|
+
const { width, height } = cell.size;
|
|
2143
|
+
const nameSize = Math.max(9, Math.min(Math.round(width / 12), Math.round(height / 8)));
|
|
2144
|
+
const stateSize = Math.max(8, Math.round(nameSize * .62));
|
|
2145
|
+
const stroke = Math.max(2, Math.round(Math.min(width, height) / 100));
|
|
2146
|
+
const inset = stroke / 2;
|
|
2147
|
+
const centreX = cell.origin.x + width / 2;
|
|
2148
|
+
const centreY = cell.origin.y + height / 2;
|
|
2149
|
+
const clipId = `seed-cell-${String(index)}`;
|
|
2150
|
+
parts.push(`<clipPath id="${clipId}"><rect x="${String(cell.origin.x)}" y="${String(cell.origin.y)}" width="${String(width)}" height="${String(height)}"/></clipPath>`, `<rect x="${String(cell.origin.x + inset)}" y="${String(cell.origin.y + inset)}" width="${String(width - stroke)}" height="${String(height - stroke)}" fill="${CARD_FILL}" stroke="${CARD_BORDER}" stroke-width="${String(stroke)}"/>`, `<g clip-path="url(#${clipId})" font-family="${SEED_FONT_STACK}" text-anchor="middle">`, `<text x="${String(centreX)}" y="${String(Math.round(centreY))}" font-size="${String(nameSize)}" fill="${CARD_NAME}">${escapeSeedText(cell.name)}</text>`, `<text x="${String(centreX)}" y="${String(Math.round(centreY + nameSize * 1.15))}" font-size="${String(stateSize)}" fill="${CARD_STATE}">${GRID_SEED_WAITING_LABEL}</text>`, `</g>`);
|
|
2151
|
+
}
|
|
2152
|
+
parts.push("</svg>");
|
|
2153
|
+
return parts.join("");
|
|
2154
|
+
}
|
|
2155
|
+
/** Limited-range BT.601, which is what a raw yuv420p stream handed to x264 is read as. */
|
|
2156
|
+
var Y_OFFSET = 16;
|
|
2157
|
+
var CHROMA_OFFSET = 128;
|
|
2158
|
+
function clampByte(value) {
|
|
2159
|
+
return Math.min(255, Math.max(0, Math.round(value)));
|
|
2160
|
+
}
|
|
2161
|
+
function luma(r, g, b) {
|
|
2162
|
+
return Y_OFFSET + (65.481 * r + 128.553 * g + 24.966 * b) / 255;
|
|
2163
|
+
}
|
|
2164
|
+
function chromaU(r, g, b) {
|
|
2165
|
+
return CHROMA_OFFSET + (-37.797 * r - 74.203 * g + 112 * b) / 255;
|
|
2166
|
+
}
|
|
2167
|
+
function chromaV(r, g, b) {
|
|
2168
|
+
return CHROMA_OFFSET + (112 * r - 93.786 * g - 18.214 * b) / 255;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Packed RGB → yuv420p, once.
|
|
2172
|
+
*
|
|
2173
|
+
* Chroma is the average of each 2×2 block's colour, converted once per block —
|
|
2174
|
+
* which is what 4:2:0 means, and what keeps a card's border from fringing.
|
|
2175
|
+
* Sizes must be even; the canvas refuses anything else, so a caller that got
|
|
2176
|
+
* here with an odd size has a bug the blit would hide.
|
|
2177
|
+
*/
|
|
2178
|
+
function rgbToYuv420p(rgb, size) {
|
|
2179
|
+
const { width, height } = size;
|
|
2180
|
+
const out = Buffer.alloc(yuv420pSize(width, height));
|
|
2181
|
+
const lumaBytes = width * height;
|
|
2182
|
+
const chromaBytes = width / 2 * (height / 2);
|
|
2183
|
+
for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
|
|
2184
|
+
const at = (y * width + x) * 3;
|
|
2185
|
+
out[y * width + x] = clampByte(luma(rgb[at] ?? 0, rgb[at + 1] ?? 0, rgb[at + 2] ?? 0));
|
|
2186
|
+
}
|
|
2187
|
+
const halfW = width / 2;
|
|
2188
|
+
for (let y = 0; y < height / 2; y += 1) for (let x = 0; x < halfW; x += 1) {
|
|
2189
|
+
let r = 0;
|
|
2190
|
+
let g = 0;
|
|
2191
|
+
let b = 0;
|
|
2192
|
+
for (let dy = 0; dy < 2; dy += 1) for (let dx = 0; dx < 2; dx += 1) {
|
|
2193
|
+
const at = ((y * 2 + dy) * width + (x * 2 + dx)) * 3;
|
|
2194
|
+
r += rgb[at] ?? 0;
|
|
2195
|
+
g += rgb[at + 1] ?? 0;
|
|
2196
|
+
b += rgb[at + 2] ?? 0;
|
|
2197
|
+
}
|
|
2198
|
+
const at = y * halfW + x;
|
|
2199
|
+
out[lumaBytes + at] = clampByte(chromaU(r / 4, g / 4, b / 4));
|
|
2200
|
+
out[lumaBytes + chromaBytes + at] = clampByte(chromaV(r / 4, g / 4, b / 4));
|
|
2201
|
+
}
|
|
2202
|
+
return out;
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Render the seed as one yuv420p frame the canvas can take whole.
|
|
2206
|
+
*
|
|
2207
|
+
* Rejects on failure rather than answering black: the caller decides, and it
|
|
2208
|
+
* decides to carry on without a seed — a grid that would not start because its
|
|
2209
|
+
* placeholder would not draw is a worse grid than one that starts black.
|
|
2210
|
+
*/
|
|
2211
|
+
async function renderGridSeed(spec) {
|
|
2212
|
+
const { default: sharp } = await import("sharp");
|
|
2213
|
+
const rgb = await sharp(Buffer.from(buildGridSeedSvg(spec))).flatten({ background: {
|
|
2214
|
+
r: 0,
|
|
2215
|
+
g: 0,
|
|
2216
|
+
b: 0
|
|
2217
|
+
} }).raw().toBuffer();
|
|
2218
|
+
const expected = spec.canvas.width * spec.canvas.height * 3;
|
|
2219
|
+
if (rgb.length !== expected) throw new Error(`camera-grid: the seed rasterized to ${String(rgb.length)} bytes, expected ${String(expected)}`);
|
|
2220
|
+
return rgbToYuv420p(rgb, spec.canvas);
|
|
2221
|
+
}
|
|
2222
|
+
//#endregion
|
|
2029
2223
|
//#region src/builtins/camera-grid/grid-tile-feed.ts
|
|
2030
2224
|
/**
|
|
2031
2225
|
* One source's decoded tiles, of which only the LATEST is ever wanted.
|
|
@@ -2120,11 +2314,16 @@ var GridTileFeed = class {
|
|
|
2120
2314
|
*
|
|
2121
2315
|
* ## It also removes the wait
|
|
2122
2316
|
*
|
|
2123
|
-
* A cell with no frame yet is
|
|
2124
|
-
*
|
|
2317
|
+
* A cell with no frame yet is not a reason to hold the picture back. The output
|
|
2318
|
+
* exists from the first tick and cells fill in as their sources arrive —
|
|
2125
2319
|
* instead of the whole grid waiting for the slowest source's first key frame,
|
|
2126
2320
|
* which is what the ~9 s cold start was.
|
|
2127
2321
|
*
|
|
2322
|
+
* What a cell with no frame yet SHOWS is the seed (`grid-canvas-seed.ts`),
|
|
2323
|
+
* blitted in once at construction: its own rectangle, with its camera's name
|
|
2324
|
+
* and the word `Waiting`. It was black, and black said the same thing about an
|
|
2325
|
+
* empty grid, a broken grid and a grid that is merely still opening.
|
|
2326
|
+
*
|
|
2128
2327
|
* ## What it costs
|
|
2129
2328
|
*
|
|
2130
2329
|
* The same N decodes ffmpeg already paid, in N children instead of one, plus
|
|
@@ -2143,6 +2342,10 @@ var GridCompositor = class {
|
|
|
2143
2342
|
constructor(deps) {
|
|
2144
2343
|
this.deps = deps;
|
|
2145
2344
|
this.canvas = new GridCanvas(deps.plan.canvas.width, deps.plan.canvas.height);
|
|
2345
|
+
if (deps.seed !== null) this.canvas.blit(deps.seed, deps.plan.canvas, {
|
|
2346
|
+
x: 0,
|
|
2347
|
+
y: 0
|
|
2348
|
+
});
|
|
2146
2349
|
for (const tile of deps.plan.tiles) this.placed.set(tile.id, {
|
|
2147
2350
|
tile,
|
|
2148
2351
|
feed: new GridTileFeed(tile.frameBytes)
|
|
@@ -2385,11 +2588,17 @@ function gridCanvasEncoderInvocation(input) {
|
|
|
2385
2588
|
*
|
|
2386
2589
|
* ## The order things start in, and why it is the order
|
|
2387
2590
|
*
|
|
2388
|
-
* The
|
|
2389
|
-
* canvas from its very first beat, so the encoder produces a stream
|
|
2390
|
-
* consumer sees a picture — before any camera has delivered anything.
|
|
2391
|
-
* the cold start the old shape spent waiting for the slowest source's
|
|
2392
|
-
* frame, and it is now spent watching cells fill in.
|
|
2591
|
+
* The seed first, then the encoder, then the tick, then the decoders. The tick
|
|
2592
|
+
* writes the canvas from its very first beat, so the encoder produces a stream
|
|
2593
|
+
* — and the consumer sees a picture — before any camera has delivered anything.
|
|
2594
|
+
* That is the cold start the old shape spent waiting for the slowest source's
|
|
2595
|
+
* first key frame, and it is now spent watching cells fill in.
|
|
2596
|
+
*
|
|
2597
|
+
* What that first picture SAYS is the seed (`grid-canvas-seed.ts`): one card
|
|
2598
|
+
* per cell, at the cell's own rectangle, naming its camera. It is drawn once,
|
|
2599
|
+
* here, before the compositor is built — a tick must never wait on a render —
|
|
2600
|
+
* and it is bounded and fallible: a seed that cannot be drawn leaves the canvas
|
|
2601
|
+
* black, which is what it was before, and never stops the composition.
|
|
2393
2602
|
*
|
|
2394
2603
|
* ## Back-pressure is a SKIPPED tick, never a queue
|
|
2395
2604
|
*
|
|
@@ -2436,6 +2645,77 @@ var DECODER_FIRST_DATA_MS = 15e3;
|
|
|
2436
2645
|
* simply replaced while its cell holds the last frame it produced.
|
|
2437
2646
|
*/
|
|
2438
2647
|
var TILE_MAX_RESTARTS = 5;
|
|
2648
|
+
/**
|
|
2649
|
+
* How long the seed may wait for the source cameras' names.
|
|
2650
|
+
*
|
|
2651
|
+
* The seed is drawn BEFORE the encoder is spawned — that is what puts it on the
|
|
2652
|
+
* first tick — so everything it waits for is added to the cold start the rest
|
|
2653
|
+
* of this file exists to keep short. A name is a nicety; the card exists
|
|
2654
|
+
* either way, labelled by device id. So the read is bounded and the deadline
|
|
2655
|
+
* is not an error.
|
|
2656
|
+
*/
|
|
2657
|
+
var SEED_NAMES_TIMEOUT_MS = 1500;
|
|
2658
|
+
/**
|
|
2659
|
+
* The names for the cards, or an empty map.
|
|
2660
|
+
*
|
|
2661
|
+
* Never throws and never delays past the deadline: an unnamed card is a
|
|
2662
|
+
* complete card (`gridSeedName` falls back to the device id), and a grid that
|
|
2663
|
+
* started late because a name lookup was slow would be a worse grid.
|
|
2664
|
+
*/
|
|
2665
|
+
async function resolveSeedNames(ctx, deviceIds) {
|
|
2666
|
+
const resolve = ctx.resolveSourceNames;
|
|
2667
|
+
if (resolve === void 0) return /* @__PURE__ */ new Map();
|
|
2668
|
+
const empty = /* @__PURE__ */ new Map();
|
|
2669
|
+
return new Promise((settle) => {
|
|
2670
|
+
const timer = setTimeout(() => {
|
|
2671
|
+
ctx.logger.warn("camera grid: the seed could not read the source names in time", {
|
|
2672
|
+
tags: { deviceId: ctx.deviceId },
|
|
2673
|
+
meta: {
|
|
2674
|
+
sources: deviceIds.length,
|
|
2675
|
+
timeoutMs: SEED_NAMES_TIMEOUT_MS
|
|
2676
|
+
}
|
|
2677
|
+
});
|
|
2678
|
+
settle(empty);
|
|
2679
|
+
}, SEED_NAMES_TIMEOUT_MS);
|
|
2680
|
+
timer.unref();
|
|
2681
|
+
resolve(deviceIds).then((names) => {
|
|
2682
|
+
clearTimeout(timer);
|
|
2683
|
+
settle(names);
|
|
2684
|
+
}, (error) => {
|
|
2685
|
+
clearTimeout(timer);
|
|
2686
|
+
ctx.logger.warn("camera grid: the seed could not read the source names", {
|
|
2687
|
+
tags: { deviceId: ctx.deviceId },
|
|
2688
|
+
meta: {
|
|
2689
|
+
sources: deviceIds.length,
|
|
2690
|
+
error: require_dist.errMsg(error)
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
settle(empty);
|
|
2694
|
+
});
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* The picture the canvas starts as: one card per cell, naming its camera.
|
|
2699
|
+
*
|
|
2700
|
+
* `null` — black, which is what this was before — when it cannot be drawn. A
|
|
2701
|
+
* placeholder that refused to render must never be the reason a grid does not
|
|
2702
|
+
* start, so every failure here is a warning and a composition that carries on.
|
|
2703
|
+
*/
|
|
2704
|
+
async function buildCanvasSeed(ctx, tilePlan) {
|
|
2705
|
+
const names = await resolveSeedNames(ctx, [...new Set(tilePlan.tiles.map((tile) => tile.deviceId))]);
|
|
2706
|
+
try {
|
|
2707
|
+
return await renderGridSeed(buildGridSeedSpec(tilePlan, names));
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
ctx.logger.warn("camera grid: the canvas seed could not be drawn - starting black", {
|
|
2710
|
+
tags: { deviceId: ctx.deviceId },
|
|
2711
|
+
meta: {
|
|
2712
|
+
error: require_dist.errMsg(error),
|
|
2713
|
+
tiles: tilePlan.tiles.length
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
return null;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2439
2719
|
async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
2440
2720
|
const tilePlan = buildGridTilePlan({
|
|
2441
2721
|
canvas: {
|
|
@@ -2455,12 +2735,14 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2455
2735
|
ended = true;
|
|
2456
2736
|
onEnded(reason);
|
|
2457
2737
|
};
|
|
2738
|
+
const seed = await buildCanvasSeed(ctx, tilePlan);
|
|
2458
2739
|
let encoderIn = null;
|
|
2459
2740
|
let encoderOut = null;
|
|
2460
2741
|
let draining = false;
|
|
2461
2742
|
let tick = null;
|
|
2462
2743
|
const compositor = new GridCompositor({
|
|
2463
2744
|
plan: tilePlan,
|
|
2745
|
+
seed,
|
|
2464
2746
|
write: (frame) => {
|
|
2465
2747
|
const sink = encoderIn;
|
|
2466
2748
|
if (sink === null || draining || sink.destroyed) return false;
|
|
@@ -2577,7 +2859,8 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2577
2859
|
meta: {
|
|
2578
2860
|
tiles: tilePlan.tiles.length,
|
|
2579
2861
|
canvas: `${String(tilePlan.canvas.width)}x${String(tilePlan.canvas.height)}`,
|
|
2580
|
-
fps: plan.fps
|
|
2862
|
+
fps: plan.fps,
|
|
2863
|
+
seeded: seed !== null
|
|
2581
2864
|
}
|
|
2582
2865
|
});
|
|
2583
2866
|
return {
|
|
@@ -3006,11 +3289,39 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
3006
3289
|
this.sessions.set(key, new GridSessionEntry(instanceId, profile, this.makeSession(instanceId, deviceId, profile)));
|
|
3007
3290
|
}
|
|
3008
3291
|
}
|
|
3292
|
+
/**
|
|
3293
|
+
* The source cameras' names, for the cards the canvas is seeded with.
|
|
3294
|
+
*
|
|
3295
|
+
* Asked of the device manager, which OWNS the name, at the moment the seed is
|
|
3296
|
+
* drawn — never kept here. A consumer that mirrored this would be stale on
|
|
3297
|
+
* exactly the write an operator makes (D224), for a label.
|
|
3298
|
+
*
|
|
3299
|
+
* A device that cannot be read is simply absent from the map, and its card
|
|
3300
|
+
* carries the device id instead. One unreadable camera never costs the other
|
|
3301
|
+
* cards their names, which is why these are settled rather than awaited as
|
|
3302
|
+
* one batch.
|
|
3303
|
+
*/
|
|
3304
|
+
async sourceNames(deviceIds) {
|
|
3305
|
+
const names = /* @__PURE__ */ new Map();
|
|
3306
|
+
const results = await Promise.allSettled(deviceIds.map(async (deviceId) => {
|
|
3307
|
+
return {
|
|
3308
|
+
deviceId,
|
|
3309
|
+
name: (await this.ctx.api.deviceManager.getDevice.query({ deviceId }))?.name ?? null
|
|
3310
|
+
};
|
|
3311
|
+
}));
|
|
3312
|
+
for (const result of results) {
|
|
3313
|
+
if (result.status !== "fulfilled") continue;
|
|
3314
|
+
const { deviceId, name } = result.value;
|
|
3315
|
+
if (name !== null) names.set(deviceId, name);
|
|
3316
|
+
}
|
|
3317
|
+
return names;
|
|
3318
|
+
}
|
|
3009
3319
|
makeSession(instanceId, deviceId, profile) {
|
|
3010
3320
|
const childContext = {
|
|
3011
3321
|
binaryPath: this.ffmpegBinaryPath,
|
|
3012
3322
|
logger: this.ctx.logger,
|
|
3013
|
-
deviceId
|
|
3323
|
+
deviceId,
|
|
3324
|
+
resolveSourceNames: (deviceIds) => this.sourceNames(deviceIds)
|
|
3014
3325
|
};
|
|
3015
3326
|
const session = new GridStreamSession({
|
|
3016
3327
|
deviceId,
|