dolphin-server-modules 2.11.1 → 2.11.2

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 (115) hide show
  1. package/TUTORIAL_NEPALI.md +181 -0
  2. package/dist/adapters/mongoose/index.test.d.ts +1 -0
  3. package/dist/adapters/mongoose/index.test.js +145 -0
  4. package/dist/adapters/mongoose/index.test.js.map +1 -0
  5. package/dist/adapters/mongoose/integration.test.d.ts +5 -0
  6. package/dist/adapters/mongoose/integration.test.js +217 -0
  7. package/dist/adapters/mongoose/integration.test.js.map +1 -0
  8. package/dist/ai/dolphin-agent/agent.d.ts +5 -0
  9. package/dist/ai/dolphin-agent/agent.js +93 -46
  10. package/dist/ai/dolphin-agent/agent.js.map +1 -1
  11. package/dist/ai/dolphin-agent/config.js +19 -23
  12. package/dist/ai/dolphin-agent/config.js.map +1 -1
  13. package/dist/auth/auth.test.d.ts +1 -0
  14. package/dist/auth/auth.test.js +286 -0
  15. package/dist/auth/auth.test.js.map +1 -0
  16. package/dist/authController/authController.js +1 -1
  17. package/dist/authController/authController.js.map +1 -1
  18. package/dist/authController/authController.test.d.ts +1 -0
  19. package/dist/authController/authController.test.js +359 -0
  20. package/dist/authController/authController.test.js.map +1 -0
  21. package/dist/bin/cli.js +494 -131
  22. package/dist/bin/cli.js.map +1 -1
  23. package/dist/client.test.d.ts +22 -0
  24. package/dist/client.test.js +573 -0
  25. package/dist/client.test.js.map +1 -0
  26. package/dist/controller/controller.test.d.ts +1 -0
  27. package/dist/controller/controller.test.js +37 -0
  28. package/dist/controller/controller.test.js.map +1 -0
  29. package/dist/curd/crud.test.d.ts +1 -0
  30. package/dist/curd/crud.test.js +104 -0
  31. package/dist/curd/crud.test.js.map +1 -0
  32. package/dist/demo-server.d.ts +1 -0
  33. package/dist/demo-server.js +191 -0
  34. package/dist/demo-server.js.map +1 -0
  35. package/dist/djson/djson.test.d.ts +1 -0
  36. package/dist/djson/djson.test.js +200 -0
  37. package/dist/djson/djson.test.js.map +1 -0
  38. package/dist/dolphin-bench.d.ts +1 -0
  39. package/dist/dolphin-bench.js +63 -0
  40. package/dist/dolphin-bench.js.map +1 -0
  41. package/dist/hard-performance-test.d.ts +1 -0
  42. package/dist/hard-performance-test.js +97 -0
  43. package/dist/hard-performance-test.js.map +1 -0
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.js +4 -4
  46. package/dist/index.js.map +1 -1
  47. package/dist/middleware/zod.test.d.ts +1 -0
  48. package/dist/middleware/zod.test.js +74 -0
  49. package/dist/middleware/zod.test.js.map +1 -0
  50. package/dist/performance-test.d.ts +1 -0
  51. package/dist/performance-test.js +92 -0
  52. package/dist/performance-test.js.map +1 -0
  53. package/dist/real-test-mongoose.d.ts +1 -0
  54. package/dist/real-test-mongoose.js +104 -0
  55. package/dist/real-test-mongoose.js.map +1 -0
  56. package/dist/realtime/camera.d.ts +119 -0
  57. package/dist/realtime/camera.js +299 -0
  58. package/dist/realtime/camera.js.map +1 -0
  59. package/dist/realtime/camera.test.d.ts +1 -0
  60. package/dist/realtime/camera.test.js +345 -0
  61. package/dist/realtime/camera.test.js.map +1 -0
  62. package/dist/realtime/core.d.ts +5 -5
  63. package/dist/realtime/core.js +6 -6
  64. package/dist/realtime/core.js.map +1 -1
  65. package/dist/realtime/index.d.ts +6 -4
  66. package/dist/realtime/index.js +6 -4
  67. package/dist/realtime/index.js.map +1 -1
  68. package/dist/realtime/realtime.test.d.ts +1 -0
  69. package/dist/realtime/realtime.test.js +623 -0
  70. package/dist/realtime/realtime.test.js.map +1 -0
  71. package/dist/realtime/rtsp.d.ts +65 -0
  72. package/dist/realtime/rtsp.js +410 -0
  73. package/dist/realtime/rtsp.js.map +1 -0
  74. package/dist/realtime/rtsp.test.d.ts +1 -0
  75. package/dist/realtime/rtsp.test.js +361 -0
  76. package/dist/realtime/rtsp.test.js.map +1 -0
  77. package/dist/router/router.test.d.ts +1 -0
  78. package/dist/router/router.test.js +45 -0
  79. package/dist/router/router.test.js.map +1 -0
  80. package/dist/server/server.d.ts +8 -8
  81. package/dist/server/server.js +2 -11
  82. package/dist/server/server.js.map +1 -1
  83. package/dist/server/server.test.d.ts +1 -0
  84. package/dist/server/server.test.js +299 -0
  85. package/dist/server/server.test.js.map +1 -0
  86. package/dist/services/ai-service.js +22 -11
  87. package/dist/services/ai-service.js.map +1 -1
  88. package/dist/signaling/index.d.ts +1 -1
  89. package/dist/signaling/signaling.test.d.ts +1 -0
  90. package/dist/signaling/signaling.test.js +112 -0
  91. package/dist/signaling/signaling.test.js.map +1 -0
  92. package/dist/swagger/swagger.test.d.ts +1 -0
  93. package/dist/swagger/swagger.test.js +38 -0
  94. package/dist/swagger/swagger.test.js.map +1 -0
  95. package/dist/templates/index.d.ts +6 -0
  96. package/dist/templates/index.js +247 -70
  97. package/dist/templates/index.js.map +1 -1
  98. package/dist/test-2fa-real.d.ts +1 -0
  99. package/dist/test-2fa-real.js +105 -0
  100. package/dist/test-2fa-real.js.map +1 -0
  101. package/dist/test-dolphin.d.ts +1 -0
  102. package/dist/test-dolphin.js +98 -0
  103. package/dist/test-dolphin.js.map +1 -0
  104. package/dist/utils/ctx.d.ts +50 -0
  105. package/dist/utils/ctx.js +82 -0
  106. package/dist/utils/ctx.js.map +1 -0
  107. package/package.json +156 -65
  108. package/scripts/client.js +838 -703
  109. package/scripts/benchmark.js +0 -12
  110. package/scripts/benchmark.ts +0 -12
  111. package/scripts/list-models.js +0 -34
  112. package/scripts/run-real-ai-test.js +0 -79
  113. package/scripts/test-ai-logic.js +0 -44
  114. package/scripts/test-client.js +0 -105
  115. package/scripts/test-dolphin.js +0 -36
@@ -0,0 +1,299 @@
1
+ import { EventEmitter } from 'events';
2
+ /**
3
+ * Detect codec from raw binary frame buffer
4
+ */
5
+ function detectCodec(buf) {
6
+ if (buf.length < 4)
7
+ return 'UNKNOWN';
8
+ // MJPEG: starts with FF D8 FF
9
+ if (buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
10
+ return 'MJPEG';
11
+ }
12
+ // H.264 NAL unit: starts with 00 00 00 01 or 00 00 01
13
+ if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x00 && buf[3] === 0x01) {
14
+ return 'H264';
15
+ }
16
+ if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x01) {
17
+ return 'H264';
18
+ }
19
+ // H.265 / HEVC: NAL unit type in byte 0 bits [1-6]
20
+ // HEVC starts with 00 00 00 01 as well but NAL type >= 32
21
+ if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x00 && buf[3] === 0x01) {
22
+ const nalType = (buf[4] >> 1) & 0x3F;
23
+ if (nalType >= 32)
24
+ return 'H265';
25
+ return 'H264';
26
+ }
27
+ // Raw RGB: heuristic — divisible by 3 (RGB triplets) and large enough
28
+ if (buf.length % 3 === 0 && buf.length > 1000) {
29
+ return 'RAW_RGB';
30
+ }
31
+ // Raw YUV: heuristic — divisible by 2 (YUV 4:2:2)
32
+ if (buf.length % 2 === 0 && buf.length > 1000) {
33
+ return 'RAW_YUV';
34
+ }
35
+ return 'UNKNOWN';
36
+ }
37
+ /**
38
+ * Detect if H.264 NAL unit is a keyframe (IDR frame)
39
+ */
40
+ function isH264KeyFrame(buf) {
41
+ // Find NAL start code
42
+ let offset = 0;
43
+ if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x00 && buf[3] === 0x01) {
44
+ offset = 4;
45
+ }
46
+ else if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x01) {
47
+ offset = 3;
48
+ }
49
+ if (offset >= buf.length)
50
+ return false;
51
+ const nalType = buf[offset] & 0x1F;
52
+ return nalType === 5; // IDR = keyframe
53
+ }
54
+ /**
55
+ * Try to extract MJPEG resolution from JFIF/EXIF header
56
+ */
57
+ function getMjpegResolution(buf) {
58
+ // Scan for SOF0 (FF C0) or SOF2 (FF C2) markers
59
+ for (let i = 0; i < Math.min(buf.length - 9, 512); i++) {
60
+ if (buf[i] === 0xFF && (buf[i + 1] === 0xC0 || buf[i + 1] === 0xC2)) {
61
+ const height = buf.readUInt16BE(i + 5);
62
+ const width = buf.readUInt16BE(i + 7);
63
+ if (width > 0 && height > 0)
64
+ return { width, height };
65
+ }
66
+ }
67
+ return { width: 0, height: 0 };
68
+ }
69
+ /**
70
+ * CameraFrameModule — Direct binary camera frame handler for RealtimeCore
71
+ *
72
+ * Handles raw binary frames from IP cameras without needing FFmpeg.
73
+ * Supports MJPEG, H.264, H.265, RAW_RGB, RAW_YUV formats.
74
+ *
75
+ * Usage:
76
+ * const cam = new CameraFrameModule(rt);
77
+ * cam.registerCamera({ cameraId: 'cam1', codec: 'MJPEG' });
78
+ * cam.ingestFrame('cam1', frameBuffer);
79
+ * cam.subscribe('cam1', (frame) => { ... });
80
+ */
81
+ export class CameraFrameModule extends EventEmitter {
82
+ rt;
83
+ cameras = new Map();
84
+ frameCounters = new Map();
85
+ fpsTrackers = new Map();
86
+ offlineTimers = new Map();
87
+ OFFLINE_TIMEOUT_MS = 10000; // 10s no frame → offline
88
+ FPS_WINDOW_MS = 1000;
89
+ constructor(rt) {
90
+ super();
91
+ this.rt = rt;
92
+ }
93
+ /**
94
+ * Register a camera before ingesting frames
95
+ */
96
+ registerCamera(opts) {
97
+ const existing = this.cameras.get(opts.cameraId);
98
+ if (existing)
99
+ return; // already registered
100
+ this.cameras.set(opts.cameraId, {
101
+ ...opts,
102
+ codec: opts.codec || 'UNKNOWN',
103
+ framesReceived: 0,
104
+ bytesReceived: 0,
105
+ fps: 0,
106
+ lastFrameAt: 0,
107
+ width: opts.expectedWidth || 0,
108
+ height: opts.expectedHeight || 0,
109
+ isOnline: false,
110
+ });
111
+ this.frameCounters.set(opts.cameraId, 0);
112
+ this.fpsTrackers.set(opts.cameraId, { count: 0, windowStart: Date.now() });
113
+ this.emit('camera:registered', { cameraId: opts.cameraId });
114
+ }
115
+ /**
116
+ * Ingest a raw binary frame from a camera.
117
+ * Auto-detects codec if not registered.
118
+ * Publishes to RealtimeCore topic: camera/<cameraId>/frame
119
+ */
120
+ ingestFrame(cameraId, rawBuffer) {
121
+ if (!Buffer.isBuffer(rawBuffer) || rawBuffer.length === 0)
122
+ return null;
123
+ // Auto-register if not known
124
+ if (!this.cameras.has(cameraId)) {
125
+ this.registerCamera({ cameraId });
126
+ }
127
+ const cam = this.cameras.get(cameraId);
128
+ const now = Date.now();
129
+ // Detect codec from binary signature
130
+ const codec = cam.codec !== 'UNKNOWN' ? cam.codec : detectCodec(rawBuffer);
131
+ // Extract resolution
132
+ let width = cam.width;
133
+ let height = cam.height;
134
+ if ((width === 0 || height === 0) && codec === 'MJPEG') {
135
+ const res = getMjpegResolution(rawBuffer);
136
+ width = res.width || width;
137
+ height = res.height || height;
138
+ }
139
+ // Detect keyframe
140
+ let isKeyFrame = false;
141
+ if (codec === 'MJPEG')
142
+ isKeyFrame = true; // MJPEG = every frame is a keyframe
143
+ if (codec === 'H264')
144
+ isKeyFrame = isH264KeyFrame(rawBuffer);
145
+ // Increment counters
146
+ const frameIndex = (this.frameCounters.get(cameraId) || 0) + 1;
147
+ this.frameCounters.set(cameraId, frameIndex);
148
+ // Update FPS tracker
149
+ const tracker = this.fpsTrackers.get(cameraId);
150
+ tracker.count++;
151
+ const elapsed = now - tracker.windowStart;
152
+ if (elapsed >= this.FPS_WINDOW_MS) {
153
+ cam.fps = Math.round((tracker.count / elapsed) * 1000);
154
+ tracker.count = 0;
155
+ tracker.windowStart = now;
156
+ }
157
+ // Update stats
158
+ cam.codec = codec;
159
+ cam.width = width;
160
+ cam.height = height;
161
+ cam.framesReceived = frameIndex;
162
+ cam.bytesReceived += rawBuffer.length;
163
+ cam.lastFrameAt = now;
164
+ cam.isOnline = true;
165
+ // Build frame object
166
+ const frame = {
167
+ cameraId,
168
+ codec,
169
+ width,
170
+ height,
171
+ timestamp: now,
172
+ frameIndex,
173
+ isKeyFrame,
174
+ data: rawBuffer,
175
+ sizeBytes: rawBuffer.length,
176
+ };
177
+ // Publish to RealtimeCore — high frequency binary push
178
+ this.rt.pubPush(`camera/${cameraId}/frame`, rawBuffer);
179
+ // Also publish metadata separately for lightweight subscribers
180
+ this.rt.publish(`camera/${cameraId}/meta`, {
181
+ cameraId,
182
+ codec,
183
+ width,
184
+ height,
185
+ timestamp: now,
186
+ frameIndex,
187
+ isKeyFrame,
188
+ sizeBytes: rawBuffer.length,
189
+ fps: cam.fps,
190
+ });
191
+ // Emit locally
192
+ this.emit('frame', frame);
193
+ this.emit(`frame:${cameraId}`, frame);
194
+ // Reset offline timer
195
+ this._resetOfflineTimer(cameraId);
196
+ return frame;
197
+ }
198
+ /**
199
+ * Subscribe to frames from a specific camera
200
+ */
201
+ subscribe(cameraId, fn) {
202
+ this.on(`frame:${cameraId}`, fn);
203
+ }
204
+ /**
205
+ * Subscribe to ALL cameras
206
+ */
207
+ subscribeAll(fn) {
208
+ this.on('frame', fn);
209
+ }
210
+ /**
211
+ * Unsubscribe from a camera
212
+ */
213
+ unsubscribe(cameraId, fn) {
214
+ this.off(`frame:${cameraId}`, fn);
215
+ }
216
+ /**
217
+ * Get live stats for a camera
218
+ */
219
+ getStats(cameraId) {
220
+ const cam = this.cameras.get(cameraId);
221
+ if (!cam)
222
+ return undefined;
223
+ return {
224
+ cameraId: cam.cameraId,
225
+ framesReceived: cam.framesReceived,
226
+ bytesReceived: cam.bytesReceived,
227
+ fps: cam.fps,
228
+ lastFrameAt: cam.lastFrameAt,
229
+ codec: cam.codec,
230
+ width: cam.width,
231
+ height: cam.height,
232
+ isOnline: cam.isOnline,
233
+ };
234
+ }
235
+ /**
236
+ * Get stats for all cameras
237
+ */
238
+ getAllStats() {
239
+ return Array.from(this.cameras.keys()).map(id => this.getStats(id));
240
+ }
241
+ /**
242
+ * List registered camera IDs
243
+ */
244
+ listCameras() {
245
+ return Array.from(this.cameras.keys());
246
+ }
247
+ /**
248
+ * Mark a camera as offline manually
249
+ */
250
+ markOffline(cameraId) {
251
+ const cam = this.cameras.get(cameraId);
252
+ if (cam) {
253
+ cam.isOnline = false;
254
+ this.emit('camera:offline', { cameraId });
255
+ this.rt.publish(`camera/${cameraId}/status`, { cameraId, status: 'offline', timestamp: Date.now() });
256
+ }
257
+ }
258
+ /**
259
+ * Remove a camera from the module
260
+ */
261
+ removeCamera(cameraId) {
262
+ this.cameras.delete(cameraId);
263
+ this.frameCounters.delete(cameraId);
264
+ this.fpsTrackers.delete(cameraId);
265
+ const timer = this.offlineTimers.get(cameraId);
266
+ if (timer)
267
+ clearTimeout(timer);
268
+ this.offlineTimers.delete(cameraId);
269
+ this.emit('camera:removed', { cameraId });
270
+ }
271
+ /**
272
+ * Destroy the module and clean up
273
+ */
274
+ destroy() {
275
+ for (const timer of this.offlineTimers.values())
276
+ clearTimeout(timer);
277
+ this.offlineTimers.clear();
278
+ this.cameras.clear();
279
+ this.frameCounters.clear();
280
+ this.fpsTrackers.clear();
281
+ this.removeAllListeners();
282
+ }
283
+ _resetOfflineTimer(cameraId) {
284
+ const existing = this.offlineTimers.get(cameraId);
285
+ if (existing)
286
+ clearTimeout(existing);
287
+ const timer = setTimeout(() => {
288
+ this.markOffline(cameraId);
289
+ }, this.OFFLINE_TIMEOUT_MS);
290
+ this.offlineTimers.set(cameraId, timer);
291
+ }
292
+ }
293
+ /**
294
+ * Factory function
295
+ */
296
+ export function createCameraModule(rt) {
297
+ return new CameraFrameModule(rt);
298
+ }
299
+ //# sourceMappingURL=camera.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera.js","sourceRoot":"","sources":["../../src/realtime/camera.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,345 @@
1
+ /// <reference types="jest" />
2
+ import { RealtimeCore } from './core';
3
+ import { createCameraModule } from './camera';
4
+ // ============================================
5
+ // Helper: MJPEG frame बनाउने (FF D8 FF signature)
6
+ // ============================================
7
+ function makeMjpegFrame(width = 640, height = 480) {
8
+ // Minimal MJPEG: SOI + SOF0 marker with resolution
9
+ const buf = Buffer.alloc(64, 0x00);
10
+ // SOI marker
11
+ buf[0] = 0xFF;
12
+ buf[1] = 0xD8;
13
+ // APP0 marker
14
+ buf[2] = 0xFF;
15
+ buf[3] = 0xE0;
16
+ // SOF0 marker at offset 20
17
+ buf[20] = 0xFF;
18
+ buf[21] = 0xC0;
19
+ buf[22] = 0x00;
20
+ buf[23] = 0x11; // length
21
+ buf[24] = 0x08; // precision
22
+ buf.writeUInt16BE(height, 25);
23
+ buf.writeUInt16BE(width, 27);
24
+ return buf;
25
+ }
26
+ // ============================================
27
+ // Helper: H.264 IDR keyframe बनाउने
28
+ // ============================================
29
+ function makeH264KeyFrame() {
30
+ const buf = Buffer.alloc(16, 0x00);
31
+ // NAL start code
32
+ buf[0] = 0x00;
33
+ buf[1] = 0x00;
34
+ buf[2] = 0x00;
35
+ buf[3] = 0x01;
36
+ // NAL type = 5 (IDR = keyframe)
37
+ buf[4] = 0x65;
38
+ return buf;
39
+ }
40
+ // ============================================
41
+ // Helper: H.264 P-frame (non-keyframe) बनाउने
42
+ // ============================================
43
+ function makeH264PFrame() {
44
+ const buf = Buffer.alloc(16, 0x00);
45
+ buf[0] = 0x00;
46
+ buf[1] = 0x00;
47
+ buf[2] = 0x00;
48
+ buf[3] = 0x01;
49
+ // NAL type = 1 (non-IDR = P-frame)
50
+ buf[4] = 0x41;
51
+ return buf;
52
+ }
53
+ // ============================================
54
+ // Helper: Unknown/raw frame
55
+ // ============================================
56
+ function makeRawFrame(size = 2048) {
57
+ return Buffer.alloc(size, 0xAB);
58
+ }
59
+ describe('CameraFrameModule', () => {
60
+ let rt;
61
+ let cam;
62
+ let instances = [];
63
+ beforeEach(() => {
64
+ rt = new RealtimeCore({ debug: false, maxBufferPerTopic: 200 });
65
+ cam = createCameraModule(rt);
66
+ instances.push(rt);
67
+ });
68
+ afterEach(async () => {
69
+ cam.destroy();
70
+ for (const instance of instances) {
71
+ await instance.destroy();
72
+ }
73
+ instances = [];
74
+ });
75
+ // ============================================
76
+ // Camera Registration Tests
77
+ // ============================================
78
+ describe('Camera Registration', () => {
79
+ it('should register a camera', () => {
80
+ cam.registerCamera({ cameraId: 'cam1', codec: 'MJPEG' });
81
+ expect(cam.listCameras()).toContain('cam1');
82
+ });
83
+ it('should auto-register camera on first frame', () => {
84
+ const frame = makeMjpegFrame();
85
+ cam.ingestFrame('cam-auto', frame);
86
+ expect(cam.listCameras()).toContain('cam-auto');
87
+ });
88
+ it('should not duplicate register same camera', () => {
89
+ cam.registerCamera({ cameraId: 'cam1' });
90
+ cam.registerCamera({ cameraId: 'cam1' });
91
+ expect(cam.listCameras().filter(id => id === 'cam1').length).toBe(1);
92
+ });
93
+ it('should remove a camera', () => {
94
+ cam.registerCamera({ cameraId: 'cam1' });
95
+ cam.removeCamera('cam1');
96
+ expect(cam.listCameras()).not.toContain('cam1');
97
+ });
98
+ });
99
+ // ============================================
100
+ // Codec Detection Tests
101
+ // ============================================
102
+ describe('Codec Detection', () => {
103
+ it('should detect MJPEG codec from FF D8 FF signature', () => {
104
+ const frame = makeMjpegFrame();
105
+ const result = cam.ingestFrame('cam-mjpeg', frame);
106
+ expect(result).not.toBeNull();
107
+ expect(result?.codec).toBe('MJPEG');
108
+ });
109
+ it('should detect H.264 codec from NAL start code 00 00 00 01', () => {
110
+ const frame = makeH264KeyFrame();
111
+ const result = cam.ingestFrame('cam-h264', frame);
112
+ expect(result?.codec).toBe('H264');
113
+ });
114
+ it('should detect H.264 P-frame', () => {
115
+ const frame = makeH264PFrame();
116
+ const result = cam.ingestFrame('cam-h264-p', frame);
117
+ expect(result?.codec).toBe('H264');
118
+ });
119
+ it('should return UNKNOWN for unrecognized binary', () => {
120
+ const buf = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]);
121
+ const result = cam.ingestFrame('cam-unknown', buf);
122
+ expect(result?.codec).toBe('UNKNOWN');
123
+ });
124
+ });
125
+ // ============================================
126
+ // Keyframe Detection Tests
127
+ // ============================================
128
+ describe('Keyframe Detection', () => {
129
+ it('should mark MJPEG as keyframe (every frame)', () => {
130
+ const frame = makeMjpegFrame();
131
+ const result = cam.ingestFrame('cam1', frame);
132
+ expect(result?.isKeyFrame).toBe(true);
133
+ });
134
+ it('should mark H.264 IDR (NAL type 5) as keyframe', () => {
135
+ const frame = makeH264KeyFrame();
136
+ const result = cam.ingestFrame('cam1', frame);
137
+ expect(result?.isKeyFrame).toBe(true);
138
+ });
139
+ it('should mark H.264 P-frame (NAL type 1) as non-keyframe', () => {
140
+ const frame = makeH264PFrame();
141
+ const result = cam.ingestFrame('cam1', frame);
142
+ expect(result?.isKeyFrame).toBe(false);
143
+ });
144
+ });
145
+ // ============================================
146
+ // Resolution Detection Tests
147
+ // ============================================
148
+ describe('Resolution Detection', () => {
149
+ it('should extract resolution from MJPEG SOF0 header', () => {
150
+ const frame = makeMjpegFrame(1280, 720);
151
+ const result = cam.ingestFrame('cam-res', frame);
152
+ expect(result?.width).toBe(1280);
153
+ expect(result?.height).toBe(720);
154
+ });
155
+ it('should use expectedWidth/Height if provided', () => {
156
+ cam.registerCamera({ cameraId: 'cam-preset', expectedWidth: 1920, expectedHeight: 1080 });
157
+ const frame = makeRawFrame();
158
+ const result = cam.ingestFrame('cam-preset', frame);
159
+ expect(result?.width).toBe(1920);
160
+ expect(result?.height).toBe(1080);
161
+ });
162
+ });
163
+ // ============================================
164
+ // Frame Ingestion Tests
165
+ // ============================================
166
+ describe('Frame Ingestion', () => {
167
+ it('should return null for empty buffer', () => {
168
+ const result = cam.ingestFrame('cam1', Buffer.alloc(0));
169
+ expect(result).toBeNull();
170
+ });
171
+ it('should return null for non-buffer input', () => {
172
+ const result = cam.ingestFrame('cam1', null);
173
+ expect(result).toBeNull();
174
+ });
175
+ it('should increment frame index on each ingest', () => {
176
+ const frame = makeMjpegFrame();
177
+ const r1 = cam.ingestFrame('cam1', frame);
178
+ const r2 = cam.ingestFrame('cam1', frame);
179
+ const r3 = cam.ingestFrame('cam1', frame);
180
+ expect(r1?.frameIndex).toBe(1);
181
+ expect(r2?.frameIndex).toBe(2);
182
+ expect(r3?.frameIndex).toBe(3);
183
+ });
184
+ it('should include correct timestamp', () => {
185
+ const before = Date.now();
186
+ const result = cam.ingestFrame('cam1', makeMjpegFrame());
187
+ const after = Date.now();
188
+ expect(result?.timestamp).toBeGreaterThanOrEqual(before);
189
+ expect(result?.timestamp).toBeLessThanOrEqual(after);
190
+ });
191
+ it('should include correct sizeBytes', () => {
192
+ const frame = makeMjpegFrame();
193
+ const result = cam.ingestFrame('cam1', frame);
194
+ expect(result?.sizeBytes).toBe(frame.length);
195
+ });
196
+ });
197
+ // ============================================
198
+ // Subscription Tests
199
+ // ============================================
200
+ describe('Subscriptions', () => {
201
+ it('should receive frame via subscribe()', (done) => {
202
+ cam.subscribe('cam1', (frame) => {
203
+ expect(frame.cameraId).toBe('cam1');
204
+ expect(frame.codec).toBe('MJPEG');
205
+ done();
206
+ });
207
+ cam.ingestFrame('cam1', makeMjpegFrame());
208
+ });
209
+ it('should receive ALL camera frames via subscribeAll()', (done) => {
210
+ const received = [];
211
+ cam.subscribeAll((frame) => {
212
+ received.push(frame.cameraId);
213
+ if (received.length === 2) {
214
+ expect(received).toContain('cam1');
215
+ expect(received).toContain('cam2');
216
+ done();
217
+ }
218
+ });
219
+ cam.ingestFrame('cam1', makeMjpegFrame());
220
+ cam.ingestFrame('cam2', makeH264KeyFrame());
221
+ });
222
+ it('should unsubscribe correctly', () => {
223
+ const fn = jest.fn();
224
+ cam.subscribe('cam1', fn);
225
+ cam.unsubscribe('cam1', fn);
226
+ cam.ingestFrame('cam1', makeMjpegFrame());
227
+ expect(fn).not.toHaveBeenCalled();
228
+ });
229
+ it('should publish frame to RealtimeCore topic camera/<id>/frame', () => {
230
+ const rtFn = jest.fn();
231
+ rt.subscribe('camera/cam1/frame', rtFn);
232
+ cam.ingestFrame('cam1', makeMjpegFrame());
233
+ expect(rtFn).toHaveBeenCalledTimes(1);
234
+ });
235
+ it('should publish metadata to camera/<id>/meta topic', (done) => {
236
+ rt.subscribe('camera/cam1/meta', (meta) => {
237
+ expect(meta.cameraId).toBe('cam1');
238
+ expect(meta.codec).toBe('MJPEG');
239
+ expect(meta.frameIndex).toBe(1);
240
+ expect(meta).toHaveProperty('fps');
241
+ done();
242
+ });
243
+ cam.ingestFrame('cam1', makeMjpegFrame());
244
+ });
245
+ it('should support wildcard subscription for all cameras via RealtimeCore', () => {
246
+ const fn = jest.fn();
247
+ rt.subscribe('camera/+/meta', fn);
248
+ cam.ingestFrame('cam1', makeMjpegFrame());
249
+ cam.ingestFrame('cam2', makeH264KeyFrame());
250
+ expect(fn).toHaveBeenCalledTimes(2);
251
+ });
252
+ });
253
+ // ============================================
254
+ // Stats Tests
255
+ // ============================================
256
+ describe('Camera Stats', () => {
257
+ it('should track framesReceived count', () => {
258
+ cam.ingestFrame('cam1', makeMjpegFrame());
259
+ cam.ingestFrame('cam1', makeMjpegFrame());
260
+ cam.ingestFrame('cam1', makeMjpegFrame());
261
+ const stats = cam.getStats('cam1');
262
+ expect(stats?.framesReceived).toBe(3);
263
+ });
264
+ it('should track bytesReceived', () => {
265
+ const frame = makeMjpegFrame();
266
+ cam.ingestFrame('cam1', frame);
267
+ cam.ingestFrame('cam1', frame);
268
+ const stats = cam.getStats('cam1');
269
+ expect(stats?.bytesReceived).toBe(frame.length * 2);
270
+ });
271
+ it('should mark camera as online after frame', () => {
272
+ cam.ingestFrame('cam1', makeMjpegFrame());
273
+ expect(cam.getStats('cam1')?.isOnline).toBe(true);
274
+ });
275
+ it('should return undefined stats for unknown camera', () => {
276
+ expect(cam.getStats('non-existent')).toBeUndefined();
277
+ });
278
+ it('should return all cameras stats', () => {
279
+ cam.ingestFrame('cam1', makeMjpegFrame());
280
+ cam.ingestFrame('cam2', makeH264KeyFrame());
281
+ const allStats = cam.getAllStats();
282
+ expect(allStats.length).toBe(2);
283
+ expect(allStats.map(s => s.cameraId)).toContain('cam1');
284
+ expect(allStats.map(s => s.cameraId)).toContain('cam2');
285
+ });
286
+ });
287
+ // ============================================
288
+ // Offline Detection Tests
289
+ // ============================================
290
+ describe('Offline Detection', () => {
291
+ it('should emit camera:offline when markOffline() called', (done) => {
292
+ cam.registerCamera({ cameraId: 'cam1' });
293
+ cam.on('camera:offline', ({ cameraId }) => {
294
+ expect(cameraId).toBe('cam1');
295
+ done();
296
+ });
297
+ cam.markOffline('cam1');
298
+ });
299
+ it('should set isOnline=false after markOffline()', () => {
300
+ cam.ingestFrame('cam1', makeMjpegFrame());
301
+ cam.markOffline('cam1');
302
+ expect(cam.getStats('cam1')?.isOnline).toBe(false);
303
+ });
304
+ });
305
+ // ============================================
306
+ // Multi-Camera Tests
307
+ // ============================================
308
+ describe('Multi-Camera Handling', () => {
309
+ it('should handle 10 cameras simultaneously', () => {
310
+ const results = [];
311
+ for (let i = 1; i <= 10; i++) {
312
+ const frame = i % 2 === 0 ? makeMjpegFrame() : makeH264KeyFrame();
313
+ results.push(cam.ingestFrame(`cam${i}`, frame));
314
+ }
315
+ expect(cam.listCameras().length).toBe(10);
316
+ expect(results.every(r => r !== null)).toBe(true);
317
+ });
318
+ it('should keep frame counters independent per camera', () => {
319
+ cam.ingestFrame('cam1', makeMjpegFrame());
320
+ cam.ingestFrame('cam1', makeMjpegFrame());
321
+ cam.ingestFrame('cam2', makeH264KeyFrame());
322
+ expect(cam.getStats('cam1')?.framesReceived).toBe(2);
323
+ expect(cam.getStats('cam2')?.framesReceived).toBe(1);
324
+ });
325
+ it('should emit camera:registered event', (done) => {
326
+ cam.on('camera:registered', ({ cameraId }) => {
327
+ expect(cameraId).toBe('cam-new');
328
+ done();
329
+ });
330
+ cam.registerCamera({ cameraId: 'cam-new' });
331
+ });
332
+ });
333
+ // ============================================
334
+ // Destroy / Cleanup Tests
335
+ // ============================================
336
+ describe('Cleanup', () => {
337
+ it('should clear all state on destroy()', () => {
338
+ cam.registerCamera({ cameraId: 'cam1' });
339
+ cam.ingestFrame('cam1', makeMjpegFrame());
340
+ cam.destroy();
341
+ expect(cam.listCameras().length).toBe(0);
342
+ });
343
+ });
344
+ });
345
+ //# sourceMappingURL=camera.test.js.map