@camstack/system 1.2.272 → 1.2.273

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-D5ONNqL6.js} +187 -11
  57. package/dist/{dist-Diz59Dhi.mjs → dist-DrLhdcwI.mjs} +187 -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-1686ou3s.js} +1 -1
  62. package/dist/{retired-settings-keys-C_Hg5d37.mjs → retired-settings-keys-Du_0qedo.mjs} +1 -1
  63. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { $t as streamCatalogCapability, At as motionCapability, Jt as snapshotCapability, Pn as EventCategory, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, an as errMsg, b as MOTION_CLOSE_AFTER_MS, bn as createEvent, dn as DeviceFeature, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, i as BaseDevice, on as buildFfmpegArgs, p as DeclaredDevices, pn as DeviceType, sn as BaseAddon, wn as hydrateSchema } from "../../dist-Diz59Dhi.mjs";
1
+ import { $t as streamCatalogCapability, At as motionCapability, Jt as snapshotCapability, Pn as EventCategory, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, an as errMsg, b as MOTION_CLOSE_AFTER_MS, bn as createEvent, dn as DeviceFeature, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, i as BaseDevice, on as buildFfmpegArgs, p as DeclaredDevices, pn as DeviceType, sn as BaseAddon, wn as hydrateSchema } from "../../dist-DrLhdcwI.mjs";
2
2
  import { z } from "zod";
3
3
  import { FfmpegProcess } from "@camstack/types/node";
4
4
  import { join } from "node:path";
@@ -2021,6 +2021,200 @@ async function silenceAnalysisFor(deps, deviceId) {
2021
2021
  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("; ")})`);
2022
2022
  }
2023
2023
  //#endregion
2024
+ //#region src/builtins/camera-grid/grid-canvas-seed.ts
2025
+ /**
2026
+ * What the canvas SAYS before a single camera has delivered a frame.
2027
+ *
2028
+ * ## Why black was not good enough
2029
+ *
2030
+ * The compositor emits from its very first tick (`grid-compositor.ts`) — that
2031
+ * is the whole point of a canvas, and it is what removed the ~9 s cold start.
2032
+ * But what it emitted until now was black, and black is the same picture for
2033
+ * four different situations: a grid with no cells, a grid whose sources are all
2034
+ * broken, a grid whose sources are merely slow, and a viewer that connected to
2035
+ * the wrong camera. An operator cannot tell them apart, and the one they
2036
+ * actually hit — sources still opening — is the one that fixes itself.
2037
+ *
2038
+ * The seed paints the LAYOUT instead: one card per cell, at the cell's own
2039
+ * rectangle, carrying the camera's name and the word `Waiting`. Three states
2040
+ * become three pictures — "no cards" is an empty grid, "a card still there" is
2041
+ * a tile that has not arrived, and "a picture" is a tile that has.
2042
+ *
2043
+ * It is honest about what it is. It is not a stand-in picture of the camera,
2044
+ * and it never carries a state it has not been told (a tile whose source is
2045
+ * genuinely broken is a matter for the tile's own decoder and its logs — this
2046
+ * says only "nothing has arrived here yet", which is true of both).
2047
+ *
2048
+ * ## Drawn ONCE, before the tick exists
2049
+ *
2050
+ * The seed is a full-canvas frame blitted in at the compositor's construction,
2051
+ * so the first tick already carries it and the tick never waits on anything.
2052
+ * Rendering it is the only asynchronous part, and it happens BEFORE the
2053
+ * compositor is built — never on the tick, which serves every cell.
2054
+ *
2055
+ * ## Why SVG → sharp, and why the conversion is here
2056
+ *
2057
+ * `snapshot-courtesy.ts` already renders text this way and states the reason:
2058
+ * shelling out to ffmpeg's `drawtext` was 52 ms against sharp's 4.8 ms, and an
2059
+ * SVG names a font FAMILY that fontconfig resolves instead of a font PATH that
2060
+ * has to be probed. This is that pattern, not a second one.
2061
+ *
2062
+ * What is different is the output. The canvas is yuv420p — that is what makes a
2063
+ * blit a strided memcpy — and sharp has no yuv output, so the raster is
2064
+ * converted once, here, by `rgbToYuv420p`. Once per composition start, over a
2065
+ * frame that is already being allocated; never per tick.
2066
+ *
2067
+ * ## Even, like everything else that touches the canvas
2068
+ *
2069
+ * The cards are the TILES' own rectangles, which `buildGridTilePlan` has
2070
+ * already snapped to even pixels — yuv420p subsamples chroma 2×2, and an odd
2071
+ * rectangle tints a tile rather than breaking it. Nothing here re-derives a
2072
+ * rectangle; it copies the one the decoder was sized against, so a card cannot
2073
+ * land anywhere other than exactly where its picture will.
2074
+ */
2075
+ /**
2076
+ * The word a card carries. Deliberately not a fault: nothing has arrived YET,
2077
+ * which is true of a camera that is slow and of one that is broken, and this
2078
+ * placeholder knows which it is for neither.
2079
+ */
2080
+ var GRID_SEED_WAITING_LABEL = "Waiting";
2081
+ /**
2082
+ * Family names, not paths — librsvg resolves them through fontconfig, and the
2083
+ * trailing generics keep a host without DejaVu rendering something legible
2084
+ * rather than nothing. Same stack as the courtesy frame, for the same reason.
2085
+ */
2086
+ var SEED_FONT_STACK = "DejaVu Sans,DejaVu Sans Mono,Helvetica,Arial,sans-serif";
2087
+ /** The card. Dark enough to read as "empty", lit enough to read as "a cell". */
2088
+ var CARD_FILL = "#1b1b21";
2089
+ var CARD_BORDER = "#3f3f4c";
2090
+ var CARD_NAME = "#e8e8ee";
2091
+ var CARD_STATE = "#8f8f9e";
2092
+ /**
2093
+ * The label for one source.
2094
+ *
2095
+ * A name we could not read is not a reason to paint an anonymous rectangle: the
2096
+ * DEVICE ID is what every log line about that tile carries, so it is the one
2097
+ * label an operator can act on. A blank or whitespace name is treated as no
2098
+ * name for the same reason.
2099
+ */
2100
+ function gridSeedName(names, deviceId) {
2101
+ const name = names.get(deviceId)?.trim();
2102
+ return name === void 0 || name.length === 0 ? `Camera ${String(deviceId)}` : name;
2103
+ }
2104
+ /**
2105
+ * The seed a tile plan implies — pure, and the plan is the ONLY input to the
2106
+ * geometry. A card is its tile's rectangle, never a recomputed one.
2107
+ */
2108
+ function buildGridSeedSpec(plan, names) {
2109
+ return {
2110
+ canvas: plan.canvas,
2111
+ cells: plan.tiles.map((tile) => ({
2112
+ origin: tile.origin,
2113
+ size: tile.size,
2114
+ name: gridSeedName(names, tile.deviceId)
2115
+ }))
2116
+ };
2117
+ }
2118
+ /** XML escaping for text placed inside an SVG `<text>` node. */
2119
+ function escapeSeedText(value) {
2120
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
2121
+ }
2122
+ /**
2123
+ * The document the seed is rasterized from.
2124
+ *
2125
+ * No full-bleed background: the uncovered part of a canvas is NOT a cell — the
2126
+ * cells need not cover it (`grid-canvas-derivation.ts`) — and painting a card's
2127
+ * treatment over it would say "waiting" about a tile that does not exist. It
2128
+ * stays the canvas' own black.
2129
+ *
2130
+ * Each card clips its own text. A camera named for a whole street would
2131
+ * otherwise spill across the cell next to it, and a label on the wrong tile is
2132
+ * worse than a truncated one.
2133
+ */
2134
+ function buildGridSeedSvg(spec) {
2135
+ const parts = [`<svg xmlns="http://www.w3.org/2000/svg" width="${String(spec.canvas.width)}" height="${String(spec.canvas.height)}">`];
2136
+ for (const [index, cell] of spec.cells.entries()) {
2137
+ const { width, height } = cell.size;
2138
+ const nameSize = Math.max(9, Math.min(Math.round(width / 12), Math.round(height / 8)));
2139
+ const stateSize = Math.max(8, Math.round(nameSize * .62));
2140
+ const stroke = Math.max(2, Math.round(Math.min(width, height) / 100));
2141
+ const inset = stroke / 2;
2142
+ const centreX = cell.origin.x + width / 2;
2143
+ const centreY = cell.origin.y + height / 2;
2144
+ const clipId = `seed-cell-${String(index)}`;
2145
+ 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>`);
2146
+ }
2147
+ parts.push("</svg>");
2148
+ return parts.join("");
2149
+ }
2150
+ /** Limited-range BT.601, which is what a raw yuv420p stream handed to x264 is read as. */
2151
+ var Y_OFFSET = 16;
2152
+ var CHROMA_OFFSET = 128;
2153
+ function clampByte(value) {
2154
+ return Math.min(255, Math.max(0, Math.round(value)));
2155
+ }
2156
+ function luma(r, g, b) {
2157
+ return Y_OFFSET + (65.481 * r + 128.553 * g + 24.966 * b) / 255;
2158
+ }
2159
+ function chromaU(r, g, b) {
2160
+ return CHROMA_OFFSET + (-37.797 * r - 74.203 * g + 112 * b) / 255;
2161
+ }
2162
+ function chromaV(r, g, b) {
2163
+ return CHROMA_OFFSET + (112 * r - 93.786 * g - 18.214 * b) / 255;
2164
+ }
2165
+ /**
2166
+ * Packed RGB → yuv420p, once.
2167
+ *
2168
+ * Chroma is the average of each 2×2 block's colour, converted once per block —
2169
+ * which is what 4:2:0 means, and what keeps a card's border from fringing.
2170
+ * Sizes must be even; the canvas refuses anything else, so a caller that got
2171
+ * here with an odd size has a bug the blit would hide.
2172
+ */
2173
+ function rgbToYuv420p(rgb, size) {
2174
+ const { width, height } = size;
2175
+ const out = Buffer.alloc(yuv420pSize(width, height));
2176
+ const lumaBytes = width * height;
2177
+ const chromaBytes = width / 2 * (height / 2);
2178
+ for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
2179
+ const at = (y * width + x) * 3;
2180
+ out[y * width + x] = clampByte(luma(rgb[at] ?? 0, rgb[at + 1] ?? 0, rgb[at + 2] ?? 0));
2181
+ }
2182
+ const halfW = width / 2;
2183
+ for (let y = 0; y < height / 2; y += 1) for (let x = 0; x < halfW; x += 1) {
2184
+ let r = 0;
2185
+ let g = 0;
2186
+ let b = 0;
2187
+ for (let dy = 0; dy < 2; dy += 1) for (let dx = 0; dx < 2; dx += 1) {
2188
+ const at = ((y * 2 + dy) * width + (x * 2 + dx)) * 3;
2189
+ r += rgb[at] ?? 0;
2190
+ g += rgb[at + 1] ?? 0;
2191
+ b += rgb[at + 2] ?? 0;
2192
+ }
2193
+ const at = y * halfW + x;
2194
+ out[lumaBytes + at] = clampByte(chromaU(r / 4, g / 4, b / 4));
2195
+ out[lumaBytes + chromaBytes + at] = clampByte(chromaV(r / 4, g / 4, b / 4));
2196
+ }
2197
+ return out;
2198
+ }
2199
+ /**
2200
+ * Render the seed as one yuv420p frame the canvas can take whole.
2201
+ *
2202
+ * Rejects on failure rather than answering black: the caller decides, and it
2203
+ * decides to carry on without a seed — a grid that would not start because its
2204
+ * placeholder would not draw is a worse grid than one that starts black.
2205
+ */
2206
+ async function renderGridSeed(spec) {
2207
+ const { default: sharp } = await import("sharp");
2208
+ const rgb = await sharp(Buffer.from(buildGridSeedSvg(spec))).flatten({ background: {
2209
+ r: 0,
2210
+ g: 0,
2211
+ b: 0
2212
+ } }).raw().toBuffer();
2213
+ const expected = spec.canvas.width * spec.canvas.height * 3;
2214
+ if (rgb.length !== expected) throw new Error(`camera-grid: the seed rasterized to ${String(rgb.length)} bytes, expected ${String(expected)}`);
2215
+ return rgbToYuv420p(rgb, spec.canvas);
2216
+ }
2217
+ //#endregion
2024
2218
  //#region src/builtins/camera-grid/grid-tile-feed.ts
2025
2219
  /**
2026
2220
  * One source's decoded tiles, of which only the LATEST is ever wanted.
@@ -2115,11 +2309,16 @@ var GridTileFeed = class {
2115
2309
  *
2116
2310
  * ## It also removes the wait
2117
2311
  *
2118
- * A cell with no frame yet is BLACK, not a reason to hold the picture back. The
2119
- * output exists from the first tick and cells fill in as their sources arrive —
2312
+ * A cell with no frame yet is not a reason to hold the picture back. The output
2313
+ * exists from the first tick and cells fill in as their sources arrive —
2120
2314
  * instead of the whole grid waiting for the slowest source's first key frame,
2121
2315
  * which is what the ~9 s cold start was.
2122
2316
  *
2317
+ * What a cell with no frame yet SHOWS is the seed (`grid-canvas-seed.ts`),
2318
+ * blitted in once at construction: its own rectangle, with its camera's name
2319
+ * and the word `Waiting`. It was black, and black said the same thing about an
2320
+ * empty grid, a broken grid and a grid that is merely still opening.
2321
+ *
2123
2322
  * ## What it costs
2124
2323
  *
2125
2324
  * The same N decodes ffmpeg already paid, in N children instead of one, plus
@@ -2138,6 +2337,10 @@ var GridCompositor = class {
2138
2337
  constructor(deps) {
2139
2338
  this.deps = deps;
2140
2339
  this.canvas = new GridCanvas(deps.plan.canvas.width, deps.plan.canvas.height);
2340
+ if (deps.seed !== null) this.canvas.blit(deps.seed, deps.plan.canvas, {
2341
+ x: 0,
2342
+ y: 0
2343
+ });
2141
2344
  for (const tile of deps.plan.tiles) this.placed.set(tile.id, {
2142
2345
  tile,
2143
2346
  feed: new GridTileFeed(tile.frameBytes)
@@ -2380,11 +2583,17 @@ function gridCanvasEncoderInvocation(input) {
2380
2583
  *
2381
2584
  * ## The order things start in, and why it is the order
2382
2585
  *
2383
- * The encoder first, then the tick, then the decoders. The tick writes a black
2384
- * canvas from its very first beat, so the encoder produces a stream — and the
2385
- * consumer sees a picture — before any camera has delivered anything. That is
2386
- * the cold start the old shape spent waiting for the slowest source's first key
2387
- * frame, and it is now spent watching cells fill in.
2586
+ * The seed first, then the encoder, then the tick, then the decoders. The tick
2587
+ * writes the canvas from its very first beat, so the encoder produces a stream
2588
+ * — and the consumer sees a picture — before any camera has delivered anything.
2589
+ * That is the cold start the old shape spent waiting for the slowest source's
2590
+ * first key frame, and it is now spent watching cells fill in.
2591
+ *
2592
+ * What that first picture SAYS is the seed (`grid-canvas-seed.ts`): one card
2593
+ * per cell, at the cell's own rectangle, naming its camera. It is drawn once,
2594
+ * here, before the compositor is built — a tick must never wait on a render —
2595
+ * and it is bounded and fallible: a seed that cannot be drawn leaves the canvas
2596
+ * black, which is what it was before, and never stops the composition.
2388
2597
  *
2389
2598
  * ## Back-pressure is a SKIPPED tick, never a queue
2390
2599
  *
@@ -2431,6 +2640,77 @@ var DECODER_FIRST_DATA_MS = 15e3;
2431
2640
  * simply replaced while its cell holds the last frame it produced.
2432
2641
  */
2433
2642
  var TILE_MAX_RESTARTS = 5;
2643
+ /**
2644
+ * How long the seed may wait for the source cameras' names.
2645
+ *
2646
+ * The seed is drawn BEFORE the encoder is spawned — that is what puts it on the
2647
+ * first tick — so everything it waits for is added to the cold start the rest
2648
+ * of this file exists to keep short. A name is a nicety; the card exists
2649
+ * either way, labelled by device id. So the read is bounded and the deadline
2650
+ * is not an error.
2651
+ */
2652
+ var SEED_NAMES_TIMEOUT_MS = 1500;
2653
+ /**
2654
+ * The names for the cards, or an empty map.
2655
+ *
2656
+ * Never throws and never delays past the deadline: an unnamed card is a
2657
+ * complete card (`gridSeedName` falls back to the device id), and a grid that
2658
+ * started late because a name lookup was slow would be a worse grid.
2659
+ */
2660
+ async function resolveSeedNames(ctx, deviceIds) {
2661
+ const resolve = ctx.resolveSourceNames;
2662
+ if (resolve === void 0) return /* @__PURE__ */ new Map();
2663
+ const empty = /* @__PURE__ */ new Map();
2664
+ return new Promise((settle) => {
2665
+ const timer = setTimeout(() => {
2666
+ ctx.logger.warn("camera grid: the seed could not read the source names in time", {
2667
+ tags: { deviceId: ctx.deviceId },
2668
+ meta: {
2669
+ sources: deviceIds.length,
2670
+ timeoutMs: SEED_NAMES_TIMEOUT_MS
2671
+ }
2672
+ });
2673
+ settle(empty);
2674
+ }, SEED_NAMES_TIMEOUT_MS);
2675
+ timer.unref();
2676
+ resolve(deviceIds).then((names) => {
2677
+ clearTimeout(timer);
2678
+ settle(names);
2679
+ }, (error) => {
2680
+ clearTimeout(timer);
2681
+ ctx.logger.warn("camera grid: the seed could not read the source names", {
2682
+ tags: { deviceId: ctx.deviceId },
2683
+ meta: {
2684
+ sources: deviceIds.length,
2685
+ error: errMsg(error)
2686
+ }
2687
+ });
2688
+ settle(empty);
2689
+ });
2690
+ });
2691
+ }
2692
+ /**
2693
+ * The picture the canvas starts as: one card per cell, naming its camera.
2694
+ *
2695
+ * `null` — black, which is what this was before — when it cannot be drawn. A
2696
+ * placeholder that refused to render must never be the reason a grid does not
2697
+ * start, so every failure here is a warning and a composition that carries on.
2698
+ */
2699
+ async function buildCanvasSeed(ctx, tilePlan) {
2700
+ const names = await resolveSeedNames(ctx, [...new Set(tilePlan.tiles.map((tile) => tile.deviceId))]);
2701
+ try {
2702
+ return await renderGridSeed(buildGridSeedSpec(tilePlan, names));
2703
+ } catch (error) {
2704
+ ctx.logger.warn("camera grid: the canvas seed could not be drawn - starting black", {
2705
+ tags: { deviceId: ctx.deviceId },
2706
+ meta: {
2707
+ error: errMsg(error),
2708
+ tiles: tilePlan.tiles.length
2709
+ }
2710
+ });
2711
+ return null;
2712
+ }
2713
+ }
2434
2714
  async function startGridCompositorChild(ctx, plan, sources, onEnded) {
2435
2715
  const tilePlan = buildGridTilePlan({
2436
2716
  canvas: {
@@ -2450,12 +2730,14 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
2450
2730
  ended = true;
2451
2731
  onEnded(reason);
2452
2732
  };
2733
+ const seed = await buildCanvasSeed(ctx, tilePlan);
2453
2734
  let encoderIn = null;
2454
2735
  let encoderOut = null;
2455
2736
  let draining = false;
2456
2737
  let tick = null;
2457
2738
  const compositor = new GridCompositor({
2458
2739
  plan: tilePlan,
2740
+ seed,
2459
2741
  write: (frame) => {
2460
2742
  const sink = encoderIn;
2461
2743
  if (sink === null || draining || sink.destroyed) return false;
@@ -2572,7 +2854,8 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
2572
2854
  meta: {
2573
2855
  tiles: tilePlan.tiles.length,
2574
2856
  canvas: `${String(tilePlan.canvas.width)}x${String(tilePlan.canvas.height)}`,
2575
- fps: plan.fps
2857
+ fps: plan.fps,
2858
+ seeded: seed !== null
2576
2859
  }
2577
2860
  });
2578
2861
  return {
@@ -3001,11 +3284,39 @@ var CameraGridAddon = class extends BaseAddon {
3001
3284
  this.sessions.set(key, new GridSessionEntry(instanceId, profile, this.makeSession(instanceId, deviceId, profile)));
3002
3285
  }
3003
3286
  }
3287
+ /**
3288
+ * The source cameras' names, for the cards the canvas is seeded with.
3289
+ *
3290
+ * Asked of the device manager, which OWNS the name, at the moment the seed is
3291
+ * drawn — never kept here. A consumer that mirrored this would be stale on
3292
+ * exactly the write an operator makes (D224), for a label.
3293
+ *
3294
+ * A device that cannot be read is simply absent from the map, and its card
3295
+ * carries the device id instead. One unreadable camera never costs the other
3296
+ * cards their names, which is why these are settled rather than awaited as
3297
+ * one batch.
3298
+ */
3299
+ async sourceNames(deviceIds) {
3300
+ const names = /* @__PURE__ */ new Map();
3301
+ const results = await Promise.allSettled(deviceIds.map(async (deviceId) => {
3302
+ return {
3303
+ deviceId,
3304
+ name: (await this.ctx.api.deviceManager.getDevice.query({ deviceId }))?.name ?? null
3305
+ };
3306
+ }));
3307
+ for (const result of results) {
3308
+ if (result.status !== "fulfilled") continue;
3309
+ const { deviceId, name } = result.value;
3310
+ if (name !== null) names.set(deviceId, name);
3311
+ }
3312
+ return names;
3313
+ }
3004
3314
  makeSession(instanceId, deviceId, profile) {
3005
3315
  const childContext = {
3006
3316
  binaryPath: this.ffmpegBinaryPath,
3007
3317
  logger: this.ctx.logger,
3008
- deviceId
3318
+ deviceId,
3319
+ resolveSourceNames: (deviceIds) => this.sourceNames(deviceIds)
3009
3320
  };
3010
3321
  const session = new GridStreamSession({
3011
3322
  deviceId,
@@ -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-D5ONNqL6.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-_YWIPtUH.js");
3
+ const require_dist = require("../../dist-D5ONNqL6.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { G as coreBlockAddonId, K as coreBlockIdFromAddonId, an as errMsg, c as CORE_BLOCKS_ADDON_ID, l as CORE_BLOCK_ADDON_PREFIX, p as DeclaredDevices, q as coreBlocksCapability, sn as BaseAddon, u as CoreBlockSchema } from "../../dist-Diz59Dhi.mjs";
1
+ import { G as coreBlockAddonId, K as coreBlockIdFromAddonId, an as errMsg, c as CORE_BLOCKS_ADDON_ID, l as CORE_BLOCK_ADDON_PREFIX, p as DeclaredDevices, q as coreBlocksCapability, sn as BaseAddon, u as CoreBlockSchema } from "../../dist-DrLhdcwI.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ 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");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-C2ocoH_l.js");
6
+ const require_dist = require("../../dist-D5ONNqL6.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-1686ou3s.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -1,5 +1,5 @@
1
- import { $ as deviceStateCapability, D as STREAM_PROFILE_META, H as buildStreamParamsConfigSchema, Ht as runtimeStatePolicyFor, Nn as sleep$1, Nt as parseStreamParamsFormPatch, Pn as EventCategory, Q as deviceManagerCapability, St as locationIconKey, Tn as isDeviceConfigCap, an as errMsg, dn as DeviceFeature, et as deviceStatusCapability, fn as DeviceRole, gn as WELL_KNOWN_TAB_MAP, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, o as CAMERA_SWITCH_ORDER, pn as DeviceType, qt as siteZoneKey, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, sn as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema, zt as resolveContainerPrimaryChild } from "../../dist-Diz59Dhi.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-C_Hg5d37.mjs";
1
+ import { $ as deviceStateCapability, D as STREAM_PROFILE_META, H as buildStreamParamsConfigSchema, Ht as runtimeStatePolicyFor, Nn as sleep$1, Nt as parseStreamParamsFormPatch, Pn as EventCategory, Q as deviceManagerCapability, St as locationIconKey, Tn as isDeviceConfigCap, an as errMsg, dn as DeviceFeature, et as deviceStatusCapability, fn as DeviceRole, gn as WELL_KNOWN_TAB_MAP, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, o as CAMERA_SWITCH_ORDER, pn as DeviceType, qt as siteZoneKey, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, sn as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema, zt as resolveContainerPrimaryChild } from "../../dist-DrLhdcwI.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Du_0qedo.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -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-D5ONNqL6.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { B as bareAddonId, Pn as EventCategory, T as SOURCE_DEVICE_TYPES, an as errMsg, at as evaluateSensorEdge, bn as createEvent, gt as isSourceCap, ht as isSameAddonId, pn as DeviceType, sn as BaseAddon, tt as doorbellCapability, w as SOURCE_CAPS } from "../../dist-Diz59Dhi.mjs";
1
+ import { B as bareAddonId, Pn as EventCategory, T as SOURCE_DEVICE_TYPES, an as errMsg, at as evaluateSensorEdge, bn as createEvent, gt as isSourceCap, ht as isSameAddonId, pn as DeviceType, sn as BaseAddon, tt as doorbellCapability, w as SOURCE_CAPS } from "../../dist-DrLhdcwI.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -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-D5ONNqL6.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-_YWIPtUH.js");
3
+ const require_dist = require("../../dist-D5ONNqL6.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { C as RecordingStatusSchema, Pn as EventCategory, an as errMsg, bn as createEvent, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { C as RecordingStatusSchema, Pn as EventCategory, an as errMsg, bn as createEvent, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -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-D5ONNqL6.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability, r as ApiKeyRecordSchema, sn as BaseAddon, tn as userManagementCapability } from "../../dist-Diz59Dhi.mjs";
2
+ import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability, r as ApiKeyRecordSchema, sn as BaseAddon, tn as userManagementCapability } from "../../dist-DrLhdcwI.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-_YWIPtUH.js");
3
+ const require_dist = require("../../dist-D5ONNqL6.js");
4
4
  const require_tls = require("../../tls-BFs4PzoW.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Pn as EventCategory, sn as BaseAddon, xt as localNetworkCapability } from "../../dist-Diz59Dhi.mjs";
1
+ import { Pn as EventCategory, sn as BaseAddon, xt as localNetworkCapability } from "../../dist-DrLhdcwI.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-2xbgg78V.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } 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-D5ONNqL6.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label