@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.
- package/package.json +11 -9
- 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 +206 -0
- 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 +292 -0
- 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 +177 -0
- 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 +137 -0
- 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 +265 -5
- 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/__tests__/integration-markers.spec.ts +10 -0
- 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 +459 -166
- 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 +58 -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__/client-ip.spec.ts +27 -1
- 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 +136 -0
- package/src/api/trpc/cap-mount-helpers.ts +64 -44
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/client-ip.ts +17 -0
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +801 -286
- package/src/api/trpc/generated-cap-routers.ts +5723 -719
- 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 +117 -48
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/integration-id-backfill.ts +109 -0
- 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/__tests__/addon-row-manifest.spec.ts +62 -0
- 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 +1212 -1267
- package/src/core/addon/addon-row-manifest.ts +29 -0
- 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 +19 -5
- 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 +145 -29
- package/src/core/network/network-quality.service.spec.ts +7 -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 +658 -495
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
- package/src/core/storage/settings-store.spec.ts +0 -213
- package/src/core/storage/settings-store.ts +0 -2
- package/src/core/storage/sql-schema.spec.ts +0 -140
- 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(
|
|
158
|
-
|
|
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(
|
|
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(
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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(
|
|
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) {
|
|
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`)
|