matrice-streaming 0.1.14__py3-none-any.whl → 0.1.65__py3-none-any.whl

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 (38) hide show
  1. matrice_streaming/__init__.py +44 -32
  2. matrice_streaming/streaming_gateway/camera_streamer/__init__.py +68 -1
  3. matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +1388 -0
  4. matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +966 -0
  5. matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +188 -24
  6. matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +507 -0
  7. matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +136 -0
  8. matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +1048 -0
  9. matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +192 -0
  10. matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +470 -0
  11. matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +1368 -0
  12. matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +1063 -0
  13. matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +546 -0
  14. matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +60 -15
  15. matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +1330 -0
  16. matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +412 -0
  17. matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +680 -0
  18. matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +111 -4
  19. matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +223 -27
  20. matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +694 -0
  21. matrice_streaming/streaming_gateway/debug/__init__.py +27 -2
  22. matrice_streaming/streaming_gateway/debug/benchmark.py +727 -0
  23. matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +599 -0
  24. matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +245 -95
  25. matrice_streaming/streaming_gateway/debug/debug_utils.py +29 -0
  26. matrice_streaming/streaming_gateway/debug/test_videoplayback.py +318 -0
  27. matrice_streaming/streaming_gateway/dynamic_camera_manager.py +656 -39
  28. matrice_streaming/streaming_gateway/metrics_reporter.py +676 -139
  29. matrice_streaming/streaming_gateway/streaming_action.py +71 -20
  30. matrice_streaming/streaming_gateway/streaming_gateway.py +1026 -78
  31. matrice_streaming/streaming_gateway/streaming_gateway_utils.py +175 -20
  32. matrice_streaming/streaming_gateway/streaming_status_listener.py +89 -0
  33. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/METADATA +1 -1
  34. matrice_streaming-0.1.65.dist-info/RECORD +56 -0
  35. matrice_streaming-0.1.14.dist-info/RECORD +0 -38
  36. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/WHEEL +0 -0
  37. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/licenses/LICENSE.txt +0 -0
  38. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/top_level.txt +0 -0
@@ -22,8 +22,9 @@ class MetricsConfig:
22
22
 
23
23
  # Collection and reporting intervals
24
24
  collection_interval: float = 1.0 # Collect metrics every second
25
- reporting_interval: float = 60.0 # Report aggregated metrics every 60 seconds
26
- history_window: int = 60 # Keep 60 seconds of history for statistics
25
+ reporting_interval: float = 30.0 # Report aggregated metrics every 30 seconds
26
+ history_window: int = 30 # Keep 30 seconds of history for statistics
27
+ log_interval: float = 300.0 # Log metrics sends every 5 minutes
27
28
 
28
29
  # Kafka configuration
29
30
  metrics_topic: str = "streaming_gateway_metrics"
@@ -113,6 +114,9 @@ class MetricsCollector:
113
114
  # Track frame counts for FPS calculation
114
115
  self.camera_frame_counts: Dict[str, List[tuple]] = {} # camera_id -> [(timestamp, count)]
115
116
 
117
+ # Track which flow is being used
118
+ self.use_async_workers = getattr(streaming_gateway, 'use_async_workers', False)
119
+
116
120
  def collect_snapshot(self) -> Dict[str, Any]:
117
121
  """
118
122
  Collect current metrics snapshot from streaming gateway.
@@ -125,50 +129,111 @@ class MetricsCollector:
125
129
  # Get overall statistics from streaming gateway
126
130
  gateway_stats = self.streaming_gateway.get_statistics()
127
131
 
128
- # Get camera streamer for detailed metrics
129
- camera_streamer = self.streaming_gateway.camera_streamer
130
- if not camera_streamer:
131
- return None
132
-
133
- # Collect per-camera metrics
134
- camera_metrics = {}
135
-
136
- # Get active stream keys
137
- stream_keys = gateway_stats.get("my_stream_keys", [])
138
-
139
- for stream_key in stream_keys:
140
- # Get timing statistics for this stream
141
- timing = camera_streamer.statistics.get_timing_stats(stream_key)
142
-
143
- if timing:
144
- # Extract camera_id from stream_key
145
- # Stream keys are in format like "camera_id_suffix"
146
- camera_id = stream_key.split('_')[0] if '_' in stream_key else stream_key
147
-
148
- camera_metrics[camera_id] = {
149
- "stream_key": stream_key,
150
- "read_time": timing.get("last_read_time_sec", 0.0), # Camera reading latency
151
- "write_time": timing.get("last_write_time_sec", 0.0), # Gateway sending latency
152
- "process_time": timing.get("last_process_time_sec", 0.0), # Total processing time
153
- "frame_size": timing.get("last_frame_size_bytes", 0), # ACG frame size in bytes
154
- }
155
-
156
- # Get transmission stats for frame counts
157
- transmission_stats = gateway_stats.get("transmission_stats", {})
158
-
159
- snapshot = {
160
- "timestamp": time.time(),
161
- "cameras": camera_metrics,
162
- "frames_sent": transmission_stats.get("frames_sent_full", 0),
163
- "total_frames_processed": transmission_stats.get("total_frames_processed", 0),
164
- }
165
-
166
- return snapshot
132
+ # Route to appropriate collection method based on flow
133
+ if self.use_async_workers:
134
+ return self._collect_async_worker_snapshot(gateway_stats)
135
+ else:
136
+ return self._collect_camera_streamer_snapshot(gateway_stats)
167
137
 
168
138
  except Exception as e:
169
139
  logging.error(f"Error collecting metrics snapshot: {e}", exc_info=True)
170
140
  return None
171
141
 
142
+ def _collect_camera_streamer_snapshot(self, gateway_stats: Dict[str, Any]) -> Optional[Dict[str, Any]]:
143
+ """Collect metrics from original CameraStreamer flow."""
144
+ # Get camera streamer for detailed metrics
145
+ camera_streamer = self.streaming_gateway.camera_streamer
146
+ if not camera_streamer:
147
+ return None
148
+
149
+ # Collect per-camera metrics
150
+ camera_metrics = {}
151
+
152
+ # Get active stream keys
153
+ stream_keys = gateway_stats.get("my_stream_keys", [])
154
+
155
+ for stream_key in stream_keys:
156
+ # Get timing statistics for this stream
157
+ timing = camera_streamer.statistics.get_timing_stats(stream_key)
158
+
159
+ if timing:
160
+ # Get camera_id from the streaming gateway mapping
161
+ camera_id = self.streaming_gateway.get_camera_id_for_stream_key(stream_key)
162
+ if not camera_id:
163
+ # Fallback: try to extract from stream_key if mapping not available
164
+ camera_id = stream_key.split('_')[0] if '_' in stream_key else stream_key
165
+
166
+ camera_metrics[camera_id] = {
167
+ "stream_key": stream_key,
168
+ "read_time": timing.get("last_read_time_sec", 0.0), # Camera reading latency
169
+ "write_time": timing.get("last_write_time_sec", 0.0), # Gateway sending latency
170
+ "process_time": timing.get("last_process_time_sec", 0.0), # Total processing time
171
+ "frame_size": timing.get("last_frame_size_bytes", 0), # ACG frame size in bytes
172
+ }
173
+
174
+ # Get transmission stats for frame counts
175
+ transmission_stats = gateway_stats.get("transmission_stats", {})
176
+
177
+ snapshot = {
178
+ "timestamp": time.time(),
179
+ "cameras": camera_metrics,
180
+ "frames_sent": transmission_stats.get("frames_sent_full", 0),
181
+ "total_frames_processed": transmission_stats.get("total_frames_processed", 0),
182
+ "flow_type": "camera_streamer",
183
+ }
184
+
185
+ return snapshot
186
+
187
+ def _collect_async_worker_snapshot(self, gateway_stats: Dict[str, Any]) -> Optional[Dict[str, Any]]:
188
+ """Collect metrics from new async worker flow."""
189
+ worker_manager = self.streaming_gateway.worker_manager
190
+ if not worker_manager:
191
+ return None
192
+
193
+ # Collect per-camera metrics from worker statistics
194
+ camera_metrics = {}
195
+
196
+ # Get active stream keys
197
+ stream_keys = gateway_stats.get("my_stream_keys", [])
198
+
199
+ # Get worker statistics
200
+ worker_stats = gateway_stats.get("worker_stats", {})
201
+ health_reports = worker_stats.get("health_reports", {})
202
+
203
+ for stream_key in stream_keys:
204
+ # Get camera_id from the streaming gateway mapping
205
+ camera_id = self.streaming_gateway.get_camera_id_for_stream_key(stream_key)
206
+ if not camera_id:
207
+ camera_id = stream_key.split('_')[0] if '_' in stream_key else stream_key
208
+
209
+ # For async workers, we track basic info (detailed timing not yet available)
210
+ camera_metrics[camera_id] = {
211
+ "stream_key": stream_key,
212
+ "read_time": 0.0, # Not tracked per-camera in async flow yet
213
+ "write_time": 0.0, # Not tracked per-camera in async flow yet
214
+ "process_time": 0.0, # Not tracked per-camera in async flow yet
215
+ "frame_size": 0, # Not tracked per-camera in async flow yet
216
+ }
217
+
218
+ # Calculate aggregate stats from worker health reports
219
+ total_cameras = worker_stats.get("total_cameras", len(stream_keys))
220
+ running_workers = worker_stats.get("running_workers", 0)
221
+
222
+ snapshot = {
223
+ "timestamp": time.time(),
224
+ "cameras": camera_metrics,
225
+ "frames_sent": 0, # Not tracked in async flow yet
226
+ "total_frames_processed": 0, # Not tracked in async flow yet
227
+ "flow_type": "async_workers",
228
+ "worker_stats": {
229
+ "num_workers": worker_stats.get("num_workers", 0),
230
+ "running_workers": running_workers,
231
+ "total_cameras": total_cameras,
232
+ },
233
+ }
234
+
235
+ return snapshot
236
+
172
237
  def add_to_history(self, snapshot: Dict[str, Any]):
173
238
  """
174
239
  Add snapshot to rolling history window.
@@ -201,102 +266,212 @@ class MetricsCollector:
201
266
  return None
202
267
 
203
268
  try:
204
- # Get camera streamer for accessing timing statistics
205
- camera_streamer = self.streaming_gateway.camera_streamer
206
- if not camera_streamer:
207
- return None
269
+ # Route to appropriate aggregation method based on flow
270
+ if self.use_async_workers:
271
+ return self._get_async_worker_aggregated_metrics()
272
+ else:
273
+ return self._get_camera_streamer_aggregated_metrics()
208
274
 
209
- # Get active stream keys from the most recent snapshot
210
- stream_keys = set()
211
- for snapshot in self.metrics_history:
212
- for camera_id, metrics in snapshot.get("cameras", {}).items():
213
- stream_keys.add(metrics.get("stream_key"))
275
+ except Exception as e:
276
+ logging.error(f"Error calculating aggregated metrics: {e}", exc_info=True)
277
+ return None
214
278
 
215
- # Calculate statistics for each stream using accumulated history
216
- per_camera_metrics = []
279
+ def _get_camera_streamer_aggregated_metrics(self) -> Optional[List[Dict[str, Any]]]:
280
+ """Get aggregated metrics for CameraStreamer flow."""
281
+ # Get camera streamer for accessing timing statistics
282
+ camera_streamer = self.streaming_gateway.camera_streamer
283
+ if not camera_streamer:
284
+ return None
285
+
286
+ # Get active stream keys from the most recent snapshot
287
+ stream_keys = set()
288
+ for snapshot in self.metrics_history:
289
+ for camera_id, metrics in snapshot.get("cameras", {}).items():
290
+ stream_keys.add(metrics.get("stream_key"))
291
+
292
+ # Calculate statistics for each stream using accumulated history
293
+ per_camera_metrics = []
294
+
295
+ for stream_key in stream_keys:
296
+ if not stream_key:
297
+ continue
298
+
299
+ # Get real statistics from accumulated timing history
300
+ stats = camera_streamer.statistics.get_timing_statistics(stream_key)
301
+
302
+ if not stats:
303
+ continue
304
+
305
+ # Get camera_id from the streaming gateway mapping
306
+ camera_id = self.streaming_gateway.get_camera_id_for_stream_key(stream_key)
307
+ if not camera_id:
308
+ # Fallback: try to extract from stream_key if mapping not available
309
+ camera_id = stream_key.split('_')[0] if '_' in stream_key else stream_key
310
+
311
+ # Get read time statistics (already in milliseconds)
312
+ read_time_ms = stats.get("read_time_ms", {})
313
+ read_stats = {
314
+ "min": read_time_ms.get("min", 0.0),
315
+ "max": read_time_ms.get("max", 0.0),
316
+ "avg": read_time_ms.get("avg", 0.0),
317
+ "p0": read_time_ms.get("min", 0.0),
318
+ "p50": read_time_ms.get("avg", 0.0), # Use avg as approximation for median
319
+ "p100": read_time_ms.get("max", 0.0),
320
+ "unit": "ms"
321
+ }
217
322
 
218
- for stream_key in stream_keys:
219
- if not stream_key:
220
- continue
323
+ # Get write time statistics (already in milliseconds)
324
+ write_time_ms = stats.get("write_time_ms", {})
325
+ write_stats = {
326
+ "min": write_time_ms.get("min", 0.0),
327
+ "max": write_time_ms.get("max", 0.0),
328
+ "avg": write_time_ms.get("avg", 0.0),
329
+ "p0": write_time_ms.get("min", 0.0),
330
+ "p50": write_time_ms.get("avg", 0.0),
331
+ "p100": write_time_ms.get("max", 0.0),
332
+ "unit": "ms"
333
+ }
221
334
 
222
- # Get real statistics from accumulated timing history
223
- stats = camera_streamer.statistics.get_timing_statistics(stream_key)
335
+ # Get FPS statistics (real calculations from timestamps)
336
+ fps_data = stats.get("fps", {})
337
+ fps_stats = {
338
+ "min": fps_data.get("min", 0.0),
339
+ "max": fps_data.get("max", 0.0),
340
+ "avg": fps_data.get("avg", 0.0),
341
+ "p0": fps_data.get("min", 0.0),
342
+ "p50": fps_data.get("avg", 0.0),
343
+ "p100": fps_data.get("max", 0.0),
344
+ "unit": "fps"
345
+ }
224
346
 
225
- if not stats:
226
- continue
347
+ # Get frame size statistics
348
+ frame_size_data = stats.get("frame_size_bytes", {})
349
+ frame_size_stats = {
350
+ "min": frame_size_data.get("min", 0.0),
351
+ "max": frame_size_data.get("max", 0.0),
352
+ "avg": frame_size_data.get("avg", 0.0),
353
+ "p0": frame_size_data.get("min", 0.0),
354
+ "p50": frame_size_data.get("avg", 0.0),
355
+ "p100": frame_size_data.get("max", 0.0),
356
+ "unit": "bytes"
357
+ }
227
358
 
228
- # Extract camera_id from stream_key
229
- camera_id = stream_key.split('_')[0] if '_' in stream_key else stream_key
359
+ # Build camera metrics in the required format
360
+ camera_metric = {
361
+ "camera_id": camera_id,
362
+ "camera_reading": {
363
+ "throughput": fps_stats,
364
+ "latency": read_stats
365
+ },
366
+ "gateway_sending": {
367
+ "throughput": fps_stats, # Same as camera reading
368
+ "latency": write_stats
369
+ },
370
+ "frame_size_stats": frame_size_stats
371
+ }
230
372
 
231
- # Get read time statistics (already in milliseconds)
232
- read_time_ms = stats.get("read_time_ms", {})
233
- read_stats = {
234
- "min": read_time_ms.get("min", 0.0),
235
- "max": read_time_ms.get("max", 0.0),
236
- "avg": read_time_ms.get("avg", 0.0),
237
- "p0": read_time_ms.get("min", 0.0),
238
- "p50": read_time_ms.get("avg", 0.0), # Use avg as approximation for median
239
- "p100": read_time_ms.get("max", 0.0),
240
- "unit": "ms"
241
- }
242
-
243
- # Get write time statistics (already in milliseconds)
244
- write_time_ms = stats.get("write_time_ms", {})
245
- write_stats = {
246
- "min": write_time_ms.get("min", 0.0),
247
- "max": write_time_ms.get("max", 0.0),
248
- "avg": write_time_ms.get("avg", 0.0),
249
- "p0": write_time_ms.get("min", 0.0),
250
- "p50": write_time_ms.get("avg", 0.0),
251
- "p100": write_time_ms.get("max", 0.0),
252
- "unit": "ms"
253
- }
254
-
255
- # Get FPS statistics (real calculations from timestamps)
256
- fps_data = stats.get("fps", {})
257
- fps_stats = {
258
- "min": fps_data.get("min", 0.0),
259
- "max": fps_data.get("max", 0.0),
260
- "avg": fps_data.get("avg", 0.0),
261
- "p0": fps_data.get("min", 0.0),
262
- "p50": fps_data.get("avg", 0.0),
263
- "p100": fps_data.get("max", 0.0),
264
- "unit": "fps"
265
- }
266
-
267
- # Get frame size statistics
268
- frame_size_data = stats.get("frame_size_bytes", {})
269
- frame_size_stats = {
270
- "min": frame_size_data.get("min", 0.0),
271
- "max": frame_size_data.get("max", 0.0),
272
- "avg": frame_size_data.get("avg", 0.0),
273
- "p0": frame_size_data.get("min", 0.0),
274
- "p50": frame_size_data.get("avg", 0.0),
275
- "p100": frame_size_data.get("max", 0.0),
276
- "unit": "bytes"
277
- }
278
-
279
- # Build camera metrics in the required format
280
- camera_metric = {
281
- "camera_id": camera_id,
282
- "camera_reading": {
283
- "throughput": fps_stats,
284
- "latency": read_stats
285
- },
286
- "gateway_sending": {
287
- "throughput": fps_stats, # Same as camera reading
288
- "latency": write_stats
289
- },
290
- "acg_frame_size": frame_size_stats
291
- }
292
-
293
- per_camera_metrics.append(camera_metric)
294
-
295
- return per_camera_metrics
373
+ per_camera_metrics.append(camera_metric)
374
+
375
+ return per_camera_metrics
376
+
377
+ def _get_async_worker_aggregated_metrics(self) -> Optional[List[Dict[str, Any]]]:
378
+ """Get aggregated metrics for async worker flow."""
379
+ worker_manager = self.streaming_gateway.worker_manager
380
+ if not worker_manager:
381
+ return None
382
+
383
+ # Get active stream keys from the most recent snapshot
384
+ stream_keys = set()
385
+ for snapshot in self.metrics_history:
386
+ for camera_id, metrics in snapshot.get("cameras", {}).items():
387
+ stream_keys.add(metrics.get("stream_key"))
388
+
389
+ # Get worker statistics (includes per_camera_stats from health reports)
390
+ gateway_stats = self.streaming_gateway.get_statistics()
391
+ worker_stats = gateway_stats.get("worker_stats", {})
392
+ per_camera_stats = worker_stats.get("per_camera_stats", {})
393
+
394
+ # Build per-camera metrics using real stats from workers
395
+ per_camera_metrics = []
396
+
397
+ for stream_key in stream_keys:
398
+ if not stream_key:
399
+ continue
400
+
401
+ # Get camera_id from the streaming gateway mapping
402
+ camera_id = self.streaming_gateway.get_camera_id_for_stream_key(stream_key)
403
+ if not camera_id:
404
+ camera_id = stream_key.split('_')[0] if '_' in stream_key else stream_key
405
+
406
+ # Get real stats from worker health reports if available
407
+ camera_stats = per_camera_stats.get(stream_key, {})
408
+
409
+ # Build FPS stats from worker data
410
+ fps_data = camera_stats.get('fps', {})
411
+ fps_stats = {
412
+ "min": fps_data.get("min", 0.0),
413
+ "max": fps_data.get("max", 0.0),
414
+ "avg": fps_data.get("avg", 0.0),
415
+ "p0": fps_data.get("min", 0.0),
416
+ "p50": fps_data.get("avg", 0.0),
417
+ "p100": fps_data.get("max", 0.0),
418
+ "unit": "fps"
419
+ }
296
420
 
297
- except Exception as e:
298
- logging.error(f"Error calculating aggregated metrics: {e}", exc_info=True)
299
- return None
421
+ # Build read latency stats (already in ms from worker)
422
+ read_time_ms = camera_stats.get('read_time_ms', {})
423
+ read_stats = {
424
+ "min": read_time_ms.get("min", 0.0),
425
+ "max": read_time_ms.get("max", 0.0),
426
+ "avg": read_time_ms.get("avg", 0.0),
427
+ "p0": read_time_ms.get("min", 0.0),
428
+ "p50": read_time_ms.get("avg", 0.0),
429
+ "p100": read_time_ms.get("max", 0.0),
430
+ "unit": "ms"
431
+ }
432
+
433
+ # Build write latency stats (already in ms from worker)
434
+ write_time_ms = camera_stats.get('write_time_ms', {})
435
+ write_stats = {
436
+ "min": write_time_ms.get("min", 0.0),
437
+ "max": write_time_ms.get("max", 0.0),
438
+ "avg": write_time_ms.get("avg", 0.0),
439
+ "p0": write_time_ms.get("min", 0.0),
440
+ "p50": write_time_ms.get("avg", 0.0),
441
+ "p100": write_time_ms.get("max", 0.0),
442
+ "unit": "ms"
443
+ }
444
+
445
+ # Build frame size stats (in bytes from worker)
446
+ frame_size_data = camera_stats.get('frame_size_bytes', {})
447
+ frame_size_stats = {
448
+ "min": frame_size_data.get("min", 0.0),
449
+ "max": frame_size_data.get("max", 0.0),
450
+ "avg": frame_size_data.get("avg", 0.0),
451
+ "p0": frame_size_data.get("min", 0.0),
452
+ "p50": frame_size_data.get("avg", 0.0),
453
+ "p100": frame_size_data.get("max", 0.0),
454
+ "unit": "bytes"
455
+ }
456
+
457
+ # Build camera metrics with real data
458
+ camera_metric = {
459
+ "camera_id": camera_id,
460
+ "camera_reading": {
461
+ "throughput": fps_stats,
462
+ "latency": read_stats
463
+ },
464
+ "gateway_sending": {
465
+ "throughput": fps_stats, # Same throughput for gateway sending
466
+ "latency": write_stats
467
+ },
468
+ "frame_size_stats": frame_size_stats,
469
+ "flow_type": "async_workers"
470
+ }
471
+
472
+ per_camera_metrics.append(camera_metric)
473
+
474
+ return per_camera_metrics
300
475
 
301
476
 
302
477
  class MetricsReporter:
@@ -390,7 +565,7 @@ class MetricsReporter:
390
565
  # Wait for send to complete with timeout
391
566
  future.get(timeout=self.config.kafka_timeout)
392
567
 
393
- logging.info(f"Metrics sent to Kafka topic '{self.config.metrics_topic}'")
568
+ # Logging is handled by MetricsManager to avoid excessive logs
394
569
  return True
395
570
 
396
571
  except Exception as e:
@@ -532,6 +707,10 @@ class MetricsManager:
532
707
  calculates statistics, and reports them via Kafka.
533
708
  """
534
709
 
710
+ # ANSI escape codes for BOLD text in terminal
711
+ BOLD = "\033[1m"
712
+ RESET = "\033[0m"
713
+
535
714
  def __init__(
536
715
  self,
537
716
  streaming_gateway,
@@ -560,11 +739,20 @@ class MetricsManager:
560
739
  self.collector = MetricsCollector(streaming_gateway, self.config)
561
740
  self.reporter = MetricsReporter(session, streaming_gateway_id, self.config)
562
741
 
742
+ # Track which flow is being used
743
+ self.use_async_workers = getattr(streaming_gateway, 'use_async_workers', False)
744
+
563
745
  # Tracking
564
746
  self.last_report_time = 0
747
+ self.last_log_time = 0
748
+ self.last_metrics_log_time = 0
749
+ self.metrics_log_interval = 60.0 # Log metrics summary every 60 seconds
750
+ self.last_aggregate_log_time = 0
751
+ self.aggregate_log_interval = 60.0 # Log aggregate metrics with BOLD every 60 seconds
565
752
  self.enabled = True
566
753
 
567
- logging.info("Metrics manager initialized")
754
+ flow_type = "async_workers" if self.use_async_workers else "camera_streamer"
755
+ logging.info(f"Metrics manager initialized (flow: {flow_type})")
568
756
 
569
757
  def collect_and_report(self):
570
758
  """
@@ -582,8 +770,19 @@ class MetricsManager:
582
770
  if snapshot:
583
771
  self.collector.add_to_history(snapshot)
584
772
 
585
- # Report if interval has elapsed
586
773
  current_time = time.time()
774
+
775
+ # Log metrics summary periodically
776
+ if current_time - self.last_metrics_log_time >= self.metrics_log_interval:
777
+ self._log_metrics_summary(snapshot)
778
+ self.last_metrics_log_time = current_time
779
+
780
+ # Log aggregate metrics with BOLD every minute
781
+ if current_time - self.last_aggregate_log_time >= self.aggregate_log_interval:
782
+ self._log_aggregate_metrics_bold()
783
+ self.last_aggregate_log_time = current_time
784
+
785
+ # Report if interval has elapsed
587
786
  if current_time - self.last_report_time >= self.config.reporting_interval:
588
787
  self._generate_and_send_report()
589
788
  self.last_report_time = current_time
@@ -591,6 +790,331 @@ class MetricsManager:
591
790
  except Exception as e:
592
791
  logging.error(f"Error in metrics collect_and_report: {e}", exc_info=True)
593
792
 
793
+ def _log_metrics_summary(self, snapshot: Optional[Dict[str, Any]]):
794
+ """Log a summary of current metrics to console."""
795
+ try:
796
+ gateway_stats = self.streaming_gateway.get_statistics()
797
+
798
+ if self.use_async_workers:
799
+ self._log_async_worker_metrics(gateway_stats, snapshot)
800
+ else:
801
+ self._log_camera_streamer_metrics(gateway_stats, snapshot)
802
+
803
+ except Exception as e:
804
+ logging.warning(f"Error logging metrics summary: {e}")
805
+
806
+ def _log_aggregate_metrics_bold(self):
807
+ """Log comprehensive aggregate metrics with BOLD formatting every minute.
808
+
809
+ Reports:
810
+ - Overall AVG FPS across all cameras
811
+ - Latency breakdown (read, encode/convert, write averages)
812
+ - Total throughput (sum of all cameras' FPS)
813
+ - Total data throughput (KB/s)
814
+ """
815
+ try:
816
+ gateway_stats = self.streaming_gateway.get_statistics()
817
+
818
+ if self.use_async_workers:
819
+ self._log_aggregate_async_workers_bold(gateway_stats)
820
+ else:
821
+ self._log_aggregate_camera_streamer_bold(gateway_stats)
822
+
823
+ except Exception as e:
824
+ logging.warning(f"Error logging aggregate metrics: {e}")
825
+
826
+ def _log_aggregate_async_workers_bold(self, gateway_stats: Dict[str, Any]):
827
+ """Log aggregate metrics for async workers with BOLD formatting."""
828
+ worker_stats = gateway_stats.get("worker_stats", {})
829
+ stream_keys = gateway_stats.get("my_stream_keys", [])
830
+ runtime = gateway_stats.get("runtime_seconds", 0)
831
+
832
+ num_workers = worker_stats.get("num_workers", 0)
833
+ running_workers = worker_stats.get("running_workers", 0)
834
+ total_cameras = worker_stats.get("total_cameras", len(stream_keys))
835
+ per_camera_stats = worker_stats.get("per_camera_stats", {})
836
+
837
+ # Detect SHM mode from worker_manager
838
+ use_shm = False
839
+ shm_format = "N/A"
840
+ worker_manager = getattr(self.streaming_gateway, 'worker_manager', None)
841
+ if worker_manager:
842
+ use_shm = getattr(worker_manager, 'use_shm', False)
843
+ shm_format = getattr(worker_manager, 'shm_frame_format', 'N/A') if use_shm else "N/A"
844
+
845
+ # Aggregate FPS stats
846
+ total_fps = 0.0
847
+ fps_values = []
848
+ for stream_key, stats in per_camera_stats.items():
849
+ fps_avg = stats.get("fps", {}).get("avg", 0)
850
+ if fps_avg > 0:
851
+ total_fps += fps_avg
852
+ fps_values.append(fps_avg)
853
+
854
+ avg_fps = sum(fps_values) / len(fps_values) if fps_values else 0
855
+ min_fps = min(fps_values) if fps_values else 0
856
+ max_fps = max(fps_values) if fps_values else 0
857
+
858
+ # Aggregate latency stats (in ms)
859
+ read_times = []
860
+ write_times = []
861
+ encoding_times = []
862
+
863
+ for stream_key, stats in per_camera_stats.items():
864
+ read_ms = stats.get("read_time_ms", {}).get("avg", 0)
865
+ write_ms = stats.get("write_time_ms", {}).get("avg", 0)
866
+ encoding_ms = stats.get("encoding_time_ms", {}).get("avg", 0)
867
+
868
+ if read_ms > 0:
869
+ read_times.append(read_ms)
870
+ if write_ms > 0:
871
+ write_times.append(write_ms)
872
+ if encoding_ms > 0:
873
+ encoding_times.append(encoding_ms)
874
+
875
+ avg_read_ms = sum(read_times) / len(read_times) if read_times else 0
876
+ avg_write_ms = sum(write_times) / len(write_times) if write_times else 0
877
+ avg_encoding_ms = sum(encoding_times) / len(encoding_times) if encoding_times else 0
878
+ total_latency_ms = avg_read_ms + avg_encoding_ms + avg_write_ms
879
+
880
+ # Aggregate frame size and throughput
881
+ total_frame_size_kb = 0.0
882
+ frame_sizes = []
883
+ for stream_key, stats in per_camera_stats.items():
884
+ frame_size_bytes = stats.get("frame_size_bytes", {}).get("avg", 0)
885
+ if frame_size_bytes > 0:
886
+ frame_sizes.append(frame_size_bytes)
887
+ total_frame_size_kb += frame_size_bytes / 1024
888
+
889
+ avg_frame_size_kb = (sum(frame_sizes) / len(frame_sizes) / 1024) if frame_sizes else 0
890
+
891
+ # Total throughput: sum of (FPS * frame_size) across all cameras = total KB/s
892
+ total_throughput_kbps = 0.0
893
+ for stream_key, stats in per_camera_stats.items():
894
+ fps_avg = stats.get("fps", {}).get("avg", 0)
895
+ frame_size_bytes = stats.get("frame_size_bytes", {}).get("avg", 0)
896
+ if fps_avg > 0 and frame_size_bytes > 0:
897
+ total_throughput_kbps += (fps_avg * frame_size_bytes) / 1024
898
+
899
+ total_throughput_mbps = total_throughput_kbps / 1024
900
+
901
+ # Log with BOLD formatting
902
+ B = self.BOLD
903
+ R = self.RESET
904
+
905
+ # Mode indicator
906
+ mode_str = f"SHM ({shm_format})" if use_shm else "JPEG"
907
+ encode_label = "Convert" if use_shm else "Encode"
908
+
909
+ logging.info(
910
+ f"\n{B}{'='*80}{R}\n"
911
+ f"{B}[STREAMING GATEWAY AGGREGATE METRICS - 1 MIN SUMMARY]{R}\n"
912
+ f"{B}{'='*80}{R}\n"
913
+ f"{B}Mode:{R} {mode_str} | "
914
+ f"{B}Workers:{R} {running_workers}/{num_workers} active | "
915
+ f"{B}Cameras:{R} {total_cameras} streaming | "
916
+ f"{B}Runtime:{R} {runtime:.0f}s\n"
917
+ f"{B}{'─'*80}{R}\n"
918
+ f"{B}FPS:{R} avg={avg_fps:.1f} | min={min_fps:.1f} | max={max_fps:.1f} | "
919
+ f"{B}TOTAL={total_fps:.1f} fps{R}\n"
920
+ f"{B}{'─'*80}{R}\n"
921
+ f"{B}LATENCY BREAKDOWN:{R}\n"
922
+ f" • Read: {avg_read_ms:.2f} ms (avg)\n"
923
+ f" • {encode_label}: {avg_encoding_ms:.2f} ms (avg)\n"
924
+ f" • Write: {avg_write_ms:.2f} ms (avg)\n"
925
+ f" • {B}TOTAL: {total_latency_ms:.2f} ms{R}\n"
926
+ f"{B}{'─'*80}{R}\n"
927
+ f"{B}THROUGHPUT:{R}\n"
928
+ f" • Avg Frame Size: {avg_frame_size_kb:.1f} KB\n"
929
+ f" • {B}TOTAL: {total_throughput_mbps:.2f} MB/s ({total_throughput_kbps:.1f} KB/s){R}\n"
930
+ f"{B}{'='*80}{R}"
931
+ )
932
+
933
+ def _log_aggregate_camera_streamer_bold(self, gateway_stats: Dict[str, Any]):
934
+ """Log aggregate metrics for CameraStreamer with BOLD formatting."""
935
+ camera_streamer = self.streaming_gateway.camera_streamer
936
+ if not camera_streamer:
937
+ return
938
+
939
+ stream_keys = gateway_stats.get("my_stream_keys", [])
940
+ transmission_stats = gateway_stats.get("transmission_stats", {})
941
+ runtime = gateway_stats.get("runtime_seconds", 0)
942
+
943
+ # Aggregate FPS stats
944
+ fps_values = []
945
+ read_times = []
946
+ write_times = []
947
+ encoding_times = []
948
+ frame_sizes = []
949
+
950
+ # Calculate total throughput properly per-camera
951
+ total_throughput_kbps = 0.0
952
+
953
+ for stream_key in stream_keys:
954
+ timing_stats = camera_streamer.statistics.get_timing_statistics(stream_key)
955
+ if timing_stats:
956
+ fps_avg = timing_stats.get("fps", {}).get("avg", 0)
957
+ if fps_avg > 0:
958
+ fps_values.append(fps_avg)
959
+
960
+ read_ms = timing_stats.get("read_time_ms", {}).get("avg", 0)
961
+ write_ms = timing_stats.get("write_time_ms", {}).get("avg", 0)
962
+ encoding_ms = timing_stats.get("encoding_time_ms", {}).get("avg", 0)
963
+ frame_size_bytes = timing_stats.get("frame_size_bytes", {}).get("avg", 0)
964
+
965
+ if read_ms > 0:
966
+ read_times.append(read_ms)
967
+ if write_ms > 0:
968
+ write_times.append(write_ms)
969
+ if encoding_ms > 0:
970
+ encoding_times.append(encoding_ms)
971
+ if frame_size_bytes > 0:
972
+ frame_sizes.append(frame_size_bytes)
973
+
974
+ # Calculate throughput per camera (FPS * frame_size)
975
+ if fps_avg > 0 and frame_size_bytes > 0:
976
+ total_throughput_kbps += (fps_avg * frame_size_bytes) / 1024
977
+
978
+ total_fps = sum(fps_values)
979
+ avg_fps = sum(fps_values) / len(fps_values) if fps_values else 0
980
+ min_fps = min(fps_values) if fps_values else 0
981
+ max_fps = max(fps_values) if fps_values else 0
982
+
983
+ avg_read_ms = sum(read_times) / len(read_times) if read_times else 0
984
+ avg_write_ms = sum(write_times) / len(write_times) if write_times else 0
985
+ avg_encoding_ms = sum(encoding_times) / len(encoding_times) if encoding_times else 0
986
+ total_latency_ms = avg_read_ms + avg_encoding_ms + avg_write_ms
987
+
988
+ avg_frame_size_kb = (sum(frame_sizes) / len(frame_sizes) / 1024) if frame_sizes else 0
989
+
990
+ total_throughput_mbps = total_throughput_kbps / 1024
991
+
992
+ frames_sent = transmission_stats.get("frames_sent_full", 0)
993
+ frames_skipped = transmission_stats.get("frames_skipped", 0)
994
+
995
+ # Log with BOLD formatting
996
+ B = self.BOLD
997
+ R = self.RESET
998
+
999
+ logging.info(
1000
+ f"\n{B}{'='*80}{R}\n"
1001
+ f"{B}[STREAMING GATEWAY AGGREGATE METRICS - 1 MIN SUMMARY]{R}\n"
1002
+ f"{B}{'='*80}{R}\n"
1003
+ f"{B}Cameras:{R} {len(stream_keys)} streaming | "
1004
+ f"{B}Runtime:{R} {runtime:.0f}s | "
1005
+ f"{B}Frames:{R} sent={frames_sent}, skipped={frames_skipped}\n"
1006
+ f"{B}{'─'*80}{R}\n"
1007
+ f"{B}FPS:{R} avg={avg_fps:.1f} | min={min_fps:.1f} | max={max_fps:.1f} | "
1008
+ f"{B}TOTAL={total_fps:.1f} fps{R}\n"
1009
+ f"{B}{'─'*80}{R}\n"
1010
+ f"{B}LATENCY BREAKDOWN:{R}\n"
1011
+ f" • Read: {avg_read_ms:.2f} ms (avg)\n"
1012
+ f" • Encode: {avg_encoding_ms:.2f} ms (avg)\n"
1013
+ f" • Write: {avg_write_ms:.2f} ms (avg)\n"
1014
+ f" • {B}TOTAL: {total_latency_ms:.2f} ms{R}\n"
1015
+ f"{B}{'─'*80}{R}\n"
1016
+ f"{B}THROUGHPUT:{R}\n"
1017
+ f" • Avg Frame Size: {avg_frame_size_kb:.1f} KB\n"
1018
+ f" • {B}TOTAL: {total_throughput_mbps:.2f} MB/s ({total_throughput_kbps:.1f} KB/s){R}\n"
1019
+ f"{B}{'='*80}{R}"
1020
+ )
1021
+
1022
+ def _log_camera_streamer_metrics(self, gateway_stats: Dict[str, Any], snapshot: Optional[Dict[str, Any]]):
1023
+ """Log metrics summary for CameraStreamer flow."""
1024
+ camera_streamer = self.streaming_gateway.camera_streamer
1025
+ if not camera_streamer:
1026
+ return
1027
+
1028
+ stream_keys = gateway_stats.get("my_stream_keys", [])
1029
+ transmission_stats = gateway_stats.get("transmission_stats", {})
1030
+ runtime = gateway_stats.get("runtime_seconds", 0)
1031
+
1032
+ # Build per-camera summary with frame size
1033
+ camera_summaries = []
1034
+ total_frame_size_kb = 0
1035
+ camera_count_with_size = 0
1036
+
1037
+ for stream_key in stream_keys[:5]: # Limit to first 5 cameras for log brevity
1038
+ timing_stats = camera_streamer.statistics.get_timing_statistics(stream_key)
1039
+ if timing_stats:
1040
+ read_ms = timing_stats.get("read_time_ms", {}).get("avg", 0.0)
1041
+ write_ms = timing_stats.get("write_time_ms", {}).get("avg", 0.0)
1042
+ frame_size_bytes = timing_stats.get("frame_size_bytes", {}).get("avg", 0)
1043
+ frame_kb = frame_size_bytes / 1024
1044
+ camera_summaries.append(f"{stream_key}(r:{read_ms:.1f}ms,w:{write_ms:.1f}ms,{frame_kb:.1f}KB)")
1045
+
1046
+ if frame_size_bytes > 0:
1047
+ total_frame_size_kb += frame_kb
1048
+ camera_count_with_size += 1
1049
+
1050
+ # Calculate average frame size across all cameras
1051
+ avg_frame_size_kb = total_frame_size_kb / camera_count_with_size if camera_count_with_size > 0 else 0
1052
+
1053
+ frames_sent = transmission_stats.get("frames_sent_full", 0)
1054
+ avg_fps = frames_sent / runtime if runtime > 0 else 0
1055
+
1056
+ logging.info(
1057
+ f"[METRICS] CameraStreamer | "
1058
+ f"cameras={len(stream_keys)} | "
1059
+ f"frames_sent={frames_sent} | "
1060
+ f"avg_fps={avg_fps:.1f} | "
1061
+ f"avg_frame_size={avg_frame_size_kb:.1f}KB | "
1062
+ f"runtime={runtime:.0f}s | "
1063
+ f"samples: {', '.join(camera_summaries[:3])}"
1064
+ )
1065
+
1066
+ def _log_async_worker_metrics(self, gateway_stats: Dict[str, Any], snapshot: Optional[Dict[str, Any]]):
1067
+ """Log metrics summary for async worker flow."""
1068
+ worker_stats = gateway_stats.get("worker_stats", {})
1069
+ stream_keys = gateway_stats.get("my_stream_keys", [])
1070
+ runtime = gateway_stats.get("runtime_seconds", 0)
1071
+
1072
+ num_workers = worker_stats.get("num_workers", 0)
1073
+ running_workers = worker_stats.get("running_workers", 0)
1074
+ total_cameras = worker_stats.get("total_cameras", len(stream_keys))
1075
+ worker_camera_counts = worker_stats.get("worker_camera_counts", {})
1076
+ health_reports = worker_stats.get("health_reports", {})
1077
+ per_camera_stats = worker_stats.get("per_camera_stats", {})
1078
+
1079
+ # Build worker load summary
1080
+ worker_loads = []
1081
+ for worker_id, count in worker_camera_counts.items():
1082
+ health = health_reports.get(worker_id, {})
1083
+ status = health.get("status", "unknown")
1084
+ worker_loads.append(f"W{worker_id}:{count}({status})")
1085
+
1086
+ # Calculate average frame size across all cameras
1087
+ total_frame_size_kb = 0
1088
+ camera_count_with_size = 0
1089
+ for stream_key, stats in per_camera_stats.items():
1090
+ frame_size_bytes = stats.get("frame_size_bytes", {}).get("avg", 0)
1091
+ if frame_size_bytes > 0:
1092
+ total_frame_size_kb += frame_size_bytes / 1024
1093
+ camera_count_with_size += 1
1094
+
1095
+ avg_frame_size_kb = total_frame_size_kb / camera_count_with_size if camera_count_with_size > 0 else 0
1096
+
1097
+ # Calculate average FPS across all cameras
1098
+ total_fps = 0
1099
+ camera_count_with_fps = 0
1100
+ for stream_key, stats in per_camera_stats.items():
1101
+ fps_avg = stats.get("fps", {}).get("avg", 0)
1102
+ if fps_avg > 0:
1103
+ total_fps += fps_avg
1104
+ camera_count_with_fps += 1
1105
+
1106
+ avg_fps = total_fps / camera_count_with_fps if camera_count_with_fps > 0 else 0
1107
+
1108
+ logging.info(
1109
+ f"[METRICS] AsyncWorkers | "
1110
+ f"workers={running_workers}/{num_workers} | "
1111
+ f"cameras={total_cameras} | "
1112
+ f"avg_fps={avg_fps:.1f} | "
1113
+ f"avg_frame_size={avg_frame_size_kb:.1f}KB | "
1114
+ f"runtime={runtime:.0f}s | "
1115
+ f"distribution: {', '.join(worker_loads[:4])}"
1116
+ )
1117
+
594
1118
  def _generate_and_send_report(self):
595
1119
  """Generate metrics report and send to Kafka."""
596
1120
  try:
@@ -602,26 +1126,39 @@ class MetricsManager:
602
1126
  return
603
1127
 
604
1128
  # Build report in the required format
1129
+ flow_type = "async_workers" if self.use_async_workers else "camera_streamer"
605
1130
  report = {
606
1131
  "streaming_gateway_id": self.streaming_gateway_id,
607
1132
  "action_id": self.action_id or "unknown",
608
1133
  "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
609
- "per_camera_metrics": per_camera_metrics
1134
+ "per_camera_metrics": per_camera_metrics,
1135
+ "flow_type": flow_type
610
1136
  }
611
1137
 
612
1138
  # Send report
613
1139
  success = self.reporter.send_metrics(report)
614
1140
 
1141
+ # Check if we should log (every 5 minutes)
1142
+ current_time = time.time()
1143
+ should_log = (current_time - self.last_log_time >= self.config.log_interval)
1144
+
615
1145
  if success:
616
- logging.info(f"Metrics report sent successfully ({len(per_camera_metrics)} cameras)")
1146
+ if should_log:
1147
+ logging.info(f"Metrics report sent successfully ({len(per_camera_metrics)} cameras, flow={flow_type})")
1148
+ self.last_log_time = current_time
617
1149
 
618
1150
  # Clear timing history after successful reporting to prevent unbounded memory growth
619
- camera_streamer = self.streaming_gateway.camera_streamer
620
- if camera_streamer:
621
- camera_streamer.statistics.clear_timing_history()
622
- logging.debug("Cleared timing history after successful metrics reporting")
1151
+ # Only applicable for CameraStreamer flow
1152
+ if not self.use_async_workers:
1153
+ camera_streamer = self.streaming_gateway.camera_streamer
1154
+ if camera_streamer and hasattr(camera_streamer, 'statistics'):
1155
+ camera_streamer.statistics.clear_timing_history()
1156
+ if should_log:
1157
+ logging.debug("Cleared timing history after successful metrics reporting")
623
1158
  else:
624
- logging.warning("Failed to send metrics report")
1159
+ if should_log:
1160
+ logging.warning(f"Failed to send metrics report (flow={flow_type})")
1161
+ self.last_log_time = current_time
625
1162
 
626
1163
  except Exception as e:
627
1164
  logging.error(f"Error generating/sending metrics report: {e}", exc_info=True)