@camstack/system 1.2.254 → 1.2.256

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 (94) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/autotrack-cameras.d.ts +76 -0
  10. package/dist/builtins/autotrack/autotrack-config.d.ts +89 -0
  11. package/dist/builtins/autotrack/autotrack-decision.d.ts +99 -0
  12. package/dist/builtins/autotrack/autotrack-loop.d.ts +75 -0
  13. package/dist/builtins/autotrack/autotrack.addon.d.ts +71 -0
  14. package/dist/builtins/autotrack/frame-error.d.ts +50 -0
  15. package/dist/builtins/autotrack/frame-measurements.d.ts +97 -0
  16. package/dist/builtins/autotrack/index.d.ts +2 -0
  17. package/dist/builtins/autotrack/index.js +981 -0
  18. package/dist/builtins/autotrack/index.mjs +975 -0
  19. package/dist/builtins/autotrack/ptz-mirror.d.ts +25 -0
  20. package/dist/builtins/autotrack/target-selection.d.ts +60 -0
  21. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  22. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  23. package/dist/builtins/camera-grid/addon.d.ts +118 -0
  24. package/dist/builtins/camera-grid/flv-join.d.ts +66 -0
  25. package/dist/builtins/camera-grid/grid-camera-declarations.d.ts +59 -0
  26. package/dist/builtins/camera-grid/grid-camera-device.d.ts +32 -0
  27. package/dist/builtins/camera-grid/grid-child.d.ts +9 -0
  28. package/dist/builtins/camera-grid/grid-device-settings.d.ts +11 -0
  29. package/dist/builtins/camera-grid/grid-filter-graph.d.ts +53 -0
  30. package/dist/builtins/camera-grid/grid-frame-sample.d.ts +27 -0
  31. package/dist/builtins/camera-grid/grid-instances.d.ts +49 -0
  32. package/dist/builtins/camera-grid/grid-last-frame-store.d.ts +19 -0
  33. package/dist/builtins/camera-grid/grid-layout-native-provider.d.ts +8 -0
  34. package/dist/builtins/camera-grid/grid-plan.d.ts +22 -0
  35. package/dist/builtins/camera-grid/grid-profiles.d.ts +47 -0
  36. package/dist/builtins/camera-grid/grid-row-normalization.d.ts +10 -0
  37. package/dist/builtins/camera-grid/grid-sentry-sources.d.ts +52 -0
  38. package/dist/builtins/camera-grid/grid-snapshot.d.ts +62 -0
  39. package/dist/builtins/camera-grid/grid-stream-descriptor.d.ts +24 -0
  40. package/dist/builtins/camera-grid/grid-stream-invocation.d.ts +29 -0
  41. package/dist/builtins/camera-grid/grid-stream-relay.d.ts +36 -0
  42. package/dist/builtins/camera-grid/grid-stream-session.d.ts +113 -0
  43. package/dist/builtins/camera-grid/grid-wire-format.d.ts +14 -0
  44. package/dist/builtins/camera-grid/index.d.ts +44 -0
  45. package/dist/builtins/camera-grid/index.js +2309 -0
  46. package/dist/builtins/camera-grid/index.mjs +2276 -0
  47. package/dist/builtins/camera-grid/silence-analysis.d.ts +13 -0
  48. package/dist/builtins/console-logging/index.js +1 -1
  49. package/dist/builtins/console-logging/index.mjs +1 -1
  50. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  51. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  52. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  53. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  54. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  55. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  56. package/dist/builtins/hub-forwarder/index.js +1 -1
  57. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  58. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  59. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  60. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  61. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  62. package/dist/builtins/local-network/local-network.addon.js +1 -1
  63. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  64. package/dist/builtins/loki-logging/index.js +1 -1
  65. package/dist/builtins/loki-logging/index.mjs +1 -1
  66. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  67. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  68. package/dist/builtins/platform-probe/index.js +1 -1
  69. package/dist/builtins/platform-probe/index.mjs +1 -1
  70. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  71. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  72. package/dist/builtins/snapshot/index.js +1 -1
  73. package/dist/builtins/snapshot/index.mjs +1 -1
  74. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  75. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  76. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  77. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  78. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  79. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  80. package/dist/builtins/system-config/system-config.addon.js +1 -1
  81. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  82. package/dist/builtins/winston-logging/index.js +1 -1
  83. package/dist/builtins/winston-logging/index.mjs +1 -1
  84. package/dist/{dist-CnQsUe15.js → dist-BJj6Akye.js} +1319 -0
  85. package/dist/{dist-BhTy3CUQ.mjs → dist-Ch93tyHB.mjs} +1260 -1
  86. package/dist/index.js +3 -2
  87. package/dist/index.mjs +3 -3
  88. package/dist/kernel/index.d.ts +1 -1
  89. package/dist/kernel/moleculer/trpc-links.d.ts +27 -0
  90. package/dist/{manifest-system-deps-CfR7Z90J.mjs → manifest-system-deps-BXf-Ouqx.mjs} +44 -1
  91. package/dist/{manifest-system-deps-CzyjTMcO.js → manifest-system-deps-CfVyUKLU.js} +49 -0
  92. package/dist/{retired-settings-keys-DDU-VFTU.js → retired-settings-keys-BRCn6-mt.js} +1 -1
  93. package/dist/{retired-settings-keys-CMhNYMD9.mjs → retired-settings-keys-uCaSKuwd.mjs} +1 -1
  94. package/package.json +32 -1
@@ -0,0 +1,25 @@
1
+ import { NativeAutotrackState } from './autotrack-decision.js';
2
+ export interface PtzMirror {
3
+ /** `ptz.getOptions().moveImpulseMs`. `null` = never read, or the driver
4
+ * itself reports it cannot say. */
5
+ readonly moveImpulseMs: number | null;
6
+ readonly nativeAutotrack: NativeAutotrackState;
7
+ /** When a refresh last SUCCEEDED. `null` = never. */
8
+ readonly lastRefreshAtMs: number | null;
9
+ /** Message of the last failed refresh, `null` once one succeeds again. */
10
+ readonly lastError: string | null;
11
+ /** Consecutive failed refreshes since the last success. */
12
+ readonly consecutiveFailures: number;
13
+ }
14
+ export declare const INITIAL_PTZ_MIRROR: PtzMirror;
15
+ /** What one refresh attempt found. A `failed` outcome carries a reason —
16
+ * there is no silent failure shape. */
17
+ export type MirrorRefreshOutcome = {
18
+ readonly kind: 'read';
19
+ readonly moveImpulseMs: number | null;
20
+ readonly nativeAutotrack: NativeAutotrackState;
21
+ } | {
22
+ readonly kind: 'failed';
23
+ readonly error: string;
24
+ };
25
+ export declare function applyMirrorRefresh(mirror: PtzMirror, outcome: MirrorRefreshOutcome, nowMs: number): PtzMirror;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Sticky target choice: pick ONE track, then stay on it.
3
+ *
4
+ * Re-picking "the best candidate" every frame is oscillation on another axis —
5
+ * two people a metre apart trade the top score frame to frame and the head
6
+ * saws between them, having centred neither. So the target is chosen ONCE and
7
+ * held by `trackId` until it has been gone for the disappear delay. While it
8
+ * is merely MISSING from a frame nothing is switched and nothing is released:
9
+ * `present: false` is reported and the decision layer decides what a gap means.
10
+ *
11
+ * Pure: state in, state out, no clock read inside — `nowMs` is passed. A new
12
+ * state object is returned every call; the input is never mutated.
13
+ */
14
+ /** One candidate of the configured class on this frame. */
15
+ export interface TargetCandidate {
16
+ readonly trackId: string;
17
+ /** Pixel area of the box. Used ONLY to break the first choice; it never
18
+ * unseats a target that is still alive. */
19
+ readonly areaPx: number;
20
+ }
21
+ export interface TargetSelectionState {
22
+ /** The track being followed, or `null` when nothing is held. */
23
+ readonly trackId: string | null;
24
+ /** When that track was last OBSERVED on a frame. `null` while nothing is
25
+ * held. Never advanced by a frame the track was absent from. */
26
+ readonly lastSeenAtMs: number | null;
27
+ }
28
+ export declare const INITIAL_TARGET_SELECTION_STATE: TargetSelectionState;
29
+ /** Why the selection came out the way it did — logged on every frame so a
30
+ * loop that chooses nothing is distinguishable from a loop that is dead. */
31
+ export type TargetSelectionReason =
32
+ /** The held track was observed on this frame. */
33
+ 'held'
34
+ /** The held track was absent from this frame but is still within its
35
+ * disappear delay — nothing is switched, nothing is released. */
36
+ | 'held-absent'
37
+ /** The held track exceeded its disappear delay and was released; no
38
+ * candidate was available to take its place. */
39
+ | 'released'
40
+ /** The held track was released and a new one was picked on the same frame. */
41
+ | 'switched'
42
+ /** Nothing was held and a candidate was picked. */
43
+ | 'acquired'
44
+ /** Nothing was held and no candidate was available. */
45
+ | 'idle';
46
+ export interface TargetSelectionResult {
47
+ readonly state: TargetSelectionState;
48
+ /** The held track, after this frame. `null` when nothing is held. */
49
+ readonly trackId: string | null;
50
+ /** Whether the held track was OBSERVED on this frame. This is the boolean
51
+ * the absence measurement counts. */
52
+ readonly present: boolean;
53
+ readonly reason: TargetSelectionReason;
54
+ }
55
+ export interface TargetSelectionInput {
56
+ readonly nowMs: number;
57
+ readonly candidates: readonly TargetCandidate[];
58
+ readonly disappearDelayMs: number;
59
+ }
60
+ export declare function selectTarget(state: TargetSelectionState, input: TargetSelectionInput): TargetSelectionResult;
@@ -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-CnQsUe15.js");
6
+ const require_dist = require("../../dist-BJj6Akye.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 { Cn as EventCategory, F as backupCapability, Ut as storageOccupancyCapability, Zt as BaseAddon, xt as mayWriteToLocation } from "../../dist-BhTy3CUQ.mjs";
1
+ import { Dt as mayWriteToLocation, Nn as EventCategory, Xt as storageOccupancyCapability, on as BaseAddon, z as backupCapability } from "../../dist-Ch93tyHB.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";
@@ -0,0 +1,118 @@
1
+ import { AddonInitResult, ConfigUISchema, DeviceProxy, BaseAddon } from '@camstack/types';
2
+ interface CameraGridAddonConfig {
3
+ /** Operator-written rows. Name only on creation; geometry is added later. */
4
+ readonly cameraGrids: readonly Record<string, unknown>[];
5
+ /** Durable deletion intent, so a removed grid is not re-adopted by a sweep. */
6
+ readonly cameraGridTombstones: readonly string[];
7
+ }
8
+ export declare class CameraGridAddon extends BaseAddon<CameraGridAddonConfig> {
9
+ private relay;
10
+ private reconcileTimer;
11
+ /** Keyed by {@link sessionKey} — one composition per grid per profile. */
12
+ private readonly sessions;
13
+ private readonly deviceIdByInstanceId;
14
+ private readonly tombstones;
15
+ /**
16
+ * What each grid can honestly publish, per profile.
17
+ *
18
+ * ABSENT means "not asked yet", and it is a REFUSAL to describe — the device
19
+ * throws rather than answering an empty catalog, because an empty catalog
20
+ * retracts every stream the broker holds for the grid. It is refreshed on
21
+ * every reconcile and on every layout save; the window belongs to the READER
22
+ * (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
23
+ * — that only changes when an operator changes it.
24
+ */
25
+ private readonly offersByInstanceId;
26
+ private snapshots;
27
+ private frameSampler;
28
+ private ffmpegBinaryPath;
29
+ private integrationId;
30
+ constructor();
31
+ protected onInitialize(): Promise<AddonInitResult>;
32
+ protected onConfigChanged(): Promise<void>;
33
+ protected onShutdown(): Promise<void>;
34
+ private instances;
35
+ private instanceById;
36
+ /**
37
+ * Fill in everything a name-only row is missing, and persist if it changed.
38
+ *
39
+ * Without this a freshly typed row is unparseable, and `readGridInstances`
40
+ * IGNORES what it cannot parse — so creation would silently do nothing.
41
+ */
42
+ private normalizeRows;
43
+ private replaceTombstones;
44
+ private reconcile;
45
+ private silenceAnalysis;
46
+ /**
47
+ * Which profiles each grid can honestly publish, asked of the BROKER.
48
+ *
49
+ * `listAllProfileSlots` is the one authority on "does 615 have a low
50
+ * assigned", and `sourceCamStreamId === null` is its way of saying no. A read
51
+ * that FAILS leaves the previous answer in place and says so - it is not an
52
+ * empty catalog, and turning it into one would retract the streams of every
53
+ * grid on the hub.
54
+ */
55
+ private refreshProfileOffers;
56
+ /**
57
+ * Say which profiles this grid will NOT publish, and why - once per change.
58
+ *
59
+ * A profile silently missing from a catalog is indistinguishable from a
60
+ * broker that swept it. Logged on the EDGE rather than every minute: a grid
61
+ * that can only serve `high` would otherwise write two lines a minute for
62
+ * ever.
63
+ */
64
+ private logRefusals;
65
+ private offeredProfiles;
66
+ private ensureSessions;
67
+ private makeSession;
68
+ /** The session a dial is asking for, or `null` - which the relay answers 404 to. */
69
+ private sessionFor;
70
+ /** This grid's sessions, in DESCENDING quality - the snapshot takes the first running one. */
71
+ private sessionsForDevice;
72
+ /** A grid that is gone stops immediately - it must not keep N sources dialled. */
73
+ private retireWithdrawnSessions;
74
+ /**
75
+ * A geometry change is a NEW composition. The running child is composing the
76
+ * old one, and ffmpeg has no way to be told otherwise, so it is stopped: the
77
+ * consumer's next read re-dials and gets the new picture.
78
+ */
79
+ private applyLayoutChanges;
80
+ private getSnapshot;
81
+ /**
82
+ * One frame out of a composition that is ALREADY running.
83
+ *
84
+ * `openIfRunning` is the whole guarantee: it refuses a cold session, so a
85
+ * thumbnail can never be the reason N sources get acquired. A grid that is
86
+ * not being watched has no fresh frame, and that is the honest answer.
87
+ */
88
+ private sampleFrame;
89
+ /**
90
+ * Every stream this grid offers, or `null` when the addon cannot say.
91
+ *
92
+ * `null` is a refusal to describe and the device turns it into a THROW. An
93
+ * empty array would be the broker reading "this camera offers nothing" and
94
+ * sweeping every stream it holds for the grid.
95
+ */
96
+ private descriptorsFor;
97
+ private instanceIdForDevice;
98
+ private instanceForDevice;
99
+ private gridViewFor;
100
+ private saveGridLayout;
101
+ private resolveFfmpegBinaryPath;
102
+ /**
103
+ * The integration page: a list of grids, with a NAME.
104
+ *
105
+ * Deliberately nothing else. A generated form for the geometry would render
106
+ * "cell 1 source = 592, cell 1 crop x = 0.25" and be unusable, so the
107
+ * geometry lives in a WIDGET on the camera's own device details, declared by
108
+ * the `camera-grid-layout` CAPABILITY (`camera-grid-layout.cap.ts`). It used
109
+ * to be a `type:'widget'` field in this addon's own `deviceSettingsSchema()`
110
+ * and it rendered NOWHERE: that section is only fetched for the four addons
111
+ * hand-listed in `PIPELINE_CLUSTER_DEVICE_ADDONS`, and an addon not on the
112
+ * list falls off silently. A cap-declared widget is placed by the framework,
113
+ * on the camera, beside PTZ and motion zones.
114
+ */
115
+ protected globalSettingsSchema(): ConfigUISchema;
116
+ protected integrationSettingSections(): readonly string[];
117
+ }
118
+ export type { DeviceProxy };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Making one FLV stream joinable by many readers.
3
+ *
4
+ * MPEG-TS is self-describing: a reader attaches mid-stream, waits for the next
5
+ * PAT/PMT and decodes. FLV is not. Its 9-byte signature, its `onMetaData`
6
+ * script tag and its AVC sequence header appear ONCE, at the top, and a reader
7
+ * that arrives later is handed the middle of a tag — ffmpeg answers `Invalid
8
+ * data found when processing input`, which is exactly what the hub did the
9
+ * minute the grid started emitting FLV (2026-09-17). The grid emits FLV because
10
+ * the bundled ffmpeg segfaults remuxing MPEG-TS (D519 notes, same day), so the
11
+ * format is not negotiable and this is the price of it.
12
+ *
13
+ * The join point therefore does two things, and nothing else:
14
+ *
15
+ * - **remembers what describes the stream** — the header, the newest script
16
+ * tag, the newest AVC sequence header. Newest, not first: an encoder that
17
+ * respawns emits a new sequence header, and a reader handed the old one
18
+ * decodes noise;
19
+ * - **starts each reader on a tag boundary at a KEYFRAME**, never mid-tag and
20
+ * never on an inter frame that references pictures the reader never saw.
21
+ *
22
+ * It is a parser, not a buffer: nothing is retained beyond the preamble and the
23
+ * partial tag currently being assembled. A composite at 10 fps with a 2-second
24
+ * GOP makes a reader wait at most two seconds to join, which is the same wait
25
+ * MPEG-TS would have imposed for its next key frame.
26
+ */
27
+ interface MutableReader {
28
+ started: boolean;
29
+ queued: Buffer[];
30
+ }
31
+ /**
32
+ * One reader's view of the stream.
33
+ *
34
+ * Every consumer holds one, the first included: a join point with a privileged
35
+ * "primary" reader would have two code paths for the same question, and the
36
+ * grid's first consumer is not special — it just happens to arrive when the
37
+ * preamble is still being learnt.
38
+ */
39
+ export declare class FlvReader {
40
+ private readonly state;
41
+ private readonly detach;
42
+ constructor(state: MutableReader, detach: (state: MutableReader) => void);
43
+ /** True until the preamble and a key frame have been handed over. */
44
+ get pending(): boolean;
45
+ /** The bytes owed to this reader since the last call, or `null`. */
46
+ take(): Buffer | null;
47
+ /** Stop receiving. Anything queued is dropped with the reader. */
48
+ close(): void;
49
+ }
50
+ export declare class FlvJoinPoint {
51
+ private buffer;
52
+ private header;
53
+ private script;
54
+ private sequenceHeader;
55
+ private readonly readers;
56
+ /** Attach a reader. It receives the preamble at the next key frame. */
57
+ join(): FlvReader;
58
+ /** Feed bytes from the encoder; each reader then takes what it is owed. */
59
+ accept(chunk: Buffer): void;
60
+ private consumeHeader;
61
+ private consumeTags;
62
+ private dispatch;
63
+ /** The bytes that describe the stream, or `null` while any is still unknown. */
64
+ private preamble;
65
+ }
66
+ export {};
@@ -0,0 +1,59 @@
1
+ import { GridInstance } from './grid-instances.js';
2
+ /**
3
+ * The stable id is `camera-grid-<instance uuid>` and NOTHING else.
4
+ *
5
+ * A stable id is permanent the instant it is minted: bindings, grid layouts in
6
+ * other addons, recording rows, the timeline and this addon's own tombstones
7
+ * all reference it, and a device whose stable id changes is a NEW camera with
8
+ * an empty history. So it may only be derived from something that itself never
9
+ * changes.
10
+ *
11
+ * The instance uuid is the only such thing here. Deriving it from the NAME
12
+ * would rename-break every reference; deriving it from the CELLS would mint a
13
+ * new camera every time the operator drags a rectangle — which is the entire
14
+ * activity this addon exists for. Terminal learned the same lesson from the
15
+ * other end: its original one-camera-per-node id is permanent, and its profile
16
+ * migration had to ENRICH that row rather than mint a replacement.
17
+ */
18
+ export declare function newGridCameraStableId(instanceId: string): string;
19
+ /** The config a declared grid camera device carries. */
20
+ export interface GridCameraConfig {
21
+ readonly instanceId: string;
22
+ /** `DeviceDeclaration.config` is an open record; this keeps the named type assignable. */
23
+ readonly [key: string]: unknown;
24
+ }
25
+ export interface GridCameraDeclaration {
26
+ readonly stableId: string;
27
+ readonly name: string;
28
+ readonly config: GridCameraConfig;
29
+ }
30
+ export interface ExistingGridCameraRow {
31
+ readonly id: number;
32
+ readonly stableId: string;
33
+ readonly integrationId?: string | null;
34
+ readonly config: Readonly<Record<string, unknown>>;
35
+ }
36
+ /**
37
+ * Explicit persisted instances only. A grid camera exists because an operator
38
+ * created a grid, never because some source camera happens to exist.
39
+ *
40
+ * A grid with NO cells is declared like any other: creation is name-only and
41
+ * the layout is authored afterwards in the camera's own device details, so
42
+ * "no cells yet" is the normal first state, not an invalid row.
43
+ */
44
+ export declare function buildGridCameraDeclarations(instances: readonly GridInstance[]): readonly GridCameraDeclaration[];
45
+ /**
46
+ * The rows `DeclaredDevices` is allowed to consider — every live declaration
47
+ * plus ONE bounded batch of orphans.
48
+ *
49
+ * Two guards, both copied from Terminal because both were paid for there:
50
+ *
51
+ * 1. **An unknown integration id yields an EMPTY index.** `null`/`undefined`
52
+ * means "the integration lookup has not answered", which is not the same
53
+ * fact as "this addon owns no rows" — and only the second one may drive a
54
+ * withdrawal.
55
+ * 2. **The orphan batch is bounded and deterministically ordered.** The generic
56
+ * sweep refuses an over-limit set outright; selecting a batch here drains a
57
+ * large historical orphan set across passes without weakening that guard.
58
+ */
59
+ export declare function gridCameraReconciliationIndex(rows: readonly ExistingGridCameraRow[], integrationId: string | null | undefined, declarations: readonly GridCameraDeclaration[], managedStableIds?: ReadonlySet<string>): readonly ExistingGridCameraRow[];
@@ -0,0 +1,32 @@
1
+ import { BaseDevice, CamStreamDescriptor, DeviceContext, DeviceFeature, GridLayoutPatch, GridLayoutView } from '@camstack/types';
2
+ import { z } from 'zod';
3
+ import { GridSnapshotImage } from './grid-snapshot.js';
4
+ declare const gridCameraSchema: z.ZodObject<{
5
+ instanceId: z.ZodString;
6
+ }, z.core.$strip>;
7
+ /**
8
+ * What the device needs from the running addon. Installed as a module
9
+ * singleton, like the Terminal camera's relay: `DeclaredDevices` constructs
10
+ * devices, so a device cannot be handed a constructor dependency.
11
+ */
12
+ export interface GridCameraRuntime {
13
+ /**
14
+ * The catalog descriptors for this grid — one per OFFERED profile — or `null`
15
+ * when the addon cannot say. `null` is a REFUSAL to describe, never an empty
16
+ * catalog with a shrug: see the note on the empty branch below.
17
+ */
18
+ descriptorsFor(instanceId: string): readonly CamStreamDescriptor[] | null;
19
+ /** The grid behind a device, or `null` — which means "answered: not a grid". */
20
+ getLayout(deviceId: number): GridLayoutView | null;
21
+ saveLayout(patch: GridLayoutPatch): Promise<GridLayoutView>;
22
+ /** A frame of the composition, or `null`. Never starts one. */
23
+ getSnapshot(deviceId: number): Promise<GridSnapshotImage | null>;
24
+ }
25
+ export declare function installGridCameraRuntime(next: GridCameraRuntime | null): void;
26
+ export declare class GridCameraDevice extends BaseDevice<typeof gridCameraSchema> {
27
+ readonly features: readonly DeviceFeature[];
28
+ constructor(ctx: DeviceContext);
29
+ private catalog;
30
+ setAddonOnline(online: boolean): void;
31
+ }
32
+ export {};
@@ -0,0 +1,9 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ import { GridSourceHandle, GridStreamChild, GridStreamPlan } from './grid-stream-session.js';
3
+ export interface GridChildContext {
4
+ readonly binaryPath: string;
5
+ readonly logger: IScopedLogger;
6
+ /** The COMPOSITE's device id — every line the primitive emits is tagged with it. */
7
+ readonly deviceId: number;
8
+ }
9
+ export declare function startGridChild(ctx: GridChildContext, plan: GridStreamPlan, sources: readonly GridSourceHandle[], onEnded: (reason: string) => void): Promise<GridStreamChild>;
@@ -0,0 +1,11 @@
1
+ import { ConfigUISchema } from '@camstack/types';
2
+ /** The widget id, registered in `ui-library`'s `HOST_WIDGETS`. */
3
+ export declare const GRID_LAYOUT_WIDGET_ID = "host/camera-grid-layout";
4
+ /**
5
+ * The layout section, or `null` when this device is not a grid.
6
+ *
7
+ * `isGrid` is the ANSWER to "is this a grid", never "do we know yet". A caller
8
+ * that cannot tell must not call this — an unresolved read rendered as `null`
9
+ * would hide the editor from a real grid (D315).
10
+ */
11
+ export declare function buildGridDeviceSchema(isGrid: boolean): ConfigUISchema | null;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The composite's geometry, as the `-filter_complex` the extended ffmpeg
3
+ * primitive carries.
4
+ *
5
+ * ## Why normalized, and why not `deriveDetailCropRect`
6
+ *
7
+ * Both rectangles -- the crop taken OUT of a source and the cell it lands IN --
8
+ * are normalized [0,1]. A source camera can change resolution under us (a
9
+ * profile switch, a firmware update, a substream that comes back different) and
10
+ * a rectangle stored in pixels would quietly start cutting the wrong part of
11
+ * the picture, with nothing to say so.
12
+ *
13
+ * This deliberately does NOT go through `deriveDetailCropRect`. D52 -- "one
14
+ * vector index = one crop derivation" -- protects the COMPARABILITY of a vector
15
+ * index: every crop that feeds it must be cut the same way. This is
16
+ * presentation, feeds no index, and routing it through that function would add
17
+ * a second cutter to the one thing D52 exists to keep single.
18
+ *
19
+ * ## Why a canvas and overlays rather than stacks
20
+ *
21
+ * `hstack`/`vstack` only express a regular grid of equal tiles. The operator
22
+ * composes *arbitrary* rectangles -- "a 16:9 from the middle of two streams" is
23
+ * the case this was asked for -- so every cell is scaled and overlaid onto one
24
+ * explicit canvas at its own origin. The canvas is also what makes a missing
25
+ * cell legible: a gap is black, not a stretched neighbour.
26
+ */
27
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
28
+ export interface NormalizedRect {
29
+ readonly x: number;
30
+ readonly y: number;
31
+ readonly width: number;
32
+ readonly height: number;
33
+ }
34
+ /** One source, the part of it taken, and where that part lands. */
35
+ export interface GridCell {
36
+ /** Which ffmpeg input this cell reads — its ORDINAL among the invocation's inputs. */
37
+ readonly inputIndex: number;
38
+ /** The part of the source to take, normalized against the SOURCE. */
39
+ readonly source: NormalizedRect;
40
+ /** Where it lands, normalized against the CANVAS. */
41
+ readonly cell: NormalizedRect;
42
+ }
43
+ export interface GridLayout {
44
+ /** Canvas size in pixels — the composite camera's own resolution. */
45
+ readonly width: number;
46
+ readonly height: number;
47
+ readonly cells: readonly GridCell[];
48
+ }
49
+ export interface GridFilterGraph {
50
+ readonly graph: string;
51
+ readonly videoOutLabel: string;
52
+ }
53
+ export declare function buildGridFilterGraph(layout: GridLayout): GridFilterGraph;
@@ -0,0 +1,27 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { GridStreamLogger } from './grid-stream-session.js';
3
+ export type GridSpawn = typeof nodeSpawn;
4
+ export interface GridFrameSamplerOptions {
5
+ readonly binaryPath: string;
6
+ readonly logger: GridStreamLogger;
7
+ readonly timeoutMs?: number;
8
+ /** Injectable for tests. */
9
+ readonly spawnFn?: GridSpawn;
10
+ }
11
+ /** How a caller feeds the sampler: it hands over chunks and a way to stop. */
12
+ export interface GridFrameSource {
13
+ /** Start receiving MPEG-TS chunks. Returns the handle that stops them. */
14
+ attach(onChunk: (chunk: Buffer) => void): {
15
+ close(): void;
16
+ } | null;
17
+ }
18
+ export declare class GridFrameSampler {
19
+ private readonly options;
20
+ constructor(options: GridFrameSamplerOptions);
21
+ /**
22
+ * @returns the JPEG, or `null` — a grab that produced nothing, could not be
23
+ * attached, or ran out of time. Every one of those is logged with the
24
+ * device: a branch that accepts the request and produces nothing must say so.
25
+ */
26
+ sample(deviceId: number, source: GridFrameSource): Promise<Buffer | null>;
27
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * One source camera, the part of its picture taken, and where that part lands
4
+ * on the composite's canvas. Both rectangles are NORMALIZED so a source that
5
+ * changes resolution does not silently start cutting the wrong region.
6
+ */
7
+ declare const GridCellSchema: z.ZodObject<{
8
+ deviceId: z.ZodNumber;
9
+ source: z.ZodObject<{
10
+ x: z.ZodNumber;
11
+ y: z.ZodNumber;
12
+ width: z.ZodNumber;
13
+ height: z.ZodNumber;
14
+ }, z.core.$strip>;
15
+ cell: z.ZodObject<{
16
+ x: z.ZodNumber;
17
+ y: z.ZodNumber;
18
+ width: z.ZodNumber;
19
+ height: z.ZodNumber;
20
+ }, z.core.$strip>;
21
+ }, z.core.$strip>;
22
+ export type GridCellConfig = z.infer<typeof GridCellSchema>;
23
+ declare const GridInstanceSchema: z.ZodObject<{
24
+ id: z.ZodString;
25
+ cameraStableId: z.ZodString;
26
+ name: z.ZodString;
27
+ enabled: z.ZodBoolean;
28
+ width: z.ZodNumber;
29
+ height: z.ZodNumber;
30
+ fps: z.ZodNumber;
31
+ cells: z.ZodDefault<z.ZodArray<z.ZodObject<{
32
+ deviceId: z.ZodNumber;
33
+ source: z.ZodObject<{
34
+ x: z.ZodNumber;
35
+ y: z.ZodNumber;
36
+ width: z.ZodNumber;
37
+ height: z.ZodNumber;
38
+ }, z.core.$strip>;
39
+ cell: z.ZodObject<{
40
+ x: z.ZodNumber;
41
+ y: z.ZodNumber;
42
+ width: z.ZodNumber;
43
+ height: z.ZodNumber;
44
+ }, z.core.$strip>;
45
+ }, z.core.$strip>>>;
46
+ }, z.core.$strip>;
47
+ export type GridInstance = z.infer<typeof GridInstanceSchema>;
48
+ export declare function readGridInstances(raw: readonly Record<string, unknown>[], onInvalid?: (message: string) => void): readonly GridInstance[];
49
+ export {};
@@ -0,0 +1,19 @@
1
+ import { GridLastFrameStore } from './grid-snapshot.js';
2
+ export interface GridLastFrameFileStoreOptions {
3
+ /** `ctx.dataDir` — the addon's exclusive directory. */
4
+ readonly dataDir: string;
5
+ readonly minIntervalMs?: number;
6
+ /** Injectable for tests; `Date.now` otherwise. */
7
+ readonly now?: () => number;
8
+ }
9
+ export declare class GridLastFrameFileStore implements GridLastFrameStore {
10
+ private readonly dir;
11
+ private readonly minIntervalMs;
12
+ private readonly now;
13
+ /** Last write per device, so the throttle survives without touching disk. */
14
+ private readonly lastWriteMs;
15
+ constructor(options: GridLastFrameFileStoreOptions);
16
+ put(deviceId: number, jpeg: Buffer): Promise<void>;
17
+ get(deviceId: number): Promise<Buffer | null>;
18
+ private pathFor;
19
+ }
@@ -0,0 +1,8 @@
1
+ import { GridLayoutPatch, GridLayoutView, ICameraGridLayoutSettingsProvider } from '@camstack/types';
2
+ /** What the provider needs from the running addon — nothing more. */
3
+ export interface GridLayoutSource {
4
+ /** The grid behind a device, or `null` — "answered: not a grid". */
5
+ getLayout(deviceId: number): GridLayoutView | null;
6
+ saveLayout(patch: GridLayoutPatch): Promise<GridLayoutView>;
7
+ }
8
+ export declare function buildGridLayoutNativeProvider(source: GridLayoutSource): ICameraGridLayoutSettingsProvider;
@@ -0,0 +1,22 @@
1
+ import { FfmpegVideoEncoderId } from '@camstack/types';
2
+ import { GridInstance } from './grid-instances.js';
3
+ import { GridStreamPlan } from './grid-stream-session.js';
4
+ export interface GridPlanEncodeContext {
5
+ readonly encoder: FfmpegVideoEncoderId;
6
+ readonly decodeHwAccel: string | null;
7
+ /**
8
+ * The canvas to compose onto, when it is not the one the operator laid out.
9
+ *
10
+ * A grid publishes one composition per profile, and `mid`/`low` compose onto
11
+ * a SCALED copy of the operator's canvas (`grid-profiles.ts` owns the
12
+ * scale). The geometry needs no second version of itself: every rectangle is
13
+ * normalized on both sides (D519), so the same fractions land correctly on a
14
+ * smaller picture. Absent ⇒ the operator's own canvas, which is `high`.
15
+ */
16
+ readonly canvas?: {
17
+ readonly width: number;
18
+ readonly height: number;
19
+ };
20
+ }
21
+ /** `null` when the grid has nothing to compose — the normal freshly-created state. */
22
+ export declare function gridPlanFor(instance: GridInstance, encode: GridPlanEncodeContext): GridStreamPlan | null;
@@ -0,0 +1,47 @@
1
+ import { CamProfile } from '@camstack/types';
2
+ /**
3
+ * One id, for every grid, forever — for the HIGH composition.
4
+ *
5
+ * D519's reason still holds and is what keeps this id bare: a per-instance or
6
+ * newly-spelled id mints a second cam-stream and orphans whatever profile
7
+ * assignment the operator had made. `grid` was `high` on the live hub before
8
+ * this change, so `high` keeps it and the other two are new.
9
+ */
10
+ export declare const GRID_CAM_STREAM_ID = "grid";
11
+ /** Every profile a grid could conceivably compose, in descending quality. */
12
+ export declare const GRID_PROFILES: readonly CamProfile[];
13
+ export interface GridCanvas {
14
+ readonly width: number;
15
+ readonly height: number;
16
+ }
17
+ /** The cam-stream id a grid publishes for one profile. */
18
+ export declare function gridCamStreamId(profile: CamProfile): string;
19
+ /** Even, floored, scaled by the profile. libx264 refuses an odd dimension. */
20
+ export declare function gridCanvasFor(canvas: GridCanvas, profile: CamProfile): GridCanvas;
21
+ export interface OfferedGridProfilesInput {
22
+ /** Every camera the composition takes a cell from, deduplicated. */
23
+ readonly sourceDeviceIds: readonly number[];
24
+ /**
25
+ * Which profiles this source can serve, or `null` when nobody could say.
26
+ * `null` is not an empty list: an unanswerable question is a refusal, and
27
+ * saying so separately is what keeps "we did not ask" out of the catalog.
28
+ */
29
+ readonly profilesServedBy: (deviceId: number) => readonly CamProfile[] | null;
30
+ }
31
+ export type GridProfileOffer = {
32
+ readonly profile: CamProfile;
33
+ readonly offered: true;
34
+ } | {
35
+ readonly profile: CamProfile;
36
+ readonly offered: false;
37
+ /** The sources that could not serve it. Empty only when there are none at all. */
38
+ readonly missingSources: readonly number[];
39
+ };
40
+ /**
41
+ * What this grid can honestly publish, per profile, with the refusal named.
42
+ *
43
+ * Every profile gets a row whether it is offered or not: an absent row says
44
+ * nothing, and the caller logging a refusal needs the reason more than the
45
+ * caller publishing an offer needs the offer.
46
+ */
47
+ export declare function offeredGridProfiles(input: OfferedGridProfilesInput): readonly GridProfileOffer[];
@@ -0,0 +1,10 @@
1
+ export interface NormalizedGridRows {
2
+ readonly rows: readonly Record<string, unknown>[];
3
+ /** `true` when the rows differ from what was read, i.e. they must be persisted. */
4
+ readonly changed: boolean;
5
+ }
6
+ /**
7
+ * @param mintId a UUID factory — injected so the normalization is testable
8
+ * without reaching for `crypto`.
9
+ */
10
+ export declare function normalizeGridRows(raw: readonly Record<string, unknown>[], mintId: () => string): NormalizedGridRows;