dolphin-server-modules 2.11.1 → 2.11.3
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/TUTORIAL_NEPALI.md +181 -0
- package/dist/adapters/mongoose/index.test.d.ts +1 -0
- package/dist/adapters/mongoose/index.test.js +145 -0
- package/dist/adapters/mongoose/index.test.js.map +1 -0
- package/dist/adapters/mongoose/integration.test.d.ts +5 -0
- package/dist/adapters/mongoose/integration.test.js +217 -0
- package/dist/adapters/mongoose/integration.test.js.map +1 -0
- package/dist/ai/dolphin-agent/agent.d.ts +5 -0
- package/dist/ai/dolphin-agent/agent.js +93 -46
- package/dist/ai/dolphin-agent/agent.js.map +1 -1
- package/dist/ai/dolphin-agent/config.js +19 -23
- package/dist/ai/dolphin-agent/config.js.map +1 -1
- package/dist/auth/auth.test.d.ts +1 -0
- package/dist/auth/auth.test.js +286 -0
- package/dist/auth/auth.test.js.map +1 -0
- package/dist/authController/authController.test.d.ts +1 -0
- package/dist/authController/authController.test.js +359 -0
- package/dist/authController/authController.test.js.map +1 -0
- package/dist/bin/cli.js +494 -131
- package/dist/bin/cli.js.map +1 -1
- package/dist/client.test.d.ts +22 -0
- package/dist/client.test.js +573 -0
- package/dist/client.test.js.map +1 -0
- package/dist/controller/controller.test.d.ts +1 -0
- package/dist/controller/controller.test.js +37 -0
- package/dist/controller/controller.test.js.map +1 -0
- package/dist/curd/crud.test.d.ts +1 -0
- package/dist/curd/crud.test.js +104 -0
- package/dist/curd/crud.test.js.map +1 -0
- package/dist/demo-server.d.ts +1 -0
- package/dist/demo-server.js +191 -0
- package/dist/demo-server.js.map +1 -0
- package/dist/djson/djson.test.d.ts +1 -0
- package/dist/djson/djson.test.js +200 -0
- package/dist/djson/djson.test.js.map +1 -0
- package/dist/dolphin-bench.d.ts +1 -0
- package/dist/dolphin-bench.js +63 -0
- package/dist/dolphin-bench.js.map +1 -0
- package/dist/hard-performance-test.d.ts +1 -0
- package/dist/hard-performance-test.js +97 -0
- package/dist/hard-performance-test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/zod.test.d.ts +1 -0
- package/dist/middleware/zod.test.js +74 -0
- package/dist/middleware/zod.test.js.map +1 -0
- package/dist/performance-test.d.ts +1 -0
- package/dist/performance-test.js +92 -0
- package/dist/performance-test.js.map +1 -0
- package/dist/real-test-mongoose.d.ts +1 -0
- package/dist/real-test-mongoose.js +104 -0
- package/dist/real-test-mongoose.js.map +1 -0
- package/dist/realtime/camera.d.ts +119 -0
- package/dist/realtime/camera.js +299 -0
- package/dist/realtime/camera.js.map +1 -0
- package/dist/realtime/camera.test.d.ts +1 -0
- package/dist/realtime/camera.test.js +345 -0
- package/dist/realtime/camera.test.js.map +1 -0
- package/dist/realtime/core.d.ts +4 -4
- package/dist/realtime/core.js +5 -5
- package/dist/realtime/core.js.map +1 -1
- package/dist/realtime/index.d.ts +2 -0
- package/dist/realtime/index.js +2 -0
- package/dist/realtime/index.js.map +1 -1
- package/dist/realtime/realtime.test.d.ts +1 -0
- package/dist/realtime/realtime.test.js +623 -0
- package/dist/realtime/realtime.test.js.map +1 -0
- package/dist/realtime/rtsp.d.ts +65 -0
- package/dist/realtime/rtsp.js +410 -0
- package/dist/realtime/rtsp.js.map +1 -0
- package/dist/realtime/rtsp.test.d.ts +1 -0
- package/dist/realtime/rtsp.test.js +361 -0
- package/dist/realtime/rtsp.test.js.map +1 -0
- package/dist/router/router.test.d.ts +1 -0
- package/dist/router/router.test.js +45 -0
- package/dist/router/router.test.js.map +1 -0
- package/dist/server/server.d.ts +8 -8
- package/dist/server/server.js +1 -10
- package/dist/server/server.js.map +1 -1
- package/dist/server/server.test.d.ts +1 -0
- package/dist/server/server.test.js +299 -0
- package/dist/server/server.test.js.map +1 -0
- package/dist/services/ai-service.js +22 -11
- package/dist/services/ai-service.js.map +1 -1
- package/dist/signaling/signaling.test.d.ts +1 -0
- package/dist/signaling/signaling.test.js +112 -0
- package/dist/signaling/signaling.test.js.map +1 -0
- package/dist/swagger/swagger.test.d.ts +1 -0
- package/dist/swagger/swagger.test.js +38 -0
- package/dist/swagger/swagger.test.js.map +1 -0
- package/dist/templates/index.d.ts +6 -0
- package/dist/templates/index.js +247 -70
- package/dist/templates/index.js.map +1 -1
- package/dist/test-2fa-real.d.ts +1 -0
- package/dist/test-2fa-real.js +105 -0
- package/dist/test-2fa-real.js.map +1 -0
- package/dist/test-dolphin.d.ts +1 -0
- package/dist/test-dolphin.js +98 -0
- package/dist/test-dolphin.js.map +1 -0
- package/dist/utils/ctx.d.ts +50 -0
- package/dist/utils/ctx.js +82 -0
- package/dist/utils/ctx.js.map +1 -0
- package/package.json +171 -65
- package/scripts/client.js +838 -703
- package/scripts/benchmark.js +0 -12
- package/scripts/benchmark.ts +0 -12
- package/scripts/list-models.js +0 -34
- package/scripts/run-real-ai-test.js +0 -79
- package/scripts/test-ai-logic.js +0 -44
- package/scripts/test-client.js +0 -105
- 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":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAkDtC;;GAEG;AACH,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAErC,8BAA8B;IAC9B,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1D,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,sDAAsD;IACtD,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7E,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1D,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,mDAAmD;IACnD,0DAA0D;IAC1D,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7E,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QACrC,IAAI,OAAO,IAAI,EAAE;YAAE,OAAO,MAAM,CAAC;QACjC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,sEAAsE;IACtE,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,kDAAkD;IAClD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,SAAS,cAAc,CAAC,GAAW;IACjC,sBAAsB;IACtB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7E,MAAM,GAAG,CAAC,CAAC;IACb,CAAC;SAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACjE,MAAM,GAAG,CAAC,CAAC;IACb,CAAC;IACD,IAAI,MAAM,IAAI,GAAG,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACnC,OAAO,OAAO,KAAK,CAAC,CAAC,CAAC,iBAAiB;AACzC,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,GAAW;IACrC,gDAAgD;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvD,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;YACpE,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACvC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACtC,IAAI,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC;gBAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QACxD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,iBAAkB,SAAQ,YAAY;IACzC,EAAE,CAAe;IACjB,OAAO,GAAG,IAAI,GAAG,EAAuC,CAAC;IACzD,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,WAAW,GAAG,IAAI,GAAG,EAAkD,CAAC;IACxE,aAAa,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEzC,kBAAkB,GAAG,KAAK,CAAC,CAAC,yBAAyB;IACrD,aAAa,GAAG,IAAI,CAAC;IAEtC,YAAY,EAAgB;QAC1B,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,IAAmB;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjD,IAAI,QAAQ;YAAE,OAAO,CAAC,qBAAqB;QAE3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC9B,GAAG,IAAI;YACP,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,SAAS;YAC9B,cAAc,EAAE,CAAC;YACjB,aAAa,EAAE,CAAC;YAChB,GAAG,EAAE,CAAC;YACN,WAAW,EAAE,CAAC;YACd,KAAK,EAAE,IAAI,CAAC,aAAa,IAAI,CAAC;YAC9B,MAAM,EAAE,IAAI,CAAC,cAAc,IAAI,CAAC;YAChC,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAE3E,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,QAAgB,EAAE,SAAiB;QAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEvE,6BAA6B;QAC7B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpC,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEvB,qCAAqC;QACrC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAE3E,qBAAqB;QACrB,IAAI,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;QACtB,IAAI,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACxB,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YACvD,MAAM,GAAG,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;YAC1C,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC;YAC3B,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC;QAChC,CAAC;QAED,kBAAkB;QAClB,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,IAAI,KAAK,KAAK,OAAO;YAAE,UAAU,GAAG,IAAI,CAAC,CAAC,oCAAoC;QAC9E,IAAI,KAAK,KAAK,MAAM;YAAE,UAAU,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;QAE7D,qBAAqB;QACrB,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/D,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAE7C,qBAAqB;QACrB,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC;QAChD,OAAO,CAAC,KAAK,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC;QAC1C,IAAI,OAAO,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YAClC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC;YAClB,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC;QAC5B,CAAC;QAED,eAAe;QACf,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;QAClB,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;QAClB,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC;QACpB,GAAG,CAAC,cAAc,GAAG,UAAU,CAAC;QAChC,GAAG,CAAC,aAAa,IAAI,SAAS,CAAC,MAAM,CAAC;QACtC,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC;QACtB,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC;QAEpB,qBAAqB;QACrB,MAAM,KAAK,GAAgB;YACzB,QAAQ;YACR,KAAK;YACL,KAAK;YACL,MAAM;YACN,SAAS,EAAE,GAAG;YACd,UAAU;YACV,UAAU;YACV,IAAI,EAAE,SAAS;YACf,SAAS,EAAE,SAAS,CAAC,MAAM;SAC5B,CAAC;QAEF,uDAAuD;QACvD,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE,SAAS,CAAC,CAAC;QAEvD,+DAA+D;QAC/D,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,QAAQ,OAAO,EAAE;YACzC,QAAQ;YACR,KAAK;YACL,KAAK;YACL,MAAM;YACN,SAAS,EAAE,GAAG;YACd,UAAU;YACV,UAAU;YACV,SAAS,EAAE,SAAS,CAAC,MAAM;YAC3B,GAAG,EAAE,GAAG,CAAC,GAAG;SACb,CAAC,CAAC;QAEH,eAAe;QACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,CAAC,SAAS,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC;QAEtC,sBAAsB;QACtB,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAElC,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,QAAgB,EAAE,EAAgC;QAC1D,IAAI,CAAC,EAAE,CAAC,SAAS,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,EAAgC;QAC3C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACvB,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,QAAgB,EAAE,EAAgC;QAC5D,IAAI,CAAC,GAAG,CAAC,SAAS,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,QAAgB;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO;YACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,cAAc,EAAE,GAAG,CAAC,cAAc;YAClC,aAAa,EAAE,GAAG,CAAC,aAAa;YAChC,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;SACvB,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,WAAW;QACT,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAE,CAAC,CAAC;IACvE,CAAC;IAED;;OAEG;IACH,WAAW;QACT,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,QAAgB;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,GAAG,EAAE,CAAC;YACR,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,QAAQ,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,QAAgB;QAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9B,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/C,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,OAAO;QACL,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACrE,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;IAEO,kBAAkB,CAAC,QAAgB;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClD,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QAErC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAE5B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC1C,CAAC;CACF;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAgB;IACjD,OAAO,IAAI,iBAAiB,CAAC,EAAE,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -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
|