@camstack/server 1.0.0 → 1.0.1

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 (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,615 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-argument -- test file, mock typing */
2
- /**
3
- * Stream diagnostic — measures quality, latency, and fanout.
4
- * Run: FRIGATE_HOST=192.168.1.128 npx vitest run server/backend/src/__tests__/streaming-diagnostic.test.ts --reporter verbose
5
- */
6
-
7
- // E2E diagnostic test with heavy broker usage — file-level disable for cascading unsafe rules
8
- /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
9
-
10
- import { describe, it, expect, afterAll } from 'vitest'
11
- import { StreamBrokerManager, FfmpegDecoderProvider } from '@camstack/addon-pipeline/stream-broker'
12
- import type { EncodedPacket, FrameHandle, IStreamBroker } from '@camstack/types'
13
- import type { IScopedLogger } from '@camstack/types'
14
-
15
- const FRIGATE = process.env.FRIGATE_HOST ?? ''
16
- const STREAMS = [
17
- { id: 'ingresso_main', url: `rtsp://${FRIGATE}:8554/ingresso_main`, codec: 'h264' },
18
- { id: 'salone_main', url: `rtsp://${FRIGATE}:8554/salone_main`, codec: 'h264' },
19
- ]
20
-
21
- const mockLogger: IScopedLogger = {
22
- debug: () => {},
23
- info: () => {},
24
- warn: console.warn,
25
- error: console.error,
26
- child: () => mockLogger,
27
- }
28
-
29
- const mockLoggingService = {
30
- createLogger: () => mockLogger,
31
- }
32
-
33
- function wait(ms: number) {
34
- return new Promise((r) => setTimeout(r, ms))
35
- }
36
-
37
- function waitForFrames(count: number, frames: unknown[], timeoutMs: number): Promise<void> {
38
- return new Promise((resolve, reject) => {
39
- const start = Date.now()
40
- const iv = setInterval(() => {
41
- if (frames.length >= count) {
42
- clearInterval(iv)
43
- resolve()
44
- }
45
- if (Date.now() - start > timeoutMs) {
46
- clearInterval(iv)
47
- reject(new Error(`Timeout: got ${frames.length}/${count} frames`))
48
- }
49
- }, 50)
50
- })
51
- }
52
-
53
- /**
54
- * Poll-based replacement for the removed `IStreamBroker.onDecodedFrame`
55
- * callback (Phase 5 / D9 — decoded frames now travel as shm `FrameHandle`s).
56
- * Opens a frame-handle subscription, drains `pullFrameHandles` on a 50ms
57
- * timer, and invokes `onHandle` once per decoded frame. The returned
58
- * function tears the subscription down. `byteLength` stands in for the old
59
- * `DecodedFrame.data.length`; `pts` for `DecodedFrame.timestamp`.
60
- */
61
- async function subscribeDecodedFrames(
62
- broker: IStreamBroker,
63
- maxFps: number,
64
- onHandle: (handle: FrameHandle) => void,
65
- ): Promise<() => Promise<void>> {
66
- const { subscriptionId } = await broker.subscribeFrameHandles({ format: 'rgb', maxFps })
67
- const iv = setInterval(() => {
68
- for (const handle of broker.pullFrameHandles(subscriptionId, 8)) {
69
- onHandle(handle)
70
- }
71
- }, 50)
72
- return async () => {
73
- clearInterval(iv)
74
- await broker.unsubscribeFrameHandles(subscriptionId)
75
- }
76
- }
77
-
78
- describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
79
- const manager = new StreamBrokerManager([new FfmpegDecoderProvider()], mockLogger)
80
-
81
- afterAll(async () => {
82
- await manager.destroyAll()
83
- })
84
-
85
- it('measures first-frame latency per stream', async () => {
86
- console.log('\n--- First-Frame Latency ---')
87
-
88
- for (const s of STREAMS) {
89
- const broker = await manager.createBroker(`latency-${s.id}`, {
90
- type: 'rtsp',
91
- url: s.url,
92
- videoCodec: s.codec,
93
- })
94
- const startMs = Date.now()
95
- await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: s.codec })
96
-
97
- const result = await new Promise<{ latencyMs: number; sizeKB: number }>((resolve, reject) => {
98
- const timeout = setTimeout(() => reject(new Error('No frame in 15s')), 15000)
99
- let unsub: (() => Promise<void>) | undefined
100
- subscribeDecodedFrames(broker, 10, (handle: FrameHandle) => {
101
- clearTimeout(timeout)
102
- void unsub?.()
103
- resolve({
104
- latencyMs: Date.now() - startMs,
105
- sizeKB: +(handle.byteLength / 1024).toFixed(1),
106
- })
107
- })
108
- .then((fn) => {
109
- unsub = fn
110
- })
111
- .catch(reject)
112
- })
113
-
114
- console.log(` ${s.id}: ${result.latencyMs}ms, frame=${result.sizeKB}KB`)
115
- expect(result.latencyMs).toBeLessThan(15000)
116
- expect(result.sizeKB).toBeGreaterThan(1) // at least 1KB JPEG
117
-
118
- await broker.stop()
119
- await manager.destroyBroker(`latency-${s.id}`)
120
- }
121
- }, 30000)
122
-
123
- it('measures frame quality over 5 seconds', async () => {
124
- console.log('\n--- Frame Quality (5s @ 5fps) ---')
125
-
126
- const broker = await manager.createBroker('quality', {
127
- type: 'rtsp',
128
- url: STREAMS[0]!.url,
129
- videoCodec: 'h264',
130
- })
131
- await (broker as any).start({ type: 'rtsp', url: STREAMS[0]!.url, videoCodec: 'h264' })
132
-
133
- const frames: { sizeKB: number; ts: number }[] = []
134
-
135
- // Wait for first frame before measuring
136
- await new Promise<void>((resolve, reject) => {
137
- const timeout = setTimeout(() => reject(new Error('No initial frame')), 10000)
138
- let unsub: (() => Promise<void>) | undefined
139
- subscribeDecodedFrames(broker, 5, () => {
140
- clearTimeout(timeout)
141
- void unsub?.()
142
- resolve()
143
- })
144
- .then((fn) => {
145
- unsub = fn
146
- })
147
- .catch(reject)
148
- })
149
-
150
- // Now collect for 5 seconds
151
- const unsub = await subscribeDecodedFrames(broker, 5, (handle: FrameHandle) => {
152
- frames.push({ sizeKB: +(handle.byteLength / 1024).toFixed(1), ts: handle.pts })
153
- })
154
-
155
- await wait(5000)
156
- await unsub()
157
-
158
- const sizes = frames.map((f) => f.sizeKB)
159
- const avgSize = +(sizes.reduce((a, b) => a + b, 0) / sizes.length).toFixed(1)
160
- const minSize = Math.min(...sizes)
161
- const maxSize = Math.max(...sizes)
162
-
163
- // Frame-to-frame gaps
164
- const gaps: number[] = []
165
- for (let i = 1; i < frames.length; i++) {
166
- gaps.push(frames[i]!.ts - frames[i - 1]!.ts)
167
- }
168
- const avgGap = gaps.length > 0 ? Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length) : 0
169
- const maxGap = gaps.length > 0 ? Math.max(...gaps) : 0
170
-
171
- console.log(` Frames: ${frames.length} (expected ~25 @ 5fps)`)
172
- console.log(` JPEG size: avg=${avgSize}KB, min=${minSize}KB, max=${maxSize}KB`)
173
- console.log(` Frame gap: avg=${avgGap}ms, max=${maxGap}ms (expected ~200ms @ 5fps)`)
174
- console.log(` All valid: ${frames.every((f) => f.sizeKB > 0.5) ? 'YES' : 'NO'}`)
175
-
176
- expect(frames.length).toBeGreaterThan(10)
177
- expect(avgSize).toBeGreaterThan(1) // > 1KB avg = real JPEG content
178
- expect(avgGap).toBeLessThan(500) // not more than 500ms avg gap at 5fps
179
-
180
- await broker.stop()
181
- await manager.destroyBroker('quality')
182
- }, 30000)
183
-
184
- it('measures fanout: encoded + 2 decoded subscribers', async () => {
185
- console.log('\n--- Fanout (3s, encoded + 2 decoded) ---')
186
-
187
- const broker = await manager.createBroker('fanout', {
188
- type: 'rtsp',
189
- url: STREAMS[0]!.url,
190
- videoCodec: 'h264',
191
- })
192
- await (broker as any).start({ type: 'rtsp', url: STREAMS[0]!.url, videoCodec: 'h264' })
193
-
194
- let encodedCount = 0
195
- let encodedBytes = 0
196
- let keyframes = 0
197
- let decodedA = 0
198
- let decodedB = 0
199
-
200
- // Wait for stream to start
201
- await new Promise<void>((resolve, reject) => {
202
- const timeout = setTimeout(() => reject(new Error('No frame')), 10000)
203
- let unsub: (() => Promise<void>) | undefined
204
- subscribeDecodedFrames(broker, 10, () => {
205
- clearTimeout(timeout)
206
- void unsub?.()
207
- resolve()
208
- })
209
- .then((fn) => {
210
- unsub = fn
211
- })
212
- .catch(reject)
213
- })
214
-
215
- // Subscribe all 3
216
- const unsubE = broker.onEncodedData((pkt: EncodedPacket) => {
217
- encodedCount++
218
- encodedBytes += pkt.data.length
219
- if (pkt.keyframe) keyframes++
220
- })
221
- // Phase 5 / D9: the broker no longer throttles per-subscriber fps — the
222
- // shm frame plane keeps one decoder session per format (here both
223
- // subscribers share the `rgb` ring) and latest-wins ring reads drop
224
- // frames implicitly. Both subscribers therefore see the same cadence;
225
- // the old per-subscriber fps-isolation guarantee is gone.
226
- const unsubD1 = await subscribeDecodedFrames(broker, 5, () => {
227
- decodedA++
228
- })
229
- const unsubD2 = await subscribeDecodedFrames(broker, 5, () => {
230
- decodedB++
231
- })
232
-
233
- await wait(3000)
234
-
235
- // Capture stats BEFORE unsubscribing
236
- const stats = broker.getStats()
237
-
238
- unsubE()
239
- await unsubD1()
240
- await unsubD2()
241
-
242
- console.log(
243
- ` Encoded packets: ${encodedCount} (${(encodedBytes / 1024).toFixed(0)}KB, ${keyframes} keyframes)`,
244
- )
245
- console.log(` Decoded subscriber A: ${decodedA} frames`)
246
- console.log(` Decoded subscriber B: ${decodedB} frames`)
247
- console.log(
248
- ` Broker stats: inputFps=${stats.inputFps}, decodeFps=${stats.decodeFps}, uptime=${stats.uptimeMs}ms`,
249
- )
250
-
251
- // Assertions
252
- expect(encodedCount).toBeGreaterThan(0)
253
- expect(decodedA).toBeGreaterThan(0)
254
- expect(decodedB).toBeGreaterThan(0)
255
- expect(stats.decodeFps).toBeGreaterThan(0) // plane's aggregate decode rate
256
- expect(keyframes).toBeGreaterThan(0) // at least 1 keyframe in 3s
257
-
258
- await broker.stop()
259
- await manager.destroyBroker('fanout')
260
- }, 30000)
261
-
262
- it('measures concurrent broker isolation (2 cameras)', async () => {
263
- console.log('\n--- Concurrent Brokers (2 cameras, 3s @ 3fps) ---')
264
-
265
- const results: Record<string, { frames: number; sizes: number[] }> = {}
266
- const brokerIds: string[] = []
267
- const collectUnsubs: Array<() => Promise<void>> = []
268
-
269
- for (const s of STREAMS) {
270
- const brokerId = `concurrent-${s.id}`
271
- brokerIds.push(brokerId)
272
- const broker = await manager.createBroker(brokerId, {
273
- type: 'rtsp',
274
- url: s.url,
275
- videoCodec: s.codec,
276
- })
277
- await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: s.codec })
278
- results[s.id] = { frames: 0, sizes: [] }
279
-
280
- // Wait for first frame
281
- await new Promise<void>((resolve, reject) => {
282
- const timeout = setTimeout(() => reject(new Error(`No frame from ${s.id}`)), 10000)
283
- let unsub: (() => Promise<void>) | undefined
284
- subscribeDecodedFrames(broker, 5, () => {
285
- clearTimeout(timeout)
286
- void unsub?.()
287
- resolve()
288
- })
289
- .then((fn) => {
290
- unsub = fn
291
- })
292
- .catch(reject)
293
- })
294
-
295
- collectUnsubs.push(
296
- await subscribeDecodedFrames(broker, 3, (handle: FrameHandle) => {
297
- results[s.id]!.frames++
298
- results[s.id]!.sizes.push(handle.byteLength)
299
- }),
300
- )
301
- }
302
-
303
- await wait(3000)
304
- for (const unsub of collectUnsubs) await unsub()
305
-
306
- for (const s of STREAMS) {
307
- const r = results[s.id]!
308
- const avgKB =
309
- r.sizes.length > 0
310
- ? +(r.sizes.reduce((a, b) => a + b, 0) / r.sizes.length / 1024).toFixed(1)
311
- : 0
312
- console.log(` ${s.id}: ${r.frames} frames, avg=${avgKB}KB`)
313
- }
314
-
315
- const allProducing = Object.values(results).every((r) => r.frames > 0)
316
- console.log(` All producing: ${allProducing ? 'YES' : 'FAIL'}`)
317
-
318
- expect(allProducing).toBe(true)
319
-
320
- for (const id of brokerIds) {
321
- const b = manager.getBroker(id)
322
- if (b) {
323
- await b.stop()
324
- }
325
- await manager.destroyBroker(id)
326
- }
327
- }, 30000)
328
-
329
- it('stress test: 3 cameras x 3 subscribers each at different FPS', async () => {
330
- console.log('\n--- Stress: 3 cameras x 3 subs (5s) ---')
331
-
332
- const cameras = [
333
- { id: 'ingresso_main', url: `rtsp://${FRIGATE}:8554/ingresso_main` },
334
- { id: 'ingresso_sub', url: `rtsp://${FRIGATE}:8554/ingresso_sub` },
335
- { id: 'salone_main', url: `rtsp://${FRIGATE}:8554/salone_main` },
336
- ]
337
-
338
- // Phase 5 / D9: the broker no longer throttles per-subscriber fps. This
339
- // stays a load test — 3 concurrent frame-handle subscribers per camera —
340
- // but the obsolete per-subscriber fps-isolation assertion is dropped.
341
- const subscriberCount = 3
342
- const results: Record<string, Record<number, { frames: number; sizes: number[] }>> = {}
343
- const unsubs: Array<() => Promise<void>> = []
344
- const brokerIds: string[] = []
345
-
346
- // Start all 3 cameras
347
- for (const cam of cameras) {
348
- const brokerId = `stress-${cam.id}`
349
- brokerIds.push(brokerId)
350
- results[cam.id] = {}
351
-
352
- const broker = await manager.createBroker(brokerId, {
353
- type: 'rtsp',
354
- url: cam.url,
355
- videoCodec: 'h264',
356
- })
357
- await (broker as any).start({ type: 'rtsp', url: cam.url, videoCodec: 'h264' })
358
-
359
- // Wait for first frame (shared decoder warmup)
360
- await new Promise<void>((resolve, reject) => {
361
- const timeout = setTimeout(() => reject(new Error(`No frame from ${cam.id} in 10s`)), 10000)
362
- let unsub: (() => Promise<void>) | undefined
363
- subscribeDecodedFrames(broker, 10, () => {
364
- clearTimeout(timeout)
365
- void unsub?.()
366
- resolve()
367
- })
368
- .then((fn) => {
369
- unsub = fn
370
- })
371
- .catch(reject)
372
- })
373
-
374
- // Subscribe N concurrent frame-handle readers
375
- for (let sub = 0; sub < subscriberCount; sub++) {
376
- results[cam.id]![sub] = { frames: 0, sizes: [] }
377
- unsubs.push(
378
- await subscribeDecodedFrames(broker, 5, (handle: FrameHandle) => {
379
- results[cam.id]![sub]!.frames++
380
- results[cam.id]![sub]!.sizes.push(handle.byteLength)
381
- }),
382
- )
383
- }
384
- }
385
-
386
- // Let it run for 5 seconds
387
- await wait(5000)
388
-
389
- // Unsubscribe all
390
- for (const unsub of unsubs) await unsub()
391
-
392
- // Print results
393
- console.log(' Camera | sub0 | sub1 | sub2 | Avg KB')
394
- console.log(' -------------------|-------|-------|-------|-------')
395
- let allOk = true
396
- for (const cam of cameras) {
397
- const r = results[cam.id]!
398
- const allSizes = [...r[0]!.sizes, ...r[1]!.sizes, ...r[2]!.sizes]
399
- const avgKB =
400
- allSizes.length > 0
401
- ? +(allSizes.reduce((a, b) => a + b, 0) / allSizes.length / 1024).toFixed(0)
402
- : 0
403
- const line = ` ${cam.id.padEnd(19)}| ${String(r[0]!.frames).padStart(5)} | ${String(r[1]!.frames).padStart(5)} | ${String(r[2]!.frames).padStart(5)} | ${avgKB}`
404
- console.log(line)
405
-
406
- // Every concurrent subscriber must produce frames.
407
- for (let sub = 0; sub < subscriberCount; sub++) {
408
- if (r[sub]!.frames === 0) allOk = false
409
- }
410
- }
411
-
412
- // Verify totals
413
- const totalFrames = Object.values(results).reduce(
414
- (sum, cam) => sum + Object.values(cam).reduce((s, sub) => s + sub.frames, 0),
415
- 0,
416
- )
417
- const totalSubs = cameras.length * subscriberCount // 9
418
- console.log(`\n Total: ${totalFrames} frames across ${totalSubs} subscribers`)
419
- console.log(` All subs producing: ${allOk ? 'YES' : 'FAIL'}`)
420
-
421
- // Assertions — every concurrent subscriber on every camera gets frames.
422
- for (const cam of cameras) {
423
- for (let sub = 0; sub < subscriberCount; sub++) {
424
- expect(
425
- results[cam.id]![sub]!.frames,
426
- `${cam.id} sub${sub} should produce frames`,
427
- ).toBeGreaterThan(0)
428
- }
429
- }
430
-
431
- // Cleanup
432
- for (const id of brokerIds) {
433
- const b = manager.getBroker(id)
434
- if (b) await b.stop()
435
- await manager.destroyBroker(id)
436
- }
437
- }, 60000)
438
-
439
- it('benchmarks resource usage per stream (CPU, memory, bandwidth)', async () => {
440
- console.log('\n--- Resource Benchmark (3 cameras, 5s each) ---')
441
-
442
- const cameras = [
443
- {
444
- id: 'ingresso_main',
445
- url: `rtsp://${FRIGATE}:8554/ingresso_main`,
446
- label: 'ingresso main (HD)',
447
- },
448
- {
449
- id: 'ingresso_sub',
450
- url: `rtsp://${FRIGATE}:8554/ingresso_sub`,
451
- label: 'ingresso sub (SD)',
452
- },
453
- { id: 'salone_main', url: `rtsp://${FRIGATE}:8554/salone_main`, label: 'salone main (HD)' },
454
- ]
455
-
456
- console.log(
457
- ' Stream | Enc BW | Dec FPS | Dec KB/f | CPU % | RSS MB | Heap MB',
458
- )
459
- console.log(
460
- ' --------------------|-----------|---------|----------|--------|--------|--------',
461
- )
462
-
463
- for (const cam of cameras) {
464
- const brokerId = `bench-${cam.id}`
465
- const broker = await manager.createBroker(brokerId, {
466
- type: 'rtsp',
467
- url: cam.url,
468
- videoCodec: 'h264',
469
- })
470
- await (broker as any).start({ type: 'rtsp', url: cam.url, videoCodec: 'h264' })
471
-
472
- // Wait for first frame
473
- await new Promise<void>((resolve, reject) => {
474
- const timeout = setTimeout(() => reject(new Error(`No frame from ${cam.id}`)), 10000)
475
- let unsub: (() => Promise<void>) | undefined
476
- subscribeDecodedFrames(broker, 10, () => {
477
- clearTimeout(timeout)
478
- void unsub?.()
479
- resolve()
480
- })
481
- .then((fn) => {
482
- unsub = fn
483
- })
484
- .catch(reject)
485
- })
486
-
487
- // Measure baseline
488
- const memBefore = process.memoryUsage()
489
- const cpuBefore = process.cpuUsage()
490
- let encodedBytes = 0
491
- let encodedPackets = 0
492
- const decodedSizes: number[] = []
493
-
494
- const unsubE = broker.onEncodedData((pkt: EncodedPacket) => {
495
- encodedBytes += pkt.data.length
496
- encodedPackets++
497
- })
498
- const unsubD = await subscribeDecodedFrames(broker, 5, (handle: FrameHandle) => {
499
- decodedSizes.push(handle.byteLength)
500
- })
501
-
502
- // Run for 5 seconds
503
- await wait(5000)
504
-
505
- const cpuAfter = process.cpuUsage(cpuBefore)
506
- const memAfter = process.memoryUsage()
507
-
508
- unsubE()
509
- await unsubD()
510
-
511
- // Calculate metrics
512
- const durationS = 5
513
- const encBwKBs = (encodedBytes / 1024 / durationS).toFixed(0)
514
- const decFps = (decodedSizes.length / durationS).toFixed(1)
515
- const avgDecKB =
516
- decodedSizes.length > 0
517
- ? (decodedSizes.reduce((a, b) => a + b, 0) / decodedSizes.length / 1024).toFixed(0)
518
- : '0'
519
- // CPU: user + system microseconds → percentage of 5s wall time
520
- const cpuTotalUs = cpuAfter.user + cpuAfter.system
521
- const cpuPct = ((cpuTotalUs / (durationS * 1_000_000)) * 100).toFixed(1)
522
- const rssDeltaMB = ((memAfter.rss - memBefore.rss) / 1024 / 1024).toFixed(1)
523
- const heapMB = (memAfter.heapUsed / 1024 / 1024).toFixed(1)
524
-
525
- const label = cam.label.padEnd(20)
526
- console.log(
527
- ` ${label}| ${encBwKBs.padStart(5)} KB/s | ${decFps.padStart(7)} | ${avgDecKB.padStart(6)} KB | ${cpuPct.padStart(5)}% | ${rssDeltaMB.padStart(6)} | ${heapMB.padStart(7)}`,
528
- )
529
-
530
- await broker.stop()
531
- await manager.destroyBroker(brokerId)
532
-
533
- // Small pause between cameras for GC
534
- await wait(500)
535
- }
536
-
537
- // Now test all 3 simultaneously
538
- console.log(
539
- ' --------------------|-----------|---------|----------|--------|--------|--------',
540
- )
541
- console.log(' All 3 concurrent:')
542
-
543
- const memBaseline = process.memoryUsage()
544
- const cpuBaseline = process.cpuUsage()
545
- const allBrokers: string[] = []
546
- const allUnsubs: Array<() => void | Promise<void>> = []
547
- let totalEncBytes = 0
548
- let totalDecFrames = 0
549
-
550
- for (const cam of cameras) {
551
- const brokerId = `bench-all-${cam.id}`
552
- allBrokers.push(brokerId)
553
- const broker = await manager.createBroker(brokerId, {
554
- type: 'rtsp',
555
- url: cam.url,
556
- videoCodec: 'h264',
557
- })
558
- await (broker as any).start({ type: 'rtsp', url: cam.url, videoCodec: 'h264' })
559
-
560
- await new Promise<void>((resolve, reject) => {
561
- const timeout = setTimeout(() => reject(new Error(`No frame from ${cam.id}`)), 10000)
562
- let unsub: (() => Promise<void>) | undefined
563
- subscribeDecodedFrames(broker, 10, () => {
564
- clearTimeout(timeout)
565
- void unsub?.()
566
- resolve()
567
- })
568
- .then((fn) => {
569
- unsub = fn
570
- })
571
- .catch(reject)
572
- })
573
-
574
- allUnsubs.push(
575
- broker.onEncodedData((pkt: EncodedPacket) => {
576
- totalEncBytes += pkt.data.length
577
- }),
578
- )
579
- allUnsubs.push(
580
- await subscribeDecodedFrames(broker, 5, () => {
581
- totalDecFrames++
582
- }),
583
- )
584
- }
585
-
586
- await wait(5000)
587
-
588
- const cpuAll = process.cpuUsage(cpuBaseline)
589
- const memAll = process.memoryUsage()
590
-
591
- for (const unsub of allUnsubs) await unsub()
592
-
593
- const allCpuPct = (((cpuAll.user + cpuAll.system) / (5 * 1_000_000)) * 100).toFixed(1)
594
- const allRssMB = ((memAll.rss - memBaseline.rss) / 1024 / 1024).toFixed(1)
595
- const allHeapMB = (memAll.heapUsed / 1024 / 1024).toFixed(1)
596
- const allEncKBs = (totalEncBytes / 1024 / 5).toFixed(0)
597
-
598
- console.log(` Encoded BW: ${allEncKBs} KB/s total`)
599
- console.log(
600
- ` Decoded: ${totalDecFrames} frames total (${(totalDecFrames / 5).toFixed(1)} fps combined)`,
601
- )
602
- console.log(` CPU: ${allCpuPct}%`)
603
- console.log(` RSS delta: ${allRssMB} MB`)
604
- console.log(` Heap: ${allHeapMB} MB`)
605
-
606
- for (const id of allBrokers) {
607
- const b = manager.getBroker(id)
608
- if (b) await b.stop()
609
- await manager.destroyBroker(id)
610
- }
611
-
612
- // Basic sanity — we should be able to handle 3 streams
613
- expect(totalDecFrames).toBeGreaterThan(10)
614
- }, 90000)
615
- })