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
@@ -0,0 +1,599 @@
1
+ """Debug GStreamer gateway for testing without Redis/Kafka/API."""
2
+ import logging
3
+ import time
4
+ from typing import List, Optional, Dict, Any
5
+ from pathlib import Path
6
+
7
+ from .debug_stream_backend import DebugStreamBackend
8
+ from .debug_utils import MockSession, create_debug_input_streams, create_camera_configs_from_streams
9
+
10
+ # Check GStreamer availability
11
+ GSTREAMER_AVAILABLE = False
12
+ GSTREAMER_WORKER_AVAILABLE = False
13
+ try:
14
+ from ..camera_streamer.gstreamer_camera_streamer import (
15
+ GStreamerCameraStreamer,
16
+ GStreamerConfig,
17
+ is_gstreamer_available,
18
+ )
19
+ GSTREAMER_AVAILABLE = is_gstreamer_available()
20
+
21
+ # Try to import GStreamerWorkerManager
22
+ from ..camera_streamer.gstreamer_worker_manager import GStreamerWorkerManager
23
+ GSTREAMER_WORKER_AVAILABLE = True
24
+ except ImportError:
25
+ pass
26
+
27
+
28
+ class DebugGStreamerGateway:
29
+ """Debug version of GStreamer streaming gateway for local testing.
30
+
31
+ This class allows you to test the complete GStreamer pipeline using:
32
+ - Local video files (MP4, AVI, etc.)
33
+ - RTSP streams
34
+ - USB cameras
35
+ - Test patterns
36
+
37
+ Without requiring:
38
+ - Kafka or Redis servers
39
+ - API authentication
40
+ - Network connectivity
41
+ - Real streaming gateway configuration
42
+
43
+ Perfect for:
44
+ - Local development and testing
45
+ - CI/CD pipelines
46
+ - GStreamer encoder testing (NVENC, x264, OpenH264, JPEG)
47
+ - Performance benchmarking
48
+ - Frame optimization testing
49
+
50
+ Example usage:
51
+ # JPEG frame-by-frame with NVENC fallback
52
+ gateway = DebugGStreamerGateway(
53
+ video_paths=["video1.mp4", "video2.mp4"],
54
+ fps=30,
55
+ gstreamer_encoder="jpeg",
56
+ jpeg_quality=85,
57
+ loop_videos=True
58
+ )
59
+ gateway.start_streaming()
60
+
61
+ # Wait and check stats
62
+ time.sleep(30)
63
+ stats = gateway.get_statistics()
64
+ print(f"FPS: {stats['avg_fps']:.1f}")
65
+ print(f"Bandwidth: {stats['bandwidth_mbps']:.2f} Mbps")
66
+ print(f"Cache hits: {stats['cache_efficiency']:.1f}%")
67
+
68
+ # Stop
69
+ gateway.stop_streaming()
70
+
71
+ # Test NVENC hardware encoding
72
+ gateway = DebugGStreamerGateway(
73
+ video_paths=["high_res_video.mp4"],
74
+ fps=60,
75
+ gstreamer_encoder="nvenc",
76
+ gstreamer_codec="h264",
77
+ gstreamer_preset="low-latency",
78
+ gstreamer_gpu_id=0
79
+ )
80
+ gateway.start_streaming()
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ video_paths: List[str],
86
+ fps: int = 30,
87
+ loop_videos: bool = True,
88
+ output_dir: Optional[str] = None,
89
+ save_to_files: bool = False,
90
+ log_messages: bool = True,
91
+ save_frame_data: bool = False,
92
+ width: Optional[int] = None,
93
+ height: Optional[int] = None,
94
+ # GStreamer specific options
95
+ gstreamer_encoder: str = "jpeg", # jpeg, nvenc, x264, openh264, auto
96
+ gstreamer_codec: str = "h264", # h264, h265
97
+ gstreamer_preset: str = "low-latency", # NVENC preset
98
+ gstreamer_gpu_id: int = 0, # GPU device ID
99
+ jpeg_quality: int = 85, # JPEG quality (1-100)
100
+ # Frame optimization
101
+ enable_frame_optimizer: bool = True,
102
+ frame_optimizer_scale: float = 0.4,
103
+ frame_optimizer_threshold: float = 0.05,
104
+ # Worker mode parameters (Mode 4)
105
+ use_workers: bool = False,
106
+ num_workers: Optional[int] = None,
107
+ cpu_percentage: float = 0.8,
108
+ max_cameras_per_worker: int = 10,
109
+ ):
110
+ """Initialize debug GStreamer gateway.
111
+
112
+ Args:
113
+ video_paths: List of video file paths to stream
114
+ fps: Frames per second to stream
115
+ loop_videos: Loop videos continuously
116
+ output_dir: Directory to save debug output
117
+ save_to_files: Save streamed messages to files
118
+ log_messages: Log message metadata
119
+ save_frame_data: Include frame data in saved files
120
+ width: Override video width
121
+ height: Override video height
122
+ gstreamer_encoder: GStreamer encoder (jpeg, nvenc, x264, openh264, auto)
123
+ gstreamer_codec: Codec for hardware/software encoders (h264, h265)
124
+ gstreamer_preset: NVENC preset (low-latency, high-quality, lossless)
125
+ gstreamer_gpu_id: GPU device ID for NVENC
126
+ jpeg_quality: JPEG quality 1-100 (higher=better, larger)
127
+ enable_frame_optimizer: Enable frame similarity detection
128
+ frame_optimizer_scale: Downscale factor for similarity detection
129
+ frame_optimizer_threshold: Similarity threshold (lower=stricter)
130
+ use_workers: Use multi-process GStreamerWorkerManager (Mode 4) instead of single-threaded
131
+ num_workers: Number of worker processes (auto-calculated if None)
132
+ cpu_percentage: Percentage of CPU cores to use for auto-calculation
133
+ max_cameras_per_worker: Maximum cameras per worker process
134
+ """
135
+ # Check GStreamer availability
136
+ if not GSTREAMER_AVAILABLE:
137
+ raise RuntimeError(
138
+ "GStreamer not available. Install with:\n"
139
+ " pip install PyGObject\n"
140
+ " apt-get install gstreamer1.0-tools gstreamer1.0-plugins-*"
141
+ )
142
+
143
+ # Check worker mode availability
144
+ if use_workers and not GSTREAMER_WORKER_AVAILABLE:
145
+ raise RuntimeError(
146
+ "GStreamerWorkerManager not available. Worker mode requires the full package."
147
+ )
148
+
149
+ # Validate video paths
150
+ for video_path in video_paths:
151
+ if not Path(video_path).exists():
152
+ raise FileNotFoundError(f"Video file not found: {video_path}")
153
+
154
+ self.video_paths = video_paths
155
+ self.fps = fps
156
+ self.loop_videos = loop_videos
157
+ self.use_workers = use_workers
158
+ self.enable_frame_optimizer = enable_frame_optimizer
159
+
160
+ # Create mock session
161
+ self.session = MockSession()
162
+
163
+ # Create debug stream backend (replaces Kafka/Redis)
164
+ self.stream_backend = DebugStreamBackend(
165
+ output_dir=output_dir,
166
+ save_to_files=save_to_files,
167
+ log_messages=log_messages,
168
+ save_frame_data=save_frame_data
169
+ )
170
+
171
+ # Create input streams from video paths
172
+ self.input_streams = create_debug_input_streams(
173
+ video_paths=video_paths,
174
+ fps=fps,
175
+ loop=loop_videos
176
+ )
177
+
178
+ # Override dimensions if provided
179
+ if width or height:
180
+ for stream in self.input_streams:
181
+ if width:
182
+ stream.width = width
183
+ if height:
184
+ stream.height = height
185
+
186
+ # Create GStreamer config
187
+ self.gstreamer_config = GStreamerConfig(
188
+ encoder=gstreamer_encoder,
189
+ codec=gstreamer_codec,
190
+ preset=gstreamer_preset,
191
+ gpu_id=gstreamer_gpu_id,
192
+ jpeg_quality=jpeg_quality,
193
+ )
194
+
195
+ # Frame optimizer config
196
+ self.frame_optimizer_config = {
197
+ "scale": frame_optimizer_scale,
198
+ "similarity_threshold": frame_optimizer_threshold,
199
+ "diff_threshold": 15,
200
+ "bg_update_interval": 10,
201
+ }
202
+
203
+ # Initialize camera streamer or worker manager based on mode
204
+ self.camera_streamer = None
205
+ self.worker_manager = None
206
+
207
+ if use_workers:
208
+ # Mode 4: GStreamerWorkerManager (multi-process GStreamer workers)
209
+ # Create camera configs from input streams
210
+ camera_configs = create_camera_configs_from_streams(self.input_streams)
211
+
212
+ # Create stream config for workers
213
+ stream_config = {
214
+ 'service_id': 'debug_gstreamer_gateway',
215
+ 'server_type': 'debug',
216
+ 'gstreamer_encoder': gstreamer_encoder,
217
+ 'gstreamer_codec': gstreamer_codec,
218
+ 'gstreamer_preset': gstreamer_preset,
219
+ 'gstreamer_gpu_id': gstreamer_gpu_id,
220
+ 'jpeg_quality': jpeg_quality,
221
+ 'frame_optimizer_enabled': enable_frame_optimizer,
222
+ 'frame_optimizer_config': self.frame_optimizer_config,
223
+ # Debug backend will be injected into workers
224
+ 'debug_mode': True,
225
+ 'debug_backend_config': {
226
+ 'output_dir': output_dir,
227
+ 'save_to_files': save_to_files,
228
+ 'log_messages': log_messages,
229
+ 'save_frame_data': save_frame_data,
230
+ },
231
+ }
232
+
233
+ self.worker_manager = GStreamerWorkerManager(
234
+ camera_configs=camera_configs,
235
+ stream_config=stream_config,
236
+ num_workers=num_workers,
237
+ cpu_percentage=cpu_percentage,
238
+ max_cameras_per_worker=max_cameras_per_worker,
239
+ gstreamer_encoder=gstreamer_encoder,
240
+ gstreamer_codec=gstreamer_codec,
241
+ gstreamer_preset=gstreamer_preset,
242
+ gpu_id=gstreamer_gpu_id,
243
+ )
244
+
245
+ self.logger = logging.getLogger(__name__)
246
+ self.logger.info(
247
+ f"DebugGStreamerGateway initialized (Worker Mode): "
248
+ f"{len(video_paths)} videos, "
249
+ f"{fps} fps, "
250
+ f"encoder={gstreamer_encoder}, "
251
+ f"codec={gstreamer_codec}, "
252
+ f"workers={self.worker_manager.num_workers}"
253
+ )
254
+ else:
255
+ # Mode 3: GStreamerCameraStreamer (single-threaded)
256
+ self.camera_streamer = GStreamerCameraStreamer(
257
+ session=self.session,
258
+ service_id="debug_gstreamer_gateway",
259
+ server_type="debug",
260
+ gstreamer_config=self.gstreamer_config,
261
+ frame_optimizer_enabled=enable_frame_optimizer,
262
+ frame_optimizer_config=self.frame_optimizer_config,
263
+ gateway_util=None, # No gateway_util in debug mode
264
+ )
265
+
266
+ # Replace MatriceStream with debug backend
267
+ self.camera_streamer.matrice_stream = self.stream_backend
268
+
269
+ self.logger = logging.getLogger(__name__)
270
+ self.logger.info(
271
+ f"DebugGStreamerGateway initialized (Single-threaded Mode): "
272
+ f"{len(video_paths)} videos, "
273
+ f"{fps} fps, "
274
+ f"encoder={gstreamer_encoder}, "
275
+ f"codec={gstreamer_codec}"
276
+ )
277
+
278
+ # State
279
+ self.is_streaming = False
280
+ self.start_time = None
281
+
282
+ def start_streaming(self, block: bool = False) -> bool:
283
+ """Start streaming all video files.
284
+
285
+ Args:
286
+ block: If True, block until manually stopped
287
+
288
+ Returns:
289
+ True if started successfully
290
+ """
291
+ if self.is_streaming:
292
+ self.logger.warning("Already streaming")
293
+ return False
294
+
295
+ self.logger.info(f"Starting GStreamer debug streaming with {len(self.input_streams)} videos")
296
+
297
+ try:
298
+ if self.use_workers:
299
+ # Mode 4: Worker manager mode
300
+ self.worker_manager.start()
301
+ self.is_streaming = True
302
+ self.start_time = time.time()
303
+ self.logger.info(f"GStreamer worker manager started with {self.worker_manager.num_workers} workers")
304
+ else:
305
+ # Mode 3: Single-threaded camera streamer mode
306
+ # Register topics and start streams
307
+ for i, input_stream in enumerate(self.input_streams):
308
+ stream_key = input_stream.camera_key
309
+ topic = input_stream.camera_input_topic
310
+
311
+ # Register topic
312
+ self.camera_streamer.register_stream_topic(stream_key, topic)
313
+ self.stream_backend.setup(topic)
314
+
315
+ # Start streaming
316
+ success = self.camera_streamer.start_background_stream(
317
+ input=input_stream.source,
318
+ fps=input_stream.fps,
319
+ stream_key=stream_key,
320
+ stream_group_key=input_stream.camera_group_key,
321
+ quality=input_stream.quality,
322
+ width=input_stream.width,
323
+ height=input_stream.height,
324
+ simulate_video_file_stream=input_stream.simulate_video_file_stream,
325
+ camera_location=input_stream.camera_location,
326
+ )
327
+
328
+ if not success:
329
+ self.logger.error(f"Failed to start stream {i}: {input_stream.source}")
330
+ self.stop_streaming()
331
+ return False
332
+
333
+ self.logger.info(f"Started stream {i}: {input_stream.source}")
334
+
335
+ self.is_streaming = True
336
+ self.start_time = time.time()
337
+ self.logger.info("GStreamer debug streaming started successfully")
338
+
339
+ if block:
340
+ self.logger.info("Blocking mode - press Ctrl+C to stop")
341
+ try:
342
+ while self.is_streaming:
343
+ time.sleep(1)
344
+ except KeyboardInterrupt:
345
+ self.logger.info("Interrupted by user")
346
+ self.stop_streaming()
347
+
348
+ return True
349
+
350
+ except Exception as e:
351
+ self.logger.error(f"Failed to start GStreamer debug streaming: {e}", exc_info=True)
352
+ self.stop_streaming()
353
+ return False
354
+
355
+ def stop_streaming(self):
356
+ """Stop all streaming."""
357
+ if not self.is_streaming:
358
+ self.logger.warning("Not streaming")
359
+ return
360
+
361
+ self.logger.info("Stopping GStreamer debug streaming")
362
+
363
+ try:
364
+ if self.use_workers:
365
+ # Mode 4: Stop worker manager
366
+ self.worker_manager.stop()
367
+ else:
368
+ # Mode 3: Stop camera streamer
369
+ self.camera_streamer.stop_streaming()
370
+ self.stream_backend.close()
371
+
372
+ self.is_streaming = False
373
+
374
+ runtime = time.time() - self.start_time if self.start_time else 0
375
+ self.logger.info(f"GStreamer debug streaming stopped (runtime: {runtime:.1f}s)")
376
+
377
+ except Exception as e:
378
+ self.logger.error(f"Error stopping GStreamer debug streaming: {e}", exc_info=True)
379
+
380
+ def get_statistics(self) -> Dict[str, Any]:
381
+ """Get comprehensive streaming statistics.
382
+
383
+ Returns:
384
+ Dictionary with streaming statistics including:
385
+ - Basic info (video count, fps, encoder, codec)
386
+ - Runtime metrics (duration, uptime)
387
+ - Transmission stats (frames sent, bytes, topics)
388
+ - Performance metrics (avg fps, bandwidth)
389
+ - Frame optimization metrics (cache efficiency, similarity rate)
390
+ - GStreamer pipeline metrics (per-stream stats)
391
+ """
392
+ stats = {
393
+ "is_streaming": self.is_streaming,
394
+ "video_count": len(self.video_paths),
395
+ "fps": self.fps,
396
+ "encoder": self.gstreamer_config.encoder,
397
+ "codec": self.gstreamer_config.codec,
398
+ "jpeg_quality": self.gstreamer_config.jpeg_quality,
399
+ "mode": "worker" if self.use_workers else "single-threaded",
400
+ "frame_optimizer_enabled": self.enable_frame_optimizer,
401
+ }
402
+
403
+ if self.start_time:
404
+ stats["runtime_seconds"] = time.time() - self.start_time
405
+
406
+ if self.use_workers:
407
+ # Mode 4: Get worker manager stats
408
+ try:
409
+ # Get worker statistics
410
+ worker_stats = self.worker_manager.get_worker_statistics()
411
+ stats["worker_stats"] = worker_stats
412
+
413
+ # Calculate aggregate transmission stats from health_reports
414
+ total_frames = 0
415
+ total_bytes = 0
416
+ active_streams = 0
417
+ total_errors = 0
418
+
419
+ # The worker_stats has a 'health_reports' dict with per-worker metrics
420
+ health_reports = worker_stats.get('health_reports', {})
421
+ for worker_id, report in health_reports.items():
422
+ if isinstance(report, dict):
423
+ metrics = report.get('metrics', {})
424
+ total_frames += metrics.get('frames_encoded', 0)
425
+ # Note: bytes aren't tracked directly, estimate from frames
426
+ active_streams += report.get('active_cameras', 0)
427
+ total_errors += metrics.get('encoding_errors', 0)
428
+
429
+ # Estimate bytes (we don't have direct tracking in gstreamer_worker)
430
+ # Using rough estimate of 30KB per JPEG frame at 640x480
431
+ estimated_bytes = total_frames * 30000
432
+
433
+ stats["transmission_stats"] = {
434
+ "total_frames_sent": total_frames,
435
+ "total_bytes_sent": estimated_bytes,
436
+ "active_streams": active_streams,
437
+ "num_workers": worker_stats.get('num_workers', self.worker_manager.num_workers),
438
+ "running_workers": worker_stats.get('running_workers', 0),
439
+ "total_errors": total_errors,
440
+ }
441
+
442
+ # Calculate derived metrics
443
+ if self.start_time:
444
+ duration = time.time() - self.start_time
445
+ stats["avg_fps"] = total_frames / duration if duration > 0 else 0
446
+ stats["bandwidth_mbps"] = (estimated_bytes * 8) / (duration * 1_000_000) if duration > 0 else 0
447
+
448
+ except Exception as e:
449
+ self.logger.warning(f"Failed to get worker stats: {e}")
450
+ stats["transmission_stats"] = {}
451
+ else:
452
+ # Mode 3: Get camera streamer stats
453
+ try:
454
+ transmission_stats = self.camera_streamer.get_transmission_stats()
455
+
456
+ # Normalize field names for consistency
457
+ # GStreamerCameraStreamer uses total_frames_processed
458
+ total_frames = transmission_stats.get("total_frames_processed", 0)
459
+ total_bytes = transmission_stats.get("total_bytes_sent", 0)
460
+
461
+ # Get GStreamer-specific stats
462
+ gstreamer_stats = transmission_stats.get("gstreamer", {})
463
+ if not total_bytes and gstreamer_stats:
464
+ total_bytes = gstreamer_stats.get("total_bytes", 0)
465
+
466
+ # Get backend stats for additional metrics
467
+ backend_stats = self.stream_backend.get_statistics()
468
+ if not total_frames and backend_stats:
469
+ total_frames = backend_stats.get("total_messages", 0)
470
+
471
+ # Update transmission_stats with normalized field names
472
+ transmission_stats["total_frames_sent"] = total_frames
473
+ transmission_stats["total_bytes_sent"] = total_bytes
474
+ stats["transmission_stats"] = transmission_stats
475
+
476
+ # Calculate derived metrics
477
+ if self.start_time:
478
+ duration = time.time() - self.start_time
479
+ stats["avg_fps"] = total_frames / duration if duration > 0 else 0
480
+ stats["bandwidth_mbps"] = (total_bytes * 8) / (duration * 1_000_000) if duration > 0 else 0
481
+
482
+ except Exception as e:
483
+ self.logger.warning(f"Failed to get transmission stats: {e}")
484
+
485
+ # Get frame optimizer metrics (only available in single-threaded mode)
486
+ try:
487
+ if self.camera_streamer.frame_optimizer.enabled:
488
+ opt_metrics = self.camera_streamer.frame_optimizer.get_metrics()
489
+ stats["frame_optimizer_metrics"] = opt_metrics
490
+
491
+ # Calculate cache efficiency
492
+ total_checks = opt_metrics.get("total_checks", 0)
493
+ similar_count = opt_metrics.get("similar_count", 0)
494
+ stats["cache_efficiency"] = (similar_count / total_checks * 100) if total_checks > 0 else 0
495
+
496
+ except Exception as e:
497
+ self.logger.warning(f"Failed to get frame optimizer metrics: {e}")
498
+
499
+ # Get backend stats
500
+ try:
501
+ stats["backend_stats"] = self.stream_backend.get_statistics()
502
+ except Exception as e:
503
+ self.logger.warning(f"Failed to get backend stats: {e}")
504
+
505
+ # Get per-stream timing stats
506
+ try:
507
+ timing_stats = {}
508
+ for input_stream in self.input_streams:
509
+ stream_key = input_stream.camera_key
510
+ timing = self.camera_streamer.get_stream_timing_stats(stream_key)
511
+ if timing:
512
+ timing_stats[stream_key] = timing
513
+ stats["timing_stats"] = timing_stats
514
+ except Exception as e:
515
+ self.logger.warning(f"Failed to get timing stats: {e}")
516
+
517
+ return stats
518
+
519
+ def get_timing_stats(self, stream_key: Optional[str] = None) -> Dict[str, Any]:
520
+ """Get detailed timing statistics.
521
+
522
+ Args:
523
+ stream_key: Specific stream or None for all
524
+
525
+ Returns:
526
+ Timing statistics with read_time, write_time, process_time breakdown
527
+ """
528
+ if self.use_workers:
529
+ # Worker mode doesn't support per-stream timing stats directly
530
+ return {"mode": "worker", "note": "Per-stream timing not available in worker mode"}
531
+ return self.camera_streamer.get_stream_timing_stats(stream_key)
532
+
533
+ def produce_request(self, input_data: bytes, topic: str, key: Optional[str] = None) -> bool:
534
+ """Synchronous message production for testing.
535
+
536
+ Args:
537
+ input_data: Message data
538
+ topic: Topic/stream name
539
+ key: Message key
540
+
541
+ Returns:
542
+ True if successful
543
+ """
544
+ return self.camera_streamer.produce_request(input_data, topic, key)
545
+
546
+ async def async_produce_request(self, input_data: bytes, topic: str, key: Optional[str] = None) -> bool:
547
+ """Async message production for testing.
548
+
549
+ Args:
550
+ input_data: Message data
551
+ topic: Topic/stream name
552
+ key: Message key
553
+
554
+ Returns:
555
+ True if successful
556
+ """
557
+ return await self.camera_streamer.async_produce_request(input_data, topic, key)
558
+
559
+ def refresh_connection_info(self) -> bool:
560
+ """Refresh connection (no-op for debug mode).
561
+
562
+ Returns:
563
+ True (always successful in debug mode)
564
+ """
565
+ self.logger.info("refresh_connection_info called (no-op in debug mode)")
566
+ return True
567
+
568
+ def reset_stats(self):
569
+ """Reset all statistics."""
570
+ if self.use_workers:
571
+ # Worker mode - no direct reset capability
572
+ self.logger.info("Statistics reset (worker mode - limited reset)")
573
+ else:
574
+ self.camera_streamer.reset_transmission_stats()
575
+ if self.camera_streamer.frame_optimizer.enabled:
576
+ self.camera_streamer.frame_optimizer.reset_metrics()
577
+ self.logger.info("Statistics reset")
578
+
579
+ def __enter__(self):
580
+ """Context manager entry."""
581
+ return self
582
+
583
+ def __exit__(self, exc_type, exc_val, exc_tb):
584
+ """Context manager exit."""
585
+ if exc_type:
586
+ self.logger.error(f"Exception in context: {exc_val}", exc_info=True)
587
+ self.stop_streaming()
588
+
589
+ def __repr__(self):
590
+ """String representation."""
591
+ mode_str = f"workers={self.worker_manager.num_workers}" if self.use_workers else "single-threaded"
592
+ return (
593
+ f"DebugGStreamerGateway("
594
+ f"videos={len(self.video_paths)}, "
595
+ f"fps={self.fps}, "
596
+ f"encoder={self.gstreamer_config.encoder}, "
597
+ f"mode={mode_str}, "
598
+ f"streaming={self.is_streaming})"
599
+ )