@camstack/server 0.1.7 → 0.2.0

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 (135) hide show
  1. package/package.json +11 -9
  2. package/src/__tests__/addon-install-e2e.test.ts +0 -1
  3. package/src/__tests__/addon-pages-e2e.test.ts +40 -18
  4. package/src/__tests__/addon-settings-router.spec.ts +6 -1
  5. package/src/__tests__/addon-upload.spec.ts +91 -29
  6. package/src/__tests__/agent-registry.spec.ts +26 -9
  7. package/src/__tests__/agent-status-page.spec.ts +1 -3
  8. package/src/__tests__/auth-session-cookie.test.ts +28 -1
  9. package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
  10. package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
  11. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +206 -0
  12. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
  13. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
  14. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +292 -0
  15. package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
  16. package/src/__tests__/cap-route-adapter.spec.ts +28 -15
  17. package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
  18. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
  19. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +177 -0
  20. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
  21. package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
  22. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +137 -0
  23. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
  24. package/src/__tests__/cap-routers/harness.ts +11 -7
  25. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
  26. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
  27. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
  28. package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
  29. package/src/__tests__/capability-e2e.test.ts +9 -11
  30. package/src/__tests__/cli-e2e.test.ts +80 -59
  31. package/src/__tests__/core-cap-bridge.spec.ts +3 -1
  32. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
  33. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
  34. package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
  35. package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
  36. package/src/__tests__/framework-allowlist.spec.ts +5 -4
  37. package/src/__tests__/https-e2e.test.ts +12 -6
  38. package/src/__tests__/lifecycle-e2e.test.ts +60 -11
  39. package/src/__tests__/live-events-subscription.spec.ts +17 -18
  40. package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
  41. package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
  42. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +265 -5
  43. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
  44. package/src/__tests__/native-cap-route.spec.ts +42 -19
  45. package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
  46. package/src/__tests__/singleton-contention.test.ts +23 -11
  47. package/src/__tests__/streaming-diagnostic.test.ts +156 -53
  48. package/src/__tests__/streaming-scale.test.ts +69 -35
  49. package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
  50. package/src/agent-status-page.ts +4 -3
  51. package/src/api/__tests__/addons-custom.spec.ts +22 -8
  52. package/src/api/__tests__/capabilities.router.test.ts +18 -9
  53. package/src/api/addon-upload.ts +46 -15
  54. package/src/api/addons-custom.router.ts +7 -6
  55. package/src/api/auth-whoami.ts +3 -1
  56. package/src/api/bridge-addons.router.ts +3 -1
  57. package/src/api/capabilities.router.ts +117 -78
  58. package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
  59. package/src/api/core/__tests__/integration-markers.spec.ts +10 -0
  60. package/src/api/core/addon-settings.router.ts +4 -1
  61. package/src/api/core/agents.router.ts +52 -53
  62. package/src/api/core/auth.router.ts +55 -36
  63. package/src/api/core/bulk-update-coordinator.ts +25 -22
  64. package/src/api/core/cap-providers.ts +459 -166
  65. package/src/api/core/capabilities.router.ts +30 -23
  66. package/src/api/core/hwaccel.router.ts +37 -10
  67. package/src/api/core/live-events.router.ts +16 -9
  68. package/src/api/core/logs.router.ts +58 -25
  69. package/src/api/core/notifications.router.ts +2 -1
  70. package/src/api/core/repl.router.ts +1 -3
  71. package/src/api/core/settings-backend.router.ts +68 -70
  72. package/src/api/core/system-events.router.ts +41 -32
  73. package/src/api/health/health.routes.ts +7 -13
  74. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  75. package/src/api/oauth2/consent-page.ts +4 -3
  76. package/src/api/oauth2/oauth2-routes.ts +41 -12
  77. package/src/api/trpc/__tests__/client-ip.spec.ts +27 -1
  78. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  79. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  80. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +136 -0
  81. package/src/api/trpc/cap-mount-helpers.ts +64 -44
  82. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  83. package/src/api/trpc/client-ip.ts +17 -0
  84. package/src/api/trpc/core-cap-bridge.ts +3 -1
  85. package/src/api/trpc/generated-cap-mounts.ts +801 -286
  86. package/src/api/trpc/generated-cap-routers.ts +5723 -719
  87. package/src/api/trpc/scope-access.ts +7 -7
  88. package/src/api/trpc/trpc.context.ts +7 -4
  89. package/src/api/trpc/trpc.middleware.ts +4 -2
  90. package/src/api/trpc/trpc.router.ts +117 -48
  91. package/src/auth/session-cookie.ts +10 -0
  92. package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
  93. package/src/boot/boot-config.ts +103 -122
  94. package/src/boot/integration-id-backfill.ts +109 -0
  95. package/src/boot/post-boot.service.ts +5 -3
  96. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  97. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +62 -0
  98. package/src/core/addon/addon-call-gateway.ts +20 -6
  99. package/src/core/addon/addon-package.service.ts +183 -89
  100. package/src/core/addon/addon-registry.service.ts +1212 -1267
  101. package/src/core/addon/addon-row-manifest.ts +29 -0
  102. package/src/core/addon/addon-search.service.ts +2 -1
  103. package/src/core/addon/addon-settings-provider.ts +27 -7
  104. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  105. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  106. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  107. package/src/core/agent/agent-registry.service.ts +60 -38
  108. package/src/core/auth/auth.service.spec.ts +6 -8
  109. package/src/core/config/config.service.spec.ts +1 -1
  110. package/src/core/events/event-bus.service.spec.ts +44 -21
  111. package/src/core/events/event-bus.service.ts +5 -1
  112. package/src/core/feature/feature.service.spec.ts +4 -1
  113. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  114. package/src/core/logging/logging.service.spec.ts +61 -21
  115. package/src/core/logging/logging.service.ts +19 -5
  116. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  117. package/src/core/moleculer/cap-call-fn.ts +5 -1
  118. package/src/core/moleculer/cap-route-authority.ts +18 -6
  119. package/src/core/moleculer/moleculer.service.ts +145 -29
  120. package/src/core/network/network-quality.service.spec.ts +7 -1
  121. package/src/core/notification/notification-wrapper.service.ts +1 -3
  122. package/src/core/notification/toast-wrapper.service.ts +1 -5
  123. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  124. package/src/core/repl/repl-engine.service.ts +11 -12
  125. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  126. package/src/core/streaming/stream-probe.service.ts +22 -13
  127. package/src/core/topology/topology-emitter.service.ts +5 -1
  128. package/src/launcher.ts +14 -9
  129. package/src/main.ts +658 -495
  130. package/src/manual-boot.ts +133 -154
  131. package/tsconfig.json +20 -8
  132. package/src/core/storage/settings-store.spec.ts +0 -213
  133. package/src/core/storage/settings-store.ts +0 -2
  134. package/src/core/storage/sql-schema.spec.ts +0 -140
  135. package/src/core/storage/sql-schema.ts +0 -3
@@ -86,12 +86,12 @@ class TestAddonHarness {
86
86
  } as any
87
87
  const result = await entry.addon.initialize(context)
88
88
  if (result) {
89
- const regs = Array.isArray(result) ? result : (result as any).providers ?? []
89
+ const regs = Array.isArray(result) ? result : ((result as any).providers ?? [])
90
90
  for (const reg of regs) {
91
91
  const capName: string =
92
92
  typeof reg.capability === 'string'
93
93
  ? reg.capability
94
- : (reg.capability as any)?.name ?? String(reg.capability)
94
+ : ((reg.capability as any)?.name ?? String(reg.capability))
95
95
  self.registry.registerProvider(capName, id, reg.provider)
96
96
  }
97
97
  }
@@ -154,8 +154,12 @@ describe('Singleton contention E2E: two addons on the same singleton cap', () =>
154
154
  expect(info.activeProvider).toBe('mock-analysis-a')
155
155
 
156
156
  // Both providers individually addressable.
157
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(analysisA.provider)
158
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(analysisB.provider)
157
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
158
+ analysisA.provider,
159
+ )
160
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
161
+ analysisB.provider,
162
+ )
159
163
  })
160
164
 
161
165
  it('honours a configReader preference for the SECOND addon over first-registered', async () => {
@@ -191,7 +195,11 @@ describe('Singleton contention E2E: waitForProvider before registration', () =>
191
195
  harness.declareCapabilities(analysisA)
192
196
 
193
197
  // Consumer begins waiting BEFORE the addon initializes — no provider yet.
194
- const waitPromise = harness.registry.waitForProvider('object-detector', 'mock-analysis-a', 5_000)
198
+ const waitPromise = harness.registry.waitForProvider(
199
+ 'object-detector',
200
+ 'mock-analysis-a',
201
+ 5_000,
202
+ )
195
203
 
196
204
  // Addon initializes shortly after → registerProvider fulfils the waiter.
197
205
  setTimeout(() => {
@@ -263,7 +271,9 @@ describe('Singleton contention E2E: active provider removed', () => {
263
271
  const info = harness.registry.listCapabilities().find((c) => c.name === 'object-detector')!
264
272
  expect(info.providers).toEqual(['mock-analysis-b'])
265
273
  expect(info.activeProvider).toBe('mock-analysis-b')
266
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(analysisB.provider)
274
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-b')).toBe(
275
+ analysisB.provider,
276
+ )
267
277
  })
268
278
 
269
279
  it('removing a NON-active provider keeps the active one untouched', async () => {
@@ -369,19 +379,19 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
369
379
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
370
380
 
371
381
  // Switch to B.
372
- await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b', true)
382
+ await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b')
373
383
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisB.provider)
374
384
  expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-b')
375
385
 
376
386
  // Switch back to A.
377
- await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-a', true)
387
+ await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-a')
378
388
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
379
389
  expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-a')
380
390
  })
381
391
 
382
392
  it('setActiveSingleton throws when switching to an addon that never registered', async () => {
383
393
  await expect(
384
- harness.registry.setActiveSingleton('object-detector', 'mock-analysis-c', true),
394
+ harness.registry.setActiveSingleton('object-detector', 'mock-analysis-c'),
385
395
  ).rejects.toThrow(/[Nn]o provider/)
386
396
  // Active pointer unchanged after the failed switch.
387
397
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
@@ -389,7 +399,7 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
389
399
 
390
400
  it('unregistering the explicitly-selected active provider promotes the remaining one', async () => {
391
401
  // Operator explicitly selected B.
392
- await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b', true)
402
+ await harness.registry.setActiveSingleton('object-detector', 'mock-analysis-b')
393
403
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisB.provider)
394
404
 
395
405
  // B is removed. `unregisterProvider` promotes the remaining A rather
@@ -399,7 +409,9 @@ describe('Singleton contention E2E: setActiveSingleton switching', () => {
399
409
  await harness.shutdownAddon('mock-analysis-b')
400
410
  expect(harness.registry.getSingleton('object-detector')).toBe(analysisA.provider)
401
411
  expect(harness.registry.getSingletonAddonId('object-detector')).toBe('mock-analysis-a')
402
- expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(analysisA.provider)
412
+ expect(harness.registry.getProviderByAddon('object-detector', 'mock-analysis-a')).toBe(
413
+ analysisA.provider,
414
+ )
403
415
  })
404
416
  })
405
417
 
@@ -31,15 +31,21 @@ const mockLoggingService = {
31
31
  }
32
32
 
33
33
  function wait(ms: number) {
34
- return new Promise(r => setTimeout(r, ms))
34
+ return new Promise((r) => setTimeout(r, ms))
35
35
  }
36
36
 
37
37
  function waitForFrames(count: number, frames: unknown[], timeoutMs: number): Promise<void> {
38
38
  return new Promise((resolve, reject) => {
39
39
  const start = Date.now()
40
40
  const iv = setInterval(() => {
41
- if (frames.length >= count) { clearInterval(iv); resolve() }
42
- if (Date.now() - start > timeoutMs) { clearInterval(iv); reject(new Error(`Timeout: got ${frames.length}/${count} frames`)) }
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
+ }
43
49
  }, 50)
44
50
  })
45
51
  }
@@ -81,7 +87,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
81
87
 
82
88
  for (const s of STREAMS) {
83
89
  const broker = await manager.createBroker(`latency-${s.id}`, {
84
- type: 'rtsp', url: s.url, videoCodec: s.codec,
90
+ type: 'rtsp',
91
+ url: s.url,
92
+ videoCodec: s.codec,
85
93
  })
86
94
  const startMs = Date.now()
87
95
  await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: s.codec })
@@ -92,8 +100,15 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
92
100
  subscribeDecodedFrames(broker, 10, (handle: FrameHandle) => {
93
101
  clearTimeout(timeout)
94
102
  void unsub?.()
95
- resolve({ latencyMs: Date.now() - startMs, sizeKB: +(handle.byteLength / 1024).toFixed(1) })
96
- }).then((fn) => { unsub = fn }).catch(reject)
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)
97
112
  })
98
113
 
99
114
  console.log(` ${s.id}: ${result.latencyMs}ms, frame=${result.sizeKB}KB`)
@@ -109,7 +124,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
109
124
  console.log('\n--- Frame Quality (5s @ 5fps) ---')
110
125
 
111
126
  const broker = await manager.createBroker('quality', {
112
- type: 'rtsp', url: STREAMS[0]!.url, videoCodec: 'h264',
127
+ type: 'rtsp',
128
+ url: STREAMS[0]!.url,
129
+ videoCodec: 'h264',
113
130
  })
114
131
  await (broker as any).start({ type: 'rtsp', url: STREAMS[0]!.url, videoCodec: 'h264' })
115
132
 
@@ -123,7 +140,11 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
123
140
  clearTimeout(timeout)
124
141
  void unsub?.()
125
142
  resolve()
126
- }).then((fn) => { unsub = fn }).catch(reject)
143
+ })
144
+ .then((fn) => {
145
+ unsub = fn
146
+ })
147
+ .catch(reject)
127
148
  })
128
149
 
129
150
  // Now collect for 5 seconds
@@ -134,7 +155,7 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
134
155
  await wait(5000)
135
156
  await unsub()
136
157
 
137
- const sizes = frames.map(f => f.sizeKB)
158
+ const sizes = frames.map((f) => f.sizeKB)
138
159
  const avgSize = +(sizes.reduce((a, b) => a + b, 0) / sizes.length).toFixed(1)
139
160
  const minSize = Math.min(...sizes)
140
161
  const maxSize = Math.max(...sizes)
@@ -150,7 +171,7 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
150
171
  console.log(` Frames: ${frames.length} (expected ~25 @ 5fps)`)
151
172
  console.log(` JPEG size: avg=${avgSize}KB, min=${minSize}KB, max=${maxSize}KB`)
152
173
  console.log(` Frame gap: avg=${avgGap}ms, max=${maxGap}ms (expected ~200ms @ 5fps)`)
153
- console.log(` All valid: ${frames.every(f => f.sizeKB > 0.5) ? 'YES' : 'NO'}`)
174
+ console.log(` All valid: ${frames.every((f) => f.sizeKB > 0.5) ? 'YES' : 'NO'}`)
154
175
 
155
176
  expect(frames.length).toBeGreaterThan(10)
156
177
  expect(avgSize).toBeGreaterThan(1) // > 1KB avg = real JPEG content
@@ -164,7 +185,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
164
185
  console.log('\n--- Fanout (3s, encoded + 2 decoded) ---')
165
186
 
166
187
  const broker = await manager.createBroker('fanout', {
167
- type: 'rtsp', url: STREAMS[0]!.url, videoCodec: 'h264',
188
+ type: 'rtsp',
189
+ url: STREAMS[0]!.url,
190
+ videoCodec: 'h264',
168
191
  })
169
192
  await (broker as any).start({ type: 'rtsp', url: STREAMS[0]!.url, videoCodec: 'h264' })
170
193
 
@@ -182,7 +205,11 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
182
205
  clearTimeout(timeout)
183
206
  void unsub?.()
184
207
  resolve()
185
- }).then((fn) => { unsub = fn }).catch(reject)
208
+ })
209
+ .then((fn) => {
210
+ unsub = fn
211
+ })
212
+ .catch(reject)
186
213
  })
187
214
 
188
215
  // Subscribe all 3
@@ -196,8 +223,12 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
196
223
  // subscribers share the `rgb` ring) and latest-wins ring reads drop
197
224
  // frames implicitly. Both subscribers therefore see the same cadence;
198
225
  // the old per-subscriber fps-isolation guarantee is gone.
199
- const unsubD1 = await subscribeDecodedFrames(broker, 5, () => { decodedA++ })
200
- const unsubD2 = await subscribeDecodedFrames(broker, 5, () => { decodedB++ })
226
+ const unsubD1 = await subscribeDecodedFrames(broker, 5, () => {
227
+ decodedA++
228
+ })
229
+ const unsubD2 = await subscribeDecodedFrames(broker, 5, () => {
230
+ decodedB++
231
+ })
201
232
 
202
233
  await wait(3000)
203
234
 
@@ -208,10 +239,14 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
208
239
  await unsubD1()
209
240
  await unsubD2()
210
241
 
211
- console.log(` Encoded packets: ${encodedCount} (${(encodedBytes / 1024).toFixed(0)}KB, ${keyframes} keyframes)`)
242
+ console.log(
243
+ ` Encoded packets: ${encodedCount} (${(encodedBytes / 1024).toFixed(0)}KB, ${keyframes} keyframes)`,
244
+ )
212
245
  console.log(` Decoded subscriber A: ${decodedA} frames`)
213
246
  console.log(` Decoded subscriber B: ${decodedB} frames`)
214
- console.log(` Broker stats: inputFps=${stats.inputFps}, decodeFps=${stats.decodeFps}, uptime=${stats.uptimeMs}ms`)
247
+ console.log(
248
+ ` Broker stats: inputFps=${stats.inputFps}, decodeFps=${stats.decodeFps}, uptime=${stats.uptimeMs}ms`,
249
+ )
215
250
 
216
251
  // Assertions
217
252
  expect(encodedCount).toBeGreaterThan(0)
@@ -235,7 +270,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
235
270
  const brokerId = `concurrent-${s.id}`
236
271
  brokerIds.push(brokerId)
237
272
  const broker = await manager.createBroker(brokerId, {
238
- type: 'rtsp', url: s.url, videoCodec: s.codec,
273
+ type: 'rtsp',
274
+ url: s.url,
275
+ videoCodec: s.codec,
239
276
  })
240
277
  await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: s.codec })
241
278
  results[s.id] = { frames: 0, sizes: [] }
@@ -248,13 +285,19 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
248
285
  clearTimeout(timeout)
249
286
  void unsub?.()
250
287
  resolve()
251
- }).then((fn) => { unsub = fn }).catch(reject)
288
+ })
289
+ .then((fn) => {
290
+ unsub = fn
291
+ })
292
+ .catch(reject)
252
293
  })
253
294
 
254
- collectUnsubs.push(await subscribeDecodedFrames(broker, 3, (handle: FrameHandle) => {
255
- results[s.id]!.frames++
256
- results[s.id]!.sizes.push(handle.byteLength)
257
- }))
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
+ )
258
301
  }
259
302
 
260
303
  await wait(3000)
@@ -262,18 +305,23 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
262
305
 
263
306
  for (const s of STREAMS) {
264
307
  const r = results[s.id]!
265
- const avgKB = r.sizes.length > 0 ? +(r.sizes.reduce((a, b) => a + b, 0) / r.sizes.length / 1024).toFixed(1) : 0
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
266
312
  console.log(` ${s.id}: ${r.frames} frames, avg=${avgKB}KB`)
267
313
  }
268
314
 
269
- const allProducing = Object.values(results).every(r => r.frames > 0)
315
+ const allProducing = Object.values(results).every((r) => r.frames > 0)
270
316
  console.log(` All producing: ${allProducing ? 'YES' : 'FAIL'}`)
271
317
 
272
318
  expect(allProducing).toBe(true)
273
319
 
274
320
  for (const id of brokerIds) {
275
321
  const b = manager.getBroker(id)
276
- if (b) { await b.stop() }
322
+ if (b) {
323
+ await b.stop()
324
+ }
277
325
  await manager.destroyBroker(id)
278
326
  }
279
327
  }, 30000)
@@ -302,7 +350,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
302
350
  results[cam.id] = {}
303
351
 
304
352
  const broker = await manager.createBroker(brokerId, {
305
- type: 'rtsp', url: cam.url, videoCodec: 'h264',
353
+ type: 'rtsp',
354
+ url: cam.url,
355
+ videoCodec: 'h264',
306
356
  })
307
357
  await (broker as any).start({ type: 'rtsp', url: cam.url, videoCodec: 'h264' })
308
358
 
@@ -314,16 +364,22 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
314
364
  clearTimeout(timeout)
315
365
  void unsub?.()
316
366
  resolve()
317
- }).then((fn) => { unsub = fn }).catch(reject)
367
+ })
368
+ .then((fn) => {
369
+ unsub = fn
370
+ })
371
+ .catch(reject)
318
372
  })
319
373
 
320
374
  // Subscribe N concurrent frame-handle readers
321
375
  for (let sub = 0; sub < subscriberCount; sub++) {
322
376
  results[cam.id]![sub] = { frames: 0, sizes: [] }
323
- unsubs.push(await subscribeDecodedFrames(broker, 5, (handle: FrameHandle) => {
324
- results[cam.id]![sub]!.frames++
325
- results[cam.id]![sub]!.sizes.push(handle.byteLength)
326
- }))
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
+ )
327
383
  }
328
384
  }
329
385
 
@@ -340,7 +396,10 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
340
396
  for (const cam of cameras) {
341
397
  const r = results[cam.id]!
342
398
  const allSizes = [...r[0]!.sizes, ...r[1]!.sizes, ...r[2]!.sizes]
343
- const avgKB = allSizes.length > 0 ? +(allSizes.reduce((a, b) => a + b, 0) / allSizes.length / 1024).toFixed(0) : 0
399
+ const avgKB =
400
+ allSizes.length > 0
401
+ ? +(allSizes.reduce((a, b) => a + b, 0) / allSizes.length / 1024).toFixed(0)
402
+ : 0
344
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}`
345
404
  console.log(line)
346
405
 
@@ -351,8 +410,10 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
351
410
  }
352
411
 
353
412
  // Verify totals
354
- const totalFrames = Object.values(results).reduce((sum, cam) =>
355
- sum + Object.values(cam).reduce((s, sub) => s + sub.frames, 0), 0)
413
+ const totalFrames = Object.values(results).reduce(
414
+ (sum, cam) => sum + Object.values(cam).reduce((s, sub) => s + sub.frames, 0),
415
+ 0,
416
+ )
356
417
  const totalSubs = cameras.length * subscriberCount // 9
357
418
  console.log(`\n Total: ${totalFrames} frames across ${totalSubs} subscribers`)
358
419
  console.log(` All subs producing: ${allOk ? 'YES' : 'FAIL'}`)
@@ -360,7 +421,10 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
360
421
  // Assertions — every concurrent subscriber on every camera gets frames.
361
422
  for (const cam of cameras) {
362
423
  for (let sub = 0; sub < subscriberCount; sub++) {
363
- expect(results[cam.id]![sub]!.frames, `${cam.id} sub${sub} should produce frames`).toBeGreaterThan(0)
424
+ expect(
425
+ results[cam.id]![sub]!.frames,
426
+ `${cam.id} sub${sub} should produce frames`,
427
+ ).toBeGreaterThan(0)
364
428
  }
365
429
  }
366
430
 
@@ -376,18 +440,32 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
376
440
  console.log('\n--- Resource Benchmark (3 cameras, 5s each) ---')
377
441
 
378
442
  const cameras = [
379
- { id: 'ingresso_main', url: `rtsp://${FRIGATE}:8554/ingresso_main`, label: 'ingresso main (HD)' },
380
- { id: 'ingresso_sub', url: `rtsp://${FRIGATE}:8554/ingresso_sub`, label: 'ingresso sub (SD)' },
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
+ },
381
453
  { id: 'salone_main', url: `rtsp://${FRIGATE}:8554/salone_main`, label: 'salone main (HD)' },
382
454
  ]
383
455
 
384
- console.log(' Stream | Enc BW | Dec FPS | Dec KB/f | CPU % | RSS MB | Heap MB')
385
- console.log(' --------------------|-----------|---------|----------|--------|--------|--------')
456
+ console.log(
457
+ ' Stream | Enc BW | Dec FPS | Dec KB/f | CPU % | RSS MB | Heap MB',
458
+ )
459
+ console.log(
460
+ ' --------------------|-----------|---------|----------|--------|--------|--------',
461
+ )
386
462
 
387
463
  for (const cam of cameras) {
388
464
  const brokerId = `bench-${cam.id}`
389
465
  const broker = await manager.createBroker(brokerId, {
390
- type: 'rtsp', url: cam.url, videoCodec: 'h264',
466
+ type: 'rtsp',
467
+ url: cam.url,
468
+ videoCodec: 'h264',
391
469
  })
392
470
  await (broker as any).start({ type: 'rtsp', url: cam.url, videoCodec: 'h264' })
393
471
 
@@ -399,7 +477,11 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
399
477
  clearTimeout(timeout)
400
478
  void unsub?.()
401
479
  resolve()
402
- }).then((fn) => { unsub = fn }).catch(reject)
480
+ })
481
+ .then((fn) => {
482
+ unsub = fn
483
+ })
484
+ .catch(reject)
403
485
  })
404
486
 
405
487
  // Measure baseline
@@ -430,17 +512,20 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
430
512
  const durationS = 5
431
513
  const encBwKBs = (encodedBytes / 1024 / durationS).toFixed(0)
432
514
  const decFps = (decodedSizes.length / durationS).toFixed(1)
433
- const avgDecKB = decodedSizes.length > 0
434
- ? (decodedSizes.reduce((a, b) => a + b, 0) / decodedSizes.length / 1024).toFixed(0)
435
- : '0'
515
+ const avgDecKB =
516
+ decodedSizes.length > 0
517
+ ? (decodedSizes.reduce((a, b) => a + b, 0) / decodedSizes.length / 1024).toFixed(0)
518
+ : '0'
436
519
  // CPU: user + system microseconds → percentage of 5s wall time
437
520
  const cpuTotalUs = cpuAfter.user + cpuAfter.system
438
- const cpuPct = (cpuTotalUs / (durationS * 1_000_000) * 100).toFixed(1)
521
+ const cpuPct = ((cpuTotalUs / (durationS * 1_000_000)) * 100).toFixed(1)
439
522
  const rssDeltaMB = ((memAfter.rss - memBefore.rss) / 1024 / 1024).toFixed(1)
440
523
  const heapMB = (memAfter.heapUsed / 1024 / 1024).toFixed(1)
441
524
 
442
525
  const label = cam.label.padEnd(20)
443
- console.log(` ${label}| ${encBwKBs.padStart(5)} KB/s | ${decFps.padStart(7)} | ${avgDecKB.padStart(6)} KB | ${cpuPct.padStart(5)}% | ${rssDeltaMB.padStart(6)} | ${heapMB.padStart(7)}`)
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
+ )
444
529
 
445
530
  await broker.stop()
446
531
  await manager.destroyBroker(brokerId)
@@ -450,7 +535,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
450
535
  }
451
536
 
452
537
  // Now test all 3 simultaneously
453
- console.log(' --------------------|-----------|---------|----------|--------|--------|--------')
538
+ console.log(
539
+ ' --------------------|-----------|---------|----------|--------|--------|--------',
540
+ )
454
541
  console.log(' All 3 concurrent:')
455
542
 
456
543
  const memBaseline = process.memoryUsage()
@@ -464,7 +551,9 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
464
551
  const brokerId = `bench-all-${cam.id}`
465
552
  allBrokers.push(brokerId)
466
553
  const broker = await manager.createBroker(brokerId, {
467
- type: 'rtsp', url: cam.url, videoCodec: 'h264',
554
+ type: 'rtsp',
555
+ url: cam.url,
556
+ videoCodec: 'h264',
468
557
  })
469
558
  await (broker as any).start({ type: 'rtsp', url: cam.url, videoCodec: 'h264' })
470
559
 
@@ -475,11 +564,23 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
475
564
  clearTimeout(timeout)
476
565
  void unsub?.()
477
566
  resolve()
478
- }).then((fn) => { unsub = fn }).catch(reject)
567
+ })
568
+ .then((fn) => {
569
+ unsub = fn
570
+ })
571
+ .catch(reject)
479
572
  })
480
573
 
481
- allUnsubs.push(broker.onEncodedData((pkt: EncodedPacket) => { totalEncBytes += pkt.data.length }))
482
- allUnsubs.push(await subscribeDecodedFrames(broker, 5, () => { totalDecFrames++ }))
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
+ )
483
584
  }
484
585
 
485
586
  await wait(5000)
@@ -489,13 +590,15 @@ describe.skipIf(!FRIGATE)('Stream Diagnostic (requires Frigate)', () => {
489
590
 
490
591
  for (const unsub of allUnsubs) await unsub()
491
592
 
492
- const allCpuPct = ((cpuAll.user + cpuAll.system) / (5 * 1_000_000) * 100).toFixed(1)
593
+ const allCpuPct = (((cpuAll.user + cpuAll.system) / (5 * 1_000_000)) * 100).toFixed(1)
493
594
  const allRssMB = ((memAll.rss - memBaseline.rss) / 1024 / 1024).toFixed(1)
494
595
  const allHeapMB = (memAll.heapUsed / 1024 / 1024).toFixed(1)
495
596
  const allEncKBs = (totalEncBytes / 1024 / 5).toFixed(0)
496
597
 
497
598
  console.log(` Encoded BW: ${allEncKBs} KB/s total`)
498
- console.log(` Decoded: ${totalDecFrames} frames total (${(totalDecFrames / 5).toFixed(1)} fps combined)`)
599
+ console.log(
600
+ ` Decoded: ${totalDecFrames} frames total (${(totalDecFrames / 5).toFixed(1)} fps combined)`,
601
+ )
499
602
  console.log(` CPU: ${allCpuPct}%`)
500
603
  console.log(` RSS delta: ${allRssMB} MB`)
501
604
  console.log(` Heap: ${allHeapMB} MB`)