@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.
Files changed (63) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/addon.d.ts +13 -0
  12. package/dist/builtins/camera-grid/grid-canvas-seed.d.ts +62 -0
  13. package/dist/builtins/camera-grid/grid-canvas.d.ts +4 -2
  14. package/dist/builtins/camera-grid/grid-child.d.ts +10 -0
  15. package/dist/builtins/camera-grid/grid-compositor.d.ts +10 -0
  16. package/dist/builtins/camera-grid/index.js +321 -10
  17. package/dist/builtins/camera-grid/index.mjs +321 -10
  18. package/dist/builtins/console-logging/index.js +1 -1
  19. package/dist/builtins/console-logging/index.mjs +1 -1
  20. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  22. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  23. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  24. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  25. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  26. package/dist/builtins/hub-forwarder/index.js +1 -1
  27. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  30. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  32. package/dist/builtins/local-network/local-network.addon.js +1 -1
  33. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  34. package/dist/builtins/loki-logging/index.js +1 -1
  35. package/dist/builtins/loki-logging/index.mjs +1 -1
  36. package/dist/builtins/native-metrics/cgroup-pids.d.ts +112 -0
  37. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +1 -0
  38. package/dist/builtins/native-metrics/native-metrics.addon.js +209 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.mjs +209 -1
  40. package/dist/builtins/platform-probe/index.js +1 -1
  41. package/dist/builtins/platform-probe/index.mjs +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/snapshot/index.js +1 -1
  45. package/dist/builtins/snapshot/index.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/system-config/system-config.addon.js +1 -1
  53. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  54. package/dist/builtins/winston-logging/index.js +1 -1
  55. package/dist/builtins/winston-logging/index.mjs +1 -1
  56. package/dist/{dist-_YWIPtUH.js → dist-C_4Xt0Ya.js} +209 -11
  57. package/dist/{dist-Diz59Dhi.mjs → dist-DchKwh_m.mjs} +209 -11
  58. package/dist/index.js +12 -5
  59. package/dist/index.mjs +12 -5
  60. package/dist/kernel/moleculer/process-service.d.ts +54 -0
  61. package/dist/{retired-settings-keys-C2ocoH_l.js → retired-settings-keys-6wZNp7lr.js} +1 -1
  62. package/dist/{retired-settings-keys-C_Hg5d37.mjs → retired-settings-keys-DPSHUHMu.mjs} +1 -1
  63. 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-_YWIPtUH.js");
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-Diz59Dhi.mjs";
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-_YWIPtUH.js");
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-Diz59Dhi.mjs";
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-_YWIPtUH.js");
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-Diz59Dhi.mjs";
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-_YWIPtUH.js");
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-Diz59Dhi.mjs";
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-_YWIPtUH.js");
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-Diz59Dhi.mjs";
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 is simply black — so the picture appears at
25
- * once instead of waiting for the slowest source's first key frame.
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-_YWIPtUH.js");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
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 BLACK, not a reason to hold the picture back. The
2124
- * output exists from the first tick and cells fill in as their sources arrive —
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 encoder first, then the tick, then the decoders. The tick writes a black
2389
- * canvas from its very first beat, so the encoder produces a stream — and the
2390
- * consumer sees a picture — before any camera has delivered anything. That is
2391
- * the cold start the old shape spent waiting for the slowest source's first key
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,