@camstack/server 0.1.8 → 0.2.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 (125) hide show
  1. package/package.json +9 -7
  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 +24 -4
  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 +64 -15
  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 +14 -6
  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 +11 -6
  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 +71 -17
  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/addon-settings.router.ts +4 -1
  60. package/src/api/core/agents.router.ts +52 -53
  61. package/src/api/core/auth.router.ts +55 -36
  62. package/src/api/core/bulk-update-coordinator.ts +25 -22
  63. package/src/api/core/cap-providers.ts +346 -202
  64. package/src/api/core/capabilities.router.ts +30 -23
  65. package/src/api/core/hwaccel.router.ts +37 -10
  66. package/src/api/core/live-events.router.ts +16 -9
  67. package/src/api/core/logs.router.ts +54 -25
  68. package/src/api/core/notifications.router.ts +2 -1
  69. package/src/api/core/repl.router.ts +1 -3
  70. package/src/api/core/settings-backend.router.ts +68 -70
  71. package/src/api/core/system-events.router.ts +41 -32
  72. package/src/api/health/health.routes.ts +7 -13
  73. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  74. package/src/api/oauth2/consent-page.ts +4 -3
  75. package/src/api/oauth2/oauth2-routes.ts +41 -12
  76. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  77. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  78. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
  79. package/src/api/trpc/cap-mount-helpers.ts +64 -55
  80. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  81. package/src/api/trpc/core-cap-bridge.ts +3 -1
  82. package/src/api/trpc/generated-cap-mounts.ts +593 -351
  83. package/src/api/trpc/generated-cap-routers.ts +3680 -579
  84. package/src/api/trpc/scope-access.ts +7 -7
  85. package/src/api/trpc/trpc.context.ts +7 -4
  86. package/src/api/trpc/trpc.middleware.ts +4 -2
  87. package/src/api/trpc/trpc.router.ts +79 -46
  88. package/src/auth/session-cookie.ts +10 -0
  89. package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
  90. package/src/boot/boot-config.ts +103 -122
  91. package/src/boot/post-boot.service.ts +5 -3
  92. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  93. package/src/core/addon/addon-call-gateway.ts +20 -6
  94. package/src/core/addon/addon-package.service.ts +183 -89
  95. package/src/core/addon/addon-registry.service.ts +1163 -1305
  96. package/src/core/addon/addon-search.service.ts +2 -1
  97. package/src/core/addon/addon-settings-provider.ts +27 -7
  98. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  99. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  100. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  101. package/src/core/agent/agent-registry.service.ts +60 -38
  102. package/src/core/auth/auth.service.spec.ts +6 -8
  103. package/src/core/config/config.service.spec.ts +1 -1
  104. package/src/core/events/event-bus.service.spec.ts +44 -21
  105. package/src/core/events/event-bus.service.ts +5 -1
  106. package/src/core/feature/feature.service.spec.ts +4 -1
  107. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  108. package/src/core/logging/logging.service.spec.ts +61 -21
  109. package/src/core/logging/logging.service.ts +12 -3
  110. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  111. package/src/core/moleculer/cap-call-fn.ts +5 -1
  112. package/src/core/moleculer/cap-route-authority.ts +18 -6
  113. package/src/core/moleculer/moleculer.service.ts +120 -32
  114. package/src/core/network/network-quality.service.spec.ts +6 -1
  115. package/src/core/notification/notification-wrapper.service.ts +1 -3
  116. package/src/core/notification/toast-wrapper.service.ts +1 -5
  117. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  118. package/src/core/repl/repl-engine.service.ts +11 -12
  119. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  120. package/src/core/streaming/stream-probe.service.ts +22 -13
  121. package/src/core/topology/topology-emitter.service.ts +5 -1
  122. package/src/launcher.ts +14 -9
  123. package/src/main.ts +602 -531
  124. package/src/manual-boot.ts +133 -154
  125. package/tsconfig.json +20 -8
@@ -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`)
@@ -12,12 +12,17 @@ import type { IScopedLogger } from '@camstack/types'
12
12
  const FRIGATE = process.env.FRIGATE_HOST ?? ''
13
13
 
14
14
  const mockLogger: IScopedLogger = {
15
- debug: () => {}, info: () => {}, warn: console.warn, error: console.error,
15
+ debug: () => {},
16
+ info: () => {},
17
+ warn: console.warn,
18
+ error: console.error,
16
19
  child: () => mockLogger,
17
20
  }
18
21
  const mockLoggingService = { createLogger: () => mockLogger }
19
22
 
20
- function wait(ms: number) { return new Promise(r => setTimeout(r, ms)) }
23
+ function wait(ms: number) {
24
+ return new Promise((r) => setTimeout(r, ms))
25
+ }
21
26
 
22
27
  /**
23
28
  * Poll-based replacement for the removed `IStreamBroker.onDecodedFrame`
@@ -49,27 +54,29 @@ async function discoverSubStreams(): Promise<Array<{ name: string; url: string }
49
54
  return new Promise((resolve, reject) => {
50
55
  const req = http.get(`http://${FRIGATE}:5000/api/config`, (res) => {
51
56
  let data = ''
52
- res.on('data', (chunk: Buffer) => { data += chunk })
57
+ res.on('data', (chunk: Buffer) => {
58
+ data += chunk
59
+ })
53
60
  res.on('end', () => {
54
61
  try {
55
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment --
62
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment --
56
63
  const config = JSON.parse(data)
57
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
64
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
58
65
  const cameras = config.cameras || {}
59
66
  const streams: Array<{ name: string; url: string }> = []
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any --
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any --
61
68
  for (const [name, cam] of Object.entries(cameras) as [string, any][]) {
62
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access --
69
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access --
63
70
  if (!cam.enabled && cam.enabled !== undefined) continue
64
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
71
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
65
72
  const inputs = cam?.ffmpeg?.inputs || []
66
73
  for (const inp of inputs) {
67
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
74
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
68
75
  if (inp.roles?.includes('detect') && inp.path) {
69
76
  let streamName: string
70
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
71
78
  if (inp.path.includes('8554/')) {
72
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
79
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
73
80
  streamName = inp.path.split('8554/').pop()!
74
81
  } else {
75
82
  streamName = `${name}_sub`
@@ -79,11 +86,16 @@ async function discoverSubStreams(): Promise<Array<{ name: string; url: string }
79
86
  }
80
87
  }
81
88
  resolve(streams)
82
- } catch (e) { reject(e) }
89
+ } catch (e) {
90
+ reject(e)
91
+ }
83
92
  })
84
93
  })
85
94
  req.on('error', reject)
86
- req.setTimeout(5000, () => { req.destroy(); reject(new Error('Timeout')) })
95
+ req.setTimeout(5000, () => {
96
+ req.destroy()
97
+ reject(new Error('Timeout'))
98
+ })
87
99
  })
88
100
  }
89
101
 
@@ -92,7 +104,7 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
92
104
  const manager = new StreamBrokerManager([new FfmpegDecoderProvider()], mockLogger)
93
105
 
94
106
  afterAll(async () => {
95
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
107
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
96
108
  await manager.destroyAll()
97
109
  // Wait for ffmpeg processes to exit
98
110
  await wait(2000)
@@ -106,11 +118,13 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
106
118
  const workingStreams: typeof realStreams = []
107
119
  for (const s of realStreams.slice(0, 5)) {
108
120
  try {
109
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
121
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
110
122
  const broker = await manager.createBroker(`probe-${s.name}`, {
111
- type: 'rtsp', url: s.url, videoCodec: 'h264',
123
+ type: 'rtsp',
124
+ url: s.url,
125
+ videoCodec: 'h264',
112
126
  })
113
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
114
128
  await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: 'h264' })
115
129
  const ok = await new Promise<boolean>((resolve) => {
116
130
  const timeout = setTimeout(() => resolve(false), 5000)
@@ -119,17 +133,25 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
119
133
  clearTimeout(timeout)
120
134
  void unsub?.()
121
135
  resolve(true)
122
- }).then((fn) => { unsub = fn }).catch(() => resolve(false))
136
+ })
137
+ .then((fn) => {
138
+ unsub = fn
139
+ })
140
+ .catch(() => resolve(false))
123
141
  })
124
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
142
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
125
143
  await broker.stop()
126
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
144
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
127
145
  await manager.destroyBroker(`probe-${s.name}`)
128
146
  if (ok) workingStreams.push(s)
129
- } catch { /* skip */ }
147
+ } catch {
148
+ /* skip */
149
+ }
130
150
  }
131
151
 
132
- console.log(`\nDiscovered ${realStreams.length} sub-streams, ${workingStreams.length} responding`)
152
+ console.log(
153
+ `\nDiscovered ${realStreams.length} sub-streams, ${workingStreams.length} responding`,
154
+ )
133
155
  if (workingStreams.length === 0) {
134
156
  console.log(' No working sub-streams, skipping')
135
157
  return
@@ -148,7 +170,6 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
148
170
  const scaleLevels = [5, 10, 20, 30]
149
171
 
150
172
  for (const targetCount of scaleLevels) {
151
-
152
173
  console.log(`\n=== Scale: ${targetCount} sub-streams @ 2fps ===`)
153
174
 
154
175
  const memBefore = process.memoryUsage()
@@ -169,11 +190,13 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
169
190
  perCamera[s.name] = { frames: 0, totalBytes: 0 }
170
191
 
171
192
  try {
172
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
193
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
173
194
  const broker = await manager.createBroker(brokerId, {
174
- type: 'rtsp', url: s.url, videoCodec: 'h264',
195
+ type: 'rtsp',
196
+ url: s.url,
197
+ videoCodec: 'h264',
175
198
  })
176
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
177
200
  await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: 'h264' })
178
201
 
179
202
  // Wait for first frame with tight timeout
@@ -187,7 +210,11 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
187
210
  clearTimeout(timeout)
188
211
  void unsub?.()
189
212
  resolve()
190
- }).then((fn) => { unsub = fn }).catch(() => resolve())
213
+ })
214
+ .then((fn) => {
215
+ unsub = fn
216
+ })
217
+ .catch(() => resolve())
191
218
  })
192
219
 
193
220
  // Subscribe for motion analysis
@@ -204,7 +231,9 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
204
231
  await Promise.all(connectPromises)
205
232
 
206
233
  const connectTime = Date.now() - startTime
207
- console.log(` Connected ${targetCount - connectFailures}/${targetCount} in ${(connectTime / 1000).toFixed(1)}s`)
234
+ console.log(
235
+ ` Connected ${targetCount - connectFailures}/${targetCount} in ${(connectTime / 1000).toFixed(1)}s`,
236
+ )
208
237
 
209
238
  // Run for 5 seconds of actual streaming
210
239
  const measureStart = Date.now()
@@ -232,7 +261,10 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
232
261
  if (data.frames < 3) starvingCameras++ // less than 3 frames in 5s = starving
233
262
  }
234
263
 
235
- const cpuPct = ((cpuMeasure.user + cpuMeasure.system) / (measureDuration * 1_000_000) * 100).toFixed(1)
264
+ const cpuPct = (
265
+ ((cpuMeasure.user + cpuMeasure.system) / (measureDuration * 1_000_000)) *
266
+ 100
267
+ ).toFixed(1)
236
268
  const rssMB = (memAfter.rss / 1024 / 1024).toFixed(0)
237
269
  const heapMB = (memAfter.heapUsed / 1024 / 1024).toFixed(0)
238
270
  const combinedFps = (totalFrames / measureDuration).toFixed(1)
@@ -249,26 +281,28 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
249
281
 
250
282
  // Per-camera breakdown
251
283
  console.log(` ---`)
252
- const sorted = Object.entries(perCamera).sort((a, b) => b[1].frames - a[1].frames)
284
+ const sorted = Object.entries(perCamera).toSorted((a, b) => b[1].frames - a[1].frames)
253
285
  for (const [name, data] of sorted) {
254
286
  const fps = (data.frames / measureDuration).toFixed(1)
255
287
  const avgKB = data.frames > 0 ? (data.totalBytes / data.frames / 1024).toFixed(0) : '-'
256
288
  const status = data.frames >= 3 ? '✓' : data.frames > 0 ? '⚠' : '✗'
257
- console.log(` ${status} ${name.padEnd(35)} ${fps.padStart(4)} fps ${avgKB.padStart(5)} KB/f (${data.frames} frames)`)
289
+ console.log(
290
+ ` ${status} ${name.padEnd(35)} ${fps.padStart(4)} fps ${avgKB.padStart(5)} KB/f (${data.frames} frames)`,
291
+ )
258
292
  }
259
293
 
260
294
  // Cleanup for next scale level
261
295
  for (const id of brokerIds) {
262
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
296
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
263
297
  const b = manager.getBroker(id)
264
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
298
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
265
299
  if (b) await b.stop()
266
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
300
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
267
301
  await manager.destroyBroker(id)
268
302
  }
269
303
  brokerIds.length = 0
270
304
  unsubs.length = 0
271
- Object.keys(perCamera).forEach(k => delete perCamera[k])
305
+ Object.keys(perCamera).forEach((k) => delete perCamera[k])
272
306
 
273
307
  // Expect at least 80% of cameras producing
274
308
  expect(activeCameras).toBeGreaterThanOrEqual(Math.floor(targetCount * 0.8))
@@ -227,7 +227,12 @@ describe('F3 — forked-addon routes + custom actions over UDS (backend wiring)'
227
227
  target: 'routes',
228
228
  })
229
229
  expect(rawRoutes).toEqual([
230
- { method: 'GET', path: '/:providerId/start', access: 'public', description: 'Begin OIDC redirect login flow' },
230
+ {
231
+ method: 'GET',
232
+ path: '/:providerId/start',
233
+ access: 'public',
234
+ description: 'Begin OIDC redirect login flow',
235
+ },
231
236
  { method: 'GET', path: '/:providerId/callback', access: 'public' },
232
237
  ])
233
238
  // Coarse reachability gate the route-mount fallback uses: the child is