@trillboards/edge-sdk 0.2.4 → 0.2.7

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 (204) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -5
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-service.ps1 +6 -1
  30. package/deploy/windows/install-update.ps1 +101 -0
  31. package/deploy/windows/installer.nsi +138 -0
  32. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  33. package/deploy/windows/register-task.ps1 +44 -0
  34. package/deploy/windows/run-connector.ps1 +69 -0
  35. package/deploy/windows/setup-litertlm.ps1 +361 -0
  36. package/deploy/windows/stop-existing.ps1 +72 -0
  37. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  38. package/deploy/windows/trillboards-edge.cmd +33 -0
  39. package/deploy/windows/trillboards-pair.cmd +276 -0
  40. package/deploy/windows/trillboards-setup.ps1 +456 -0
  41. package/deploy/windows/trillboards-setup.vbs +13 -0
  42. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  43. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  44. package/dist/AudienceAnalyzeClient.js +246 -0
  45. package/dist/AudienceAnalyzeClient.js.map +1 -0
  46. package/dist/ChannelProber.d.ts +150 -0
  47. package/dist/ChannelProber.d.ts.map +1 -0
  48. package/dist/ChannelProber.js +887 -0
  49. package/dist/ChannelProber.js.map +1 -0
  50. package/dist/CloudEscalationPolicy.d.ts +143 -0
  51. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  52. package/dist/CloudEscalationPolicy.js +137 -0
  53. package/dist/CloudEscalationPolicy.js.map +1 -0
  54. package/dist/CloudSensingManager.d.ts +557 -0
  55. package/dist/CloudSensingManager.d.ts.map +1 -0
  56. package/dist/CloudSensingManager.js +1267 -0
  57. package/dist/CloudSensingManager.js.map +1 -0
  58. package/dist/DiscoveryReportClient.d.ts +66 -0
  59. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  60. package/dist/DiscoveryReportClient.js +85 -0
  61. package/dist/DiscoveryReportClient.js.map +1 -0
  62. package/dist/EdgeAgent.d.ts +12 -2
  63. package/dist/EdgeAgent.d.ts.map +1 -1
  64. package/dist/EdgeAgent.js +55 -13
  65. package/dist/EdgeAgent.js.map +1 -1
  66. package/dist/LiteRtVlmProvider.d.ts +193 -0
  67. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  68. package/dist/LiteRtVlmProvider.js +436 -0
  69. package/dist/LiteRtVlmProvider.js.map +1 -0
  70. package/dist/MosaicSource.d.ts +108 -0
  71. package/dist/MosaicSource.d.ts.map +1 -0
  72. package/dist/MosaicSource.js +274 -0
  73. package/dist/MosaicSource.js.map +1 -0
  74. package/dist/OnvifClient.d.ts +83 -0
  75. package/dist/OnvifClient.d.ts.map +1 -0
  76. package/dist/OnvifClient.js +209 -0
  77. package/dist/OnvifClient.js.map +1 -0
  78. package/dist/PairClient.d.ts +106 -0
  79. package/dist/PairClient.d.ts.map +1 -0
  80. package/dist/PairClient.js +186 -0
  81. package/dist/PairClient.js.map +1 -0
  82. package/dist/RecorderAddressRecovery.d.ts +53 -0
  83. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  84. package/dist/RecorderAddressRecovery.js +359 -0
  85. package/dist/RecorderAddressRecovery.js.map +1 -0
  86. package/dist/SceneChangeDetector.d.ts +291 -0
  87. package/dist/SceneChangeDetector.d.ts.map +1 -0
  88. package/dist/SceneChangeDetector.js +682 -0
  89. package/dist/SceneChangeDetector.js.map +1 -0
  90. package/dist/SensingProfileClient.d.ts +59 -0
  91. package/dist/SensingProfileClient.d.ts.map +1 -0
  92. package/dist/SensingProfileClient.js +78 -0
  93. package/dist/SensingProfileClient.js.map +1 -0
  94. package/dist/SetupSessionClient.d.ts +114 -0
  95. package/dist/SetupSessionClient.d.ts.map +1 -0
  96. package/dist/SetupSessionClient.js +95 -0
  97. package/dist/SetupSessionClient.js.map +1 -0
  98. package/dist/VlmAdmissionGate.d.ts +126 -0
  99. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  100. package/dist/VlmAdmissionGate.js +166 -0
  101. package/dist/VlmAdmissionGate.js.map +1 -0
  102. package/dist/autoSense.d.ts +266 -0
  103. package/dist/autoSense.d.ts.map +1 -0
  104. package/dist/autoSense.js +606 -0
  105. package/dist/autoSense.js.map +1 -0
  106. package/dist/cli.d.ts +27 -1
  107. package/dist/cli.d.ts.map +1 -1
  108. package/dist/cli.js +526 -27
  109. package/dist/cli.js.map +1 -1
  110. package/dist/combinedSense.d.ts +128 -0
  111. package/dist/combinedSense.d.ts.map +1 -0
  112. package/dist/combinedSense.js +261 -0
  113. package/dist/combinedSense.js.map +1 -0
  114. package/dist/config.d.ts +351 -0
  115. package/dist/config.d.ts.map +1 -1
  116. package/dist/config.js +99 -2
  117. package/dist/config.js.map +1 -1
  118. package/dist/connectorCadence.d.ts +135 -0
  119. package/dist/connectorCadence.d.ts.map +1 -0
  120. package/dist/connectorCadence.js +151 -0
  121. package/dist/connectorCadence.js.map +1 -0
  122. package/dist/dataDir.d.ts +33 -0
  123. package/dist/dataDir.d.ts.map +1 -0
  124. package/dist/dataDir.js +76 -0
  125. package/dist/dataDir.js.map +1 -0
  126. package/dist/deployedCadence.d.ts +167 -0
  127. package/dist/deployedCadence.d.ts.map +1 -0
  128. package/dist/deployedCadence.js +209 -0
  129. package/dist/deployedCadence.js.map +1 -0
  130. package/dist/deployedCadenceSource.d.ts +48 -0
  131. package/dist/deployedCadenceSource.d.ts.map +1 -0
  132. package/dist/deployedCadenceSource.js +74 -0
  133. package/dist/deployedCadenceSource.js.map +1 -0
  134. package/dist/diagnose.d.ts +1 -1
  135. package/dist/diagnose.d.ts.map +1 -1
  136. package/dist/diagnose.js +15 -13
  137. package/dist/diagnose.js.map +1 -1
  138. package/dist/discovery.d.ts +131 -0
  139. package/dist/discovery.d.ts.map +1 -0
  140. package/dist/discovery.js +499 -0
  141. package/dist/discovery.js.map +1 -0
  142. package/dist/gemmaModelOta.d.ts +118 -0
  143. package/dist/gemmaModelOta.d.ts.map +1 -0
  144. package/dist/gemmaModelOta.js +291 -0
  145. package/dist/gemmaModelOta.js.map +1 -0
  146. package/dist/index.d.ts +16 -1
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +40 -1
  149. package/dist/index.js.map +1 -1
  150. package/dist/init.d.ts.map +1 -1
  151. package/dist/init.js +25 -87
  152. package/dist/init.js.map +1 -1
  153. package/dist/keepAwake.d.ts +33 -0
  154. package/dist/keepAwake.d.ts.map +1 -0
  155. package/dist/keepAwake.js +54 -0
  156. package/dist/keepAwake.js.map +1 -0
  157. package/dist/legacyRecorderProfile.d.ts +20 -0
  158. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  159. package/dist/legacyRecorderProfile.js +27 -0
  160. package/dist/legacyRecorderProfile.js.map +1 -0
  161. package/dist/localInferenceSchema.d.ts +132 -0
  162. package/dist/localInferenceSchema.d.ts.map +1 -0
  163. package/dist/localInferenceSchema.js +274 -0
  164. package/dist/localInferenceSchema.js.map +1 -0
  165. package/dist/localSenseE2eHarness.d.ts +101 -0
  166. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  167. package/dist/localSenseE2eHarness.js +279 -0
  168. package/dist/localSenseE2eHarness.js.map +1 -0
  169. package/dist/localSensing.d.ts +284 -0
  170. package/dist/localSensing.d.ts.map +1 -0
  171. package/dist/localSensing.js +512 -0
  172. package/dist/localSensing.js.map +1 -0
  173. package/dist/mcp-server.d.ts.map +1 -1
  174. package/dist/mcp-server.js +3 -1
  175. package/dist/mcp-server.js.map +1 -1
  176. package/dist/modelIntegrity.d.ts +80 -0
  177. package/dist/modelIntegrity.d.ts.map +1 -0
  178. package/dist/modelIntegrity.js +194 -0
  179. package/dist/modelIntegrity.js.map +1 -0
  180. package/dist/pairVerify.d.ts +100 -0
  181. package/dist/pairVerify.d.ts.map +1 -0
  182. package/dist/pairVerify.js +296 -0
  183. package/dist/pairVerify.js.map +1 -0
  184. package/dist/qrScanner.d.ts +33 -0
  185. package/dist/qrScanner.d.ts.map +1 -0
  186. package/dist/qrScanner.js +103 -0
  187. package/dist/qrScanner.js.map +1 -0
  188. package/dist/runtimePaths.d.ts +10 -0
  189. package/dist/runtimePaths.d.ts.map +1 -0
  190. package/dist/runtimePaths.js +79 -0
  191. package/dist/runtimePaths.js.map +1 -0
  192. package/dist/sensingRunMode.d.ts +86 -0
  193. package/dist/sensingRunMode.d.ts.map +1 -0
  194. package/dist/sensingRunMode.js +346 -0
  195. package/dist/sensingRunMode.js.map +1 -0
  196. package/dist/setupMode.d.ts +98 -0
  197. package/dist/setupMode.d.ts.map +1 -0
  198. package/dist/setupMode.js +828 -0
  199. package/dist/setupMode.js.map +1 -0
  200. package/dist/setupProgress.d.ts +31 -0
  201. package/dist/setupProgress.d.ts.map +1 -0
  202. package/dist/setupProgress.js +173 -0
  203. package/dist/setupProgress.js.map +1 -0
  204. package/package.json +13 -4
@@ -0,0 +1,108 @@
1
+ import type { CameraProvider } from '@trillboards/edge-core';
2
+ import type { MosaicTile } from './config';
3
+ /**
4
+ * Wraps the FULL-mosaic frame provider so its single RTSP pull is shared by all
5
+ * tiles. The tiles' capture timers fire together (they start on the same
6
+ * cadence), so a tile asking within `maxAgeMs` of the last pull reuses that
7
+ * frame; concurrent callers coalesce onto one inflight pull. The cached frame is
8
+ * a co-temporal capture handed to sibling tiles — NOT a stored/replayed frame
9
+ * (it is always younger than the capture cadence).
10
+ */
11
+ export declare class SharedFrameSource {
12
+ private readonly source;
13
+ private readonly maxAgeMs;
14
+ private readonly nowFn;
15
+ private refs;
16
+ private last;
17
+ private inflight;
18
+ /** Coalesces concurrent ensureOpen() re-opens onto a single source.open(). */
19
+ private reopening;
20
+ constructor(source: CameraProvider, opts?: {
21
+ maxAgeMs?: number;
22
+ now?: () => number;
23
+ });
24
+ /** A tile attaches — open the underlying source on the first attach. */
25
+ acquire(): Promise<void>;
26
+ /** A tile detaches — close the underlying source when the last one leaves. */
27
+ release(): Promise<void>;
28
+ isOpen(): boolean;
29
+ /**
30
+ * Re-open the underlying source if it has dropped (e.g. the RTSP grabber gave
31
+ * up its per-attempt retries and closed itself), WITHOUT touching the
32
+ * refcount — this is recovery of an existing attachment, not a new tile
33
+ * joining. Concurrent re-opens (sibling tiles reconnecting on the same tick)
34
+ * coalesce onto one `source.open()`. The stale frame cache is cleared so the
35
+ * next getFrame() pulls fresh from the recovered source. No-op when already
36
+ * open. This is what lets mosaic feeds self-recover from an RTSP drop instead
37
+ * of staying down until the process restarts.
38
+ */
39
+ ensureOpen(): Promise<void>;
40
+ /** Source (mosaic) native resolution the tile crop coordinates are relative to. */
41
+ getResolution(): {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ /** Full mosaic frame as a JPEG Buffer — cached/coalesced single pull. */
46
+ getFrame(): Promise<Buffer>;
47
+ }
48
+ export interface CropViewProviderOptions {
49
+ /** Sub-rectangle of the source frame for this tile (source pixels). Omit = full frame. */
50
+ crop?: MosaicTile;
51
+ /** Tile output width (px) — the sensing input size. */
52
+ width: number;
53
+ /** Tile output height (px). */
54
+ height: number;
55
+ /** Source (mosaic) native width/height — used to skip a re-encode for a full-frame view. */
56
+ sourceWidth?: number;
57
+ sourceHeight?: number;
58
+ /** Advisory fps reported to the cadence loop (default 8). */
59
+ fps?: number;
60
+ /** ffmpeg binary path (default 'ffmpeg'). */
61
+ ffmpegPath?: string;
62
+ /** JPEG quality 1-100 for the carved tile (default 70). */
63
+ jpegQuality?: number;
64
+ /** Carve timeout (ms) before the ffmpeg child is killed (default 8000). */
65
+ carveTimeoutMs?: number;
66
+ /**
67
+ * Carve output pixel format. 'jpeg' (default) — the VLM/cloud path that POSTs a
68
+ * JPEG. 'rgb24' — raw RGB24 (HWC) for the on-device ONNX zoo (AudienceSensingService
69
+ * → FaceDetector consumes raw RGB pixels, not JPEG). Both carve from the SAME
70
+ * cached mosaic pull; only the ffmpeg output encoding differs.
71
+ */
72
+ outputFormat?: 'jpeg' | 'rgb24';
73
+ }
74
+ /**
75
+ * A CameraProvider that yields one TILE of a shared mosaic frame. getFrame()
76
+ * pulls the (cached) full frame from the SharedFrameSource, then carves this
77
+ * tile in a single ffmpeg pass (`-i pipe:0 -vf crop=…,scale=… -f mjpeg`). A
78
+ * full-frame view at the source's own resolution skips the re-encode and returns
79
+ * the cached frame as-is.
80
+ */
81
+ export declare class CropViewProvider implements CameraProvider {
82
+ private readonly shared;
83
+ private readonly crop?;
84
+ private readonly width;
85
+ private readonly height;
86
+ private readonly sourceWidth?;
87
+ private readonly sourceHeight?;
88
+ private readonly fps;
89
+ private readonly ffmpegPath;
90
+ private readonly jpegQuality;
91
+ private readonly carveTimeoutMs;
92
+ private readonly outputFormat;
93
+ private opened;
94
+ constructor(shared: SharedFrameSource, opts: CropViewProviderOptions);
95
+ open(_deviceId?: string): Promise<void>;
96
+ close(): Promise<void>;
97
+ isOpen(): boolean;
98
+ getResolution(): {
99
+ width: number;
100
+ height: number;
101
+ };
102
+ getFps(): number;
103
+ getFrame(): Promise<Buffer>;
104
+ /** Carve this tile out of the cached full JPEG with a single ffmpeg pass.
105
+ * Output is JPEG (VLM/cloud) or raw RGB24 (on-device ONNX zoo) per outputFormat. */
106
+ private carve;
107
+ }
108
+ //# sourceMappingURL=MosaicSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MosaicSource.d.ts","sourceRoot":"","sources":["../src/MosaicSource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CropViewProvider = exports.SharedFrameSource = void 0;
4
+ const child_process_1 = require("child_process");
5
+ // edge-platform-rtsp is consumed by runtime require() (never a typed top-level
6
+ // import) throughout edge-sdk — see RTSPCameraProvider usage in CloudSensingManager
7
+ // / EdgeAgent / localSensing. We reuse its ffmpeg-arg helpers the same way so the
8
+ // tile carve math is the SAME function the RTSP pull uses (one source of truth).
9
+ // require() is memoized, so this resolves once.
10
+ function rtspHelpers() {
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ return require('@trillboards/edge-platform-rtsp');
13
+ }
14
+ /**
15
+ * Mosaic tiling — one RTSP pull shared by N tiles.
16
+ *
17
+ * A composite ("mosaic"/quad) CCTV feed is a SINGLE RTSP stream that shows
18
+ * several cameras in a grid. Each grid cell becomes its own Trillboards screen +
19
+ * sensing profile. Pulling the stream once per tile would be N RTSP sessions for
20
+ * one stream (N× the bandwidth — and many DVRs cap concurrent sessions), so
21
+ * instead the source is pulled ONCE here and every tile gets a cropped VIEW of
22
+ * the same captured frame, carved in ffmpeg (NOT a JS buffer copy) so the carve
23
+ * math is identical to the in-pass RTSP crop (buildVideoFilter is shared).
24
+ *
25
+ * @module MosaicSource
26
+ */
27
+ const DEFAULT_SHARED_FRAME_MAX_AGE_MS = 5000;
28
+ const DEFAULT_CARVE_TIMEOUT_MS = 8000;
29
+ const DEFAULT_JPEG_QUALITY = 70;
30
+ const DEFAULT_FPS = 8;
31
+ function toBuffer(frame) {
32
+ return Buffer.isBuffer(frame) ? frame : Buffer.from(frame.data.buffer);
33
+ }
34
+ /**
35
+ * Wraps the FULL-mosaic frame provider so its single RTSP pull is shared by all
36
+ * tiles. The tiles' capture timers fire together (they start on the same
37
+ * cadence), so a tile asking within `maxAgeMs` of the last pull reuses that
38
+ * frame; concurrent callers coalesce onto one inflight pull. The cached frame is
39
+ * a co-temporal capture handed to sibling tiles — NOT a stored/replayed frame
40
+ * (it is always younger than the capture cadence).
41
+ */
42
+ class SharedFrameSource {
43
+ constructor(source, opts) {
44
+ this.refs = 0;
45
+ this.last = null;
46
+ this.inflight = null;
47
+ /** Coalesces concurrent ensureOpen() re-opens onto a single source.open(). */
48
+ this.reopening = null;
49
+ this.source = source;
50
+ this.maxAgeMs = opts?.maxAgeMs ?? DEFAULT_SHARED_FRAME_MAX_AGE_MS;
51
+ this.nowFn = opts?.now ?? Date.now;
52
+ }
53
+ /** A tile attaches — open the underlying source on the first attach. */
54
+ async acquire() {
55
+ this.refs++;
56
+ if (!this.source.isOpen()) {
57
+ await this.source.open();
58
+ }
59
+ }
60
+ /** A tile detaches — close the underlying source when the last one leaves. */
61
+ async release() {
62
+ this.refs = Math.max(0, this.refs - 1);
63
+ if (this.refs === 0) {
64
+ this.last = null;
65
+ if (this.source.isOpen()) {
66
+ await this.source.close();
67
+ }
68
+ }
69
+ }
70
+ isOpen() {
71
+ return this.source.isOpen();
72
+ }
73
+ /**
74
+ * Re-open the underlying source if it has dropped (e.g. the RTSP grabber gave
75
+ * up its per-attempt retries and closed itself), WITHOUT touching the
76
+ * refcount — this is recovery of an existing attachment, not a new tile
77
+ * joining. Concurrent re-opens (sibling tiles reconnecting on the same tick)
78
+ * coalesce onto one `source.open()`. The stale frame cache is cleared so the
79
+ * next getFrame() pulls fresh from the recovered source. No-op when already
80
+ * open. This is what lets mosaic feeds self-recover from an RTSP drop instead
81
+ * of staying down until the process restarts.
82
+ */
83
+ async ensureOpen() {
84
+ if (this.source.isOpen())
85
+ return;
86
+ if (this.reopening)
87
+ return this.reopening;
88
+ this.reopening = (async () => {
89
+ try {
90
+ if (!this.source.isOpen()) {
91
+ await this.source.open();
92
+ this.last = null; // drop stale cache from before the outage
93
+ }
94
+ }
95
+ finally {
96
+ this.reopening = null;
97
+ }
98
+ })();
99
+ return this.reopening;
100
+ }
101
+ /** Source (mosaic) native resolution the tile crop coordinates are relative to. */
102
+ getResolution() {
103
+ return this.source.getResolution();
104
+ }
105
+ /** Full mosaic frame as a JPEG Buffer — cached/coalesced single pull. */
106
+ async getFrame() {
107
+ if (this.last && this.nowFn() - this.last.at < this.maxAgeMs) {
108
+ return this.last.frame;
109
+ }
110
+ if (this.inflight) {
111
+ return this.inflight;
112
+ }
113
+ this.inflight = (async () => {
114
+ try {
115
+ const frame = toBuffer(await this.source.getFrame());
116
+ this.last = { frame, at: this.nowFn() };
117
+ return frame;
118
+ }
119
+ finally {
120
+ this.inflight = null;
121
+ }
122
+ })();
123
+ return this.inflight;
124
+ }
125
+ }
126
+ exports.SharedFrameSource = SharedFrameSource;
127
+ /**
128
+ * A CameraProvider that yields one TILE of a shared mosaic frame. getFrame()
129
+ * pulls the (cached) full frame from the SharedFrameSource, then carves this
130
+ * tile in a single ffmpeg pass (`-i pipe:0 -vf crop=…,scale=… -f mjpeg`). A
131
+ * full-frame view at the source's own resolution skips the re-encode and returns
132
+ * the cached frame as-is.
133
+ */
134
+ class CropViewProvider {
135
+ constructor(shared, opts) {
136
+ this.opened = false;
137
+ this.shared = shared;
138
+ this.crop = opts.crop;
139
+ this.width = opts.width;
140
+ this.height = opts.height;
141
+ this.sourceWidth = opts.sourceWidth;
142
+ this.sourceHeight = opts.sourceHeight;
143
+ this.fps = opts.fps ?? DEFAULT_FPS;
144
+ this.ffmpegPath = opts.ffmpegPath ?? 'ffmpeg';
145
+ this.jpegQuality = opts.jpegQuality ?? DEFAULT_JPEG_QUALITY;
146
+ this.carveTimeoutMs = opts.carveTimeoutMs ?? DEFAULT_CARVE_TIMEOUT_MS;
147
+ this.outputFormat = opts.outputFormat ?? 'jpeg';
148
+ }
149
+ async open(_deviceId) {
150
+ if (this.opened) {
151
+ // Already attached (refcount held) but the shared source dropped — recover
152
+ // it WITHOUT acquiring another ref. This is the reconnect path the manager
153
+ // drives after a mosaic RTSP drop; a plain acquire() here would leak refs
154
+ // so the source would never close on shutdown.
155
+ await this.shared.ensureOpen();
156
+ return;
157
+ }
158
+ await this.shared.acquire();
159
+ this.opened = true;
160
+ }
161
+ async close() {
162
+ if (!this.opened)
163
+ return;
164
+ this.opened = false;
165
+ await this.shared.release();
166
+ }
167
+ isOpen() {
168
+ // Reflect the underlying shared source's health, not just our own attach
169
+ // flag: when the mosaic RTSP grabber drops itself, this tile must report NOT
170
+ // open so the cadence loop's reconnect path fires (it never would if we only
171
+ // returned `this.opened`). See CloudSensingManager.runScheduledTick.
172
+ return this.opened && this.shared.isOpen();
173
+ }
174
+ getResolution() {
175
+ return { width: this.width, height: this.height };
176
+ }
177
+ getFps() {
178
+ return this.fps;
179
+ }
180
+ async getFrame() {
181
+ if (!this.opened) {
182
+ throw new Error('CropViewProvider not opened. Call open() first.');
183
+ }
184
+ const full = await this.shared.getFrame();
185
+ // Full-frame JPEG view at the source's own resolution → no crop, no rescale,
186
+ // so ship the shared (JPEG) frame untouched (the source feed-as-screen path).
187
+ // The rgb24 zoo path can never take this fast-path: the shared pull is JPEG
188
+ // and the zoo needs decoded raw RGB24, so it always carves (decodes) below.
189
+ if (this.outputFormat === 'jpeg' &&
190
+ !this.crop &&
191
+ this.sourceWidth === this.width &&
192
+ this.sourceHeight === this.height) {
193
+ return full;
194
+ }
195
+ return this.carve(full);
196
+ }
197
+ /** Carve this tile out of the cached full JPEG with a single ffmpeg pass.
198
+ * Output is JPEG (VLM/cloud) or raw RGB24 (on-device ONNX zoo) per outputFormat. */
199
+ carve(fullJpeg) {
200
+ const { buildVideoFilter, mjpegQv } = rtspHelpers();
201
+ const rgb24 = this.outputFormat === 'rgb24';
202
+ return new Promise((resolve, reject) => {
203
+ const args = [
204
+ '-i', 'pipe:0',
205
+ '-frames:v', '1',
206
+ '-vf', buildVideoFilter(this.width, this.height, this.crop),
207
+ // rgb24: raw HWC RGB pixels the ONNX FaceDetector consumes directly.
208
+ // jpeg: re-encoded tile for the cloud/VLM POST.
209
+ ...(rgb24
210
+ ? ['-f', 'rawvideo', '-pix_fmt', 'rgb24']
211
+ : ['-f', 'mjpeg', '-q:v', String(mjpegQv(this.jpegQuality))]),
212
+ 'pipe:1',
213
+ ];
214
+ const proc = (0, child_process_1.spawn)(this.ffmpegPath, args, {
215
+ stdio: ['pipe', 'pipe', 'pipe'],
216
+ });
217
+ const chunks = [];
218
+ let stderr = '';
219
+ let settled = false;
220
+ const killTimer = setTimeout(() => {
221
+ proc.kill('SIGKILL');
222
+ if (!settled) {
223
+ settled = true;
224
+ reject(new Error('Mosaic tile carve timed out'));
225
+ }
226
+ }, this.carveTimeoutMs);
227
+ proc.stdout.on('data', (c) => chunks.push(c));
228
+ proc.stderr.on('data', (c) => {
229
+ stderr += c.toString('utf-8');
230
+ });
231
+ proc.on('error', (err) => {
232
+ clearTimeout(killTimer);
233
+ if (settled)
234
+ return;
235
+ settled = true;
236
+ reject(new Error(`Failed to spawn ffmpeg for tile carve: ${err.message}`));
237
+ });
238
+ proc.on('close', (code) => {
239
+ clearTimeout(killTimer);
240
+ if (settled)
241
+ return;
242
+ settled = true;
243
+ if (code !== 0) {
244
+ reject(new Error(`ffmpeg tile carve exited with code ${code}: ${stderr.slice(0, 500)}`));
245
+ return;
246
+ }
247
+ const out = Buffer.concat(chunks);
248
+ if (rgb24) {
249
+ // Raw RGB24 = exactly width*height*3 bytes (HWC, 8-bit, 3 channels).
250
+ const expected = this.width * this.height * 3;
251
+ if (out.length !== expected) {
252
+ reject(new Error(`ffmpeg tile carve produced ${out.length} bytes, expected ${expected} ` +
253
+ `for ${this.width}x${this.height} rgb24`));
254
+ return;
255
+ }
256
+ }
257
+ else if (out.length < 3 || out[0] !== 0xff || out[1] !== 0xd8) {
258
+ reject(new Error(`ffmpeg tile carve produced an invalid JPEG (${out.length} bytes)`));
259
+ return;
260
+ }
261
+ resolve(out);
262
+ });
263
+ // Feed the cached full JPEG to ffmpeg's stdin. Ignore EPIPE — if ffmpeg
264
+ // exits early the 'close' handler already reports the real error.
265
+ proc.stdin.on('error', () => {
266
+ /* ignore broken-pipe on early ffmpeg exit */
267
+ });
268
+ proc.stdin.write(fullJpeg);
269
+ proc.stdin.end();
270
+ });
271
+ }
272
+ }
273
+ exports.CropViewProvider = CropViewProvider;
274
+ //# sourceMappingURL=MosaicSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MosaicSource.js","sourceRoot":"","sources":["../src/MosaicSource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { type HttpPoster } from './AudienceAnalyzeClient';
2
+ /**
3
+ * OnvifClient — minimal AUTHENTICATED ONVIF SOAP client for the BYO-CCTV setup
4
+ * probe. Three calls, which together give the setup flow everything the
5
+ * adversarial review demanded:
6
+ *
7
+ * GetDeviceInformation → manufacturer/model/SERIAL (the stable physical
8
+ * identity behind channelKey — survives DHCP lease changes, unlike host IPs)
9
+ * GetProfiles → the recorder's REAL channel/profile list (no more
10
+ * "assume 16 channels" — the Hiten failure)
11
+ * GetStreamUri → the actual per-profile RTSP URL (no more guessed
12
+ * /Streaming/Channels/101 paths)
13
+ *
14
+ * Deliberately dependency-free: WS-UsernameToken digest auth + regex XML
15
+ * extraction, following the discovery.ts precedent (parseProbeMatches) — a
16
+ * full SOAP/XML stack is overkill for three fixed calls and would bloat the
17
+ * self-contained installer. Transport is the same injectable poster the other
18
+ * clients use (unit tests never touch the network).
19
+ *
20
+ * AUTH-SAFETY: callers (ChannelProber) MUST treat an auth failure here as a
21
+ * circuit-breaker signal for the whole host — recorders lock accounts after a
22
+ * handful of bad logins (Hikvision ~5), and locking the owner's own NVR is a
23
+ * self-inflicted support incident.
24
+ */
25
+ export interface OnvifDeviceInfo {
26
+ manufacturer: string | null;
27
+ model: string | null;
28
+ serialNumber: string | null;
29
+ firmwareVersion: string | null;
30
+ }
31
+ export interface OnvifProfile {
32
+ token: string;
33
+ name: string | null;
34
+ /** Video source token — distinct physical channels have distinct sources. */
35
+ videoSourceToken: string | null;
36
+ }
37
+ export type OnvifFailure = 'auth' | 'unreachable' | 'not_onvif' | 'fault';
38
+ export declare class OnvifError extends Error {
39
+ kind: OnvifFailure;
40
+ constructor(kind: OnvifFailure, message: string);
41
+ }
42
+ export interface OnvifClientOptions {
43
+ host: string;
44
+ /** ONVIF HTTP port. Default 80 (cheap DVRs often use 8899 — callers try both). */
45
+ port?: number;
46
+ username: string;
47
+ password: string;
48
+ timeoutMs?: number;
49
+ }
50
+ /** Build the WS-Security UsernameToken header (PasswordDigest per ONVIF core). */
51
+ export declare function buildUsernameToken(username: string, password: string, nonceRaw?: Buffer, created?: string): string;
52
+ export declare function escapeXml(s: string): string;
53
+ /** Decode the XML entities recorders emit inside tag text (Dahua returns
54
+ * `...?channel=1&amp;subtype=0` — feeding the raw text into new URL/ffmpeg
55
+ * probes the wrong stream and persists an unusable pathTemplate). */
56
+ export declare function decodeXmlEntities(s: string): string;
57
+ /** First match group of a namespace-agnostic tag (entity-decoded), or null. */
58
+ export declare function xmlTag(xml: string, localName: string): string | null;
59
+ /** All matches (full element text) of a namespace-agnostic tag. */
60
+ export declare function xmlTags(xml: string, localName: string): string[];
61
+ export declare class OnvifClient {
62
+ private readonly host;
63
+ private readonly port;
64
+ private readonly username;
65
+ private readonly password;
66
+ private readonly timeoutMs;
67
+ private readonly poster;
68
+ /** Media service endpoint discovered via GetServices (falls back to device). */
69
+ private mediaXAddr;
70
+ constructor(options: OnvifClientOptions, poster?: HttpPoster);
71
+ deviceEndpoint(): string;
72
+ private soap;
73
+ /** Stable physical identity (the channelKey backbone). */
74
+ getDeviceInformation(): Promise<OnvifDeviceInfo>;
75
+ /** Resolve the media service endpoint (GetServices; fall back to device path). */
76
+ private resolveMediaXAddr;
77
+ private rewriteHost;
78
+ /** The recorder's REAL channel/profile list. */
79
+ getProfiles(): Promise<OnvifProfile[]>;
80
+ /** The ACTUAL RTSP URI for one profile (no guessed brand paths). */
81
+ getStreamUri(profileToken: string): Promise<string | null>;
82
+ }
83
+ //# sourceMappingURL=OnvifClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OnvifClient.d.ts","sourceRoot":"","sources":["../src/OnvifClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,CAAC;AAE1E,qBAAa,UAAW,SAAQ,KAAK;IACnC,IAAI,EAAE,YAAY,CAAC;gBACP,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM;CAIhD;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAAwB,EAClC,OAAO,GAAE,MAAiC,GACzC,MAAM,CAeR;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO3C;AAWD;;qEAEqE;AACrE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,+EAA+E;AAC/E,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpE;AAED,mEAAmE;AACnE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAShE;AAOD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,gFAAgF;IAChF,OAAO,CAAC,UAAU,CAAuB;gBAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,UAAU;IAS5D,cAAc,IAAI,MAAM;YAIV,IAAI;IAgClB,0DAA0D;IACpD,oBAAoB,IAAI,OAAO,CAAC,eAAe,CAAC;IAatD,kFAAkF;YACpE,iBAAiB;IA2B/B,OAAO,CAAC,WAAW;IAWnB,gDAAgD;IAC1C,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAiB5C,oEAAoE;IAC9D,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAoBjE"}