@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.
- package/package.json +9 -7
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +346 -202
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +54 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
- package/src/api/trpc/cap-mount-helpers.ts +64 -55
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +593 -351
- package/src/api/trpc/generated-cap-routers.ts +3680 -579
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +79 -46
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1163 -1305
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +12 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +120 -32
- package/src/core/network/network-quality.service.spec.ts +6 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +602 -531
- package/src/manual-boot.ts +133 -154
- 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) {
|
|
42
|
-
|
|
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',
|
|
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({
|
|
96
|
-
|
|
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',
|
|
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
|
-
})
|
|
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',
|
|
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
|
-
})
|
|
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, () => {
|
|
200
|
-
|
|
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(
|
|
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(
|
|
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',
|
|
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
|
-
})
|
|
288
|
+
})
|
|
289
|
+
.then((fn) => {
|
|
290
|
+
unsub = fn
|
|
291
|
+
})
|
|
292
|
+
.catch(reject)
|
|
252
293
|
})
|
|
253
294
|
|
|
254
|
-
collectUnsubs.push(
|
|
255
|
-
|
|
256
|
-
|
|
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 =
|
|
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) {
|
|
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',
|
|
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
|
-
})
|
|
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(
|
|
324
|
-
|
|
325
|
-
|
|
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 =
|
|
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(
|
|
355
|
-
sum + Object.values(cam).reduce((s, sub) => s + sub.frames, 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(
|
|
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
|
-
{
|
|
380
|
-
|
|
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(
|
|
385
|
-
|
|
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',
|
|
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
|
-
})
|
|
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 =
|
|
434
|
-
|
|
435
|
-
|
|
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(
|
|
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',
|
|
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
|
-
})
|
|
567
|
+
})
|
|
568
|
+
.then((fn) => {
|
|
569
|
+
unsub = fn
|
|
570
|
+
})
|
|
571
|
+
.catch(reject)
|
|
479
572
|
})
|
|
480
573
|
|
|
481
|
-
allUnsubs.push(
|
|
482
|
-
|
|
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(
|
|
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: () => {},
|
|
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) {
|
|
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) => {
|
|
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) {
|
|
89
|
+
} catch (e) {
|
|
90
|
+
reject(e)
|
|
91
|
+
}
|
|
83
92
|
})
|
|
84
93
|
})
|
|
85
94
|
req.on('error', reject)
|
|
86
|
-
req.setTimeout(5000, () => {
|
|
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',
|
|
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
|
-
})
|
|
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 {
|
|
147
|
+
} catch {
|
|
148
|
+
/* skip */
|
|
149
|
+
}
|
|
130
150
|
}
|
|
131
151
|
|
|
132
|
-
console.log(
|
|
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',
|
|
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
|
-
})
|
|
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(
|
|
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 = (
|
|
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).
|
|
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(
|
|
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
|
-
{
|
|
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
|