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
@@ -1,13 +1,58 @@
1
1
  import logging
2
- import base64
2
+ import os
3
3
  import time
4
4
  import threading
5
5
  import atexit
6
- from typing import Dict, List, Optional
6
+ from typing import Dict, List, Optional, Any
7
7
  from .camera_streamer import CameraStreamer
8
- from .streaming_gateway_utils import StreamingGatewayUtil, InputStream
8
+ from .camera_streamer.worker_manager import WorkerManager
9
+ from .streaming_gateway_utils import (
10
+ StreamingGatewayUtil,
11
+ InputStream,
12
+ input_stream_to_camera_config,
13
+ build_stream_config,
14
+ )
9
15
  from .event_listener import EventListener
10
- from .dynamic_camera_manager import DynamicCameraManager
16
+ from .dynamic_camera_manager import DynamicCameraManager, DynamicCameraManagerForWorkers
17
+
18
+ USE_FFMPEG = os.getenv("USE_FFMPEG", "false").lower() == "true"
19
+ USE_GSTREAMER = os.getenv("USE_GSTREAMER", "false").lower() == "true"
20
+ USE_NVDEC = os.getenv("USE_NVDEC", "false").lower() == "true"
21
+
22
+ # GStreamer imports (optional - graceful degradation)
23
+ GSTREAMER_AVAILABLE = False
24
+ try:
25
+ from .camera_streamer.gstreamer_camera_streamer import (
26
+ GStreamerCameraStreamer,
27
+ GStreamerConfig,
28
+ is_gstreamer_available,
29
+ )
30
+ from .camera_streamer.gstreamer_worker_manager import GStreamerWorkerManager
31
+ GSTREAMER_AVAILABLE = is_gstreamer_available()
32
+ except (ImportError, ValueError):
33
+ # ImportError: gi module not available
34
+ # ValueError: gi.require_version fails when GStreamer not installed
35
+ pass
36
+
37
+ # FFmpeg imports (optional - graceful degradation)
38
+ FFMPEG_AVAILABLE = False
39
+ try:
40
+ from .camera_streamer.ffmpeg_config import FFmpegConfig, is_ffmpeg_available
41
+ from .camera_streamer.ffmpeg_camera_streamer import FFmpegCameraStreamer
42
+ from .camera_streamer.ffmpeg_worker_manager import FFmpegWorkerManager
43
+ FFMPEG_AVAILABLE = is_ffmpeg_available()
44
+ except (ImportError, FileNotFoundError):
45
+ # FFmpeg not available or not installed
46
+ pass
47
+
48
+ # NVDEC imports (optional - graceful degradation)
49
+ NVDEC_AVAILABLE = False
50
+ try:
51
+ from .camera_streamer.nvdec_worker_manager import NVDECWorkerManager, is_nvdec_available
52
+ NVDEC_AVAILABLE = is_nvdec_available()
53
+ except ImportError:
54
+ # NVDEC not available (requires CuPy, PyNvVideoCodec)
55
+ pass
11
56
 
12
57
 
13
58
  class StreamingGateway:
@@ -27,6 +72,43 @@ class StreamingGateway:
27
72
  video_codec: Optional[str] = None,
28
73
  force_restart: bool = False,
29
74
  enable_event_listening: bool = True,
75
+ action_id: str = None,
76
+ use_async_workers: bool = True,
77
+ num_workers: int = None, # Auto-calculate based on CPU cores and camera count
78
+ max_cameras_per_worker: int = 50,
79
+ allow_empty_start: bool = True,
80
+ # GStreamer options
81
+ use_gstreamer: bool = False,
82
+ gstreamer_encoder: str = "auto", # auto, nvenc, x264, openh264, jpeg
83
+ gstreamer_codec: str = "h264", # h264, h265
84
+ gstreamer_preset: str = "low-latency", # NVENC preset
85
+ gstreamer_gpu_id: int = 0, # GPU device ID for NVENC
86
+ # Platform-specific GStreamer options
87
+ gstreamer_platform: str = "auto", # auto, jetson, desktop-gpu, intel, amd, cpu
88
+ gstreamer_use_hardware_decode: bool = True, # Use hardware decode (nvv4l2decoder, nvdec, vaapi)
89
+ gstreamer_use_hardware_jpeg: bool = True, # Use hardware JPEG (nvjpegenc, vaapijpegenc)
90
+ gstreamer_jetson_use_nvmm: bool = True, # Use NVMM zero-copy on Jetson
91
+ gstreamer_frame_optimizer_mode: str = "hash-only", # hash-only, dual-appsink, disabled
92
+ gstreamer_fallback_on_error: bool = True, # Gracefully fallback to CPU pipeline on error
93
+ gstreamer_verbose_logging: bool = False, # Verbose pipeline logging for debugging
94
+ # FFmpeg options
95
+ use_ffmpeg: bool = USE_FFMPEG, # Use FFmpeg subprocess-based encoding
96
+ ffmpeg_hwaccel: str = "auto", # Hardware acceleration: auto, cuda, vaapi, none
97
+ ffmpeg_threads: int = 1, # FFmpeg decode threads per stream
98
+ ffmpeg_low_latency: bool = True, # Enable low-latency flags
99
+ ffmpeg_pixel_format: str = "bgr24",# Output pixel format
100
+ # NVDEC options (CUDA IPC ring buffer output)
101
+ use_nvdec: bool = USE_NVDEC, # Use NVDEC hardware decode + CUDA IPC output
102
+ nvdec_gpu_id: int = 0, # Primary GPU device ID (starting GPU)
103
+ nvdec_num_gpus: int = 0, # Number of GPUs (0=auto-detect all available)
104
+ nvdec_pool_size: int = 8, # NVDEC decoders per GPU
105
+ nvdec_burst_size: int = 4, # Frames per stream before rotating
106
+ nvdec_frame_width: int = 640, # Output frame width
107
+ nvdec_frame_height: int = 640, # Output frame height
108
+ nvdec_num_slots: int = 32, # Ring buffer slots per camera
109
+ nvdec_target_fps: int = 0, # FPS override (0=use per-camera FPS from config)
110
+ # SHM configuration (centralized)
111
+ shm_slot_count: int = 1000, # Ring buffer size per camera (increased for consumer lag)
30
112
  ):
31
113
  """Initialize StreamingGateway.
32
114
 
@@ -39,6 +121,38 @@ class StreamingGateway:
39
121
  video_codec: Video codec (h264 or h265)
40
122
  force_restart: Force stop existing streams and restart
41
123
  enable_event_listening: Enable dynamic event listening for configuration updates
124
+ action_id: Optional action ID to pass in API requests
125
+ use_async_workers: Use new async worker flow (default True)
126
+ num_workers: Number of worker processes for async flow
127
+ max_cameras_per_worker: Maximum cameras per worker process
128
+ allow_empty_start: Allow starting with zero cameras (default True)
129
+ use_gstreamer: Use GStreamer-based encoding (default False)
130
+ gstreamer_encoder: GStreamer encoder type (auto, nvenc, x264, openh264, jpeg)
131
+ gstreamer_codec: GStreamer codec (h264, h265)
132
+ gstreamer_preset: NVENC preset for hardware encoding
133
+ gstreamer_gpu_id: GPU device ID for NVENC hardware encoding
134
+ gstreamer_platform: Platform override (auto, jetson, desktop-gpu, intel, amd, cpu)
135
+ gstreamer_use_hardware_decode: Enable hardware decode (nvv4l2decoder, nvdec, vaapi)
136
+ gstreamer_use_hardware_jpeg: Enable hardware JPEG encoding when available
137
+ gstreamer_jetson_use_nvmm: Use NVMM zero-copy memory on Jetson devices
138
+ gstreamer_frame_optimizer_mode: Frame optimization mode (hash-only, dual-appsink, disabled)
139
+ gstreamer_fallback_on_error: Automatically fallback to CPU pipeline on hardware errors
140
+ gstreamer_verbose_logging: Enable verbose pipeline construction logging
141
+ use_ffmpeg: Use FFmpeg subprocess-based encoding (alternative to OpenCV/GStreamer)
142
+ ffmpeg_hwaccel: FFmpeg hardware acceleration (auto, cuda, vaapi, none)
143
+ ffmpeg_threads: Number of FFmpeg decode threads per stream
144
+ ffmpeg_low_latency: Enable FFmpeg low-latency flags
145
+ ffmpeg_pixel_format: Output pixel format (bgr24, rgb24, nv12)
146
+ use_nvdec: Use NVDEC hardware decode with CUDA IPC output (requires CuPy, PyNvVideoCodec)
147
+ nvdec_gpu_id: Primary/starting GPU device ID for round-robin camera assignment
148
+ nvdec_num_gpus: Number of GPUs to use (0=auto-detect all available GPUs)
149
+ nvdec_pool_size: Number of NVDEC decoders per GPU
150
+ nvdec_burst_size: Frames per stream before rotating to next stream
151
+ nvdec_frame_width: Default output frame width (used if camera config doesn't specify)
152
+ nvdec_frame_height: Default output frame height (used if camera config doesn't specify)
153
+ nvdec_num_slots: Ring buffer slots per camera (named by camera_id)
154
+ nvdec_target_fps: Global FPS override (0=use per-camera FPS from camera config)
155
+ shm_slot_count: Number of frame slots per camera ring buffer for SHM mode (default: 300)
42
156
  """
43
157
  if not session:
44
158
  raise ValueError("Session is required")
@@ -49,49 +163,383 @@ class StreamingGateway:
49
163
  self.streaming_gateway_id = streaming_gateway_id
50
164
  self.force_restart = force_restart
51
165
  self.enable_event_listening = enable_event_listening
52
- self.server_type = server_type
166
+ self.use_async_workers = use_async_workers
167
+ self.num_workers = num_workers
168
+ self.max_cameras_per_worker = max_cameras_per_worker
169
+ self.video_codec = video_codec
170
+
171
+ # GStreamer configuration
172
+ self.use_gstreamer = use_gstreamer
173
+ self.gstreamer_encoder = gstreamer_encoder
174
+ self.gstreamer_codec = gstreamer_codec
175
+ self.gstreamer_preset = gstreamer_preset
176
+ self.gstreamer_gpu_id = gstreamer_gpu_id
177
+ # Platform-specific GStreamer configuration
178
+ self.gstreamer_platform = gstreamer_platform
179
+ self.gstreamer_use_hardware_decode = gstreamer_use_hardware_decode
180
+ self.gstreamer_use_hardware_jpeg = gstreamer_use_hardware_jpeg
181
+ self.gstreamer_jetson_use_nvmm = gstreamer_jetson_use_nvmm
182
+ self.gstreamer_frame_optimizer_mode = gstreamer_frame_optimizer_mode
183
+ self.gstreamer_fallback_on_error = gstreamer_fallback_on_error
184
+ self.gstreamer_verbose_logging = gstreamer_verbose_logging
185
+
186
+ # Validate GStreamer availability if requested
187
+ if use_gstreamer and not GSTREAMER_AVAILABLE:
188
+ raise RuntimeError(
189
+ "GStreamer requested but not available. "
190
+ "Install with: pip install PyGObject && apt-get install gstreamer1.0-plugins-*"
191
+ )
192
+
193
+ # FFmpeg configuration
194
+ self.use_ffmpeg = use_ffmpeg
195
+ self.ffmpeg_hwaccel = ffmpeg_hwaccel
196
+ self.ffmpeg_threads = ffmpeg_threads
197
+ self.ffmpeg_low_latency = ffmpeg_low_latency
198
+ self.ffmpeg_pixel_format = ffmpeg_pixel_format
199
+
200
+ # NVDEC configuration
201
+ self.use_nvdec = use_nvdec
202
+ self.nvdec_gpu_id = nvdec_gpu_id
203
+ self.nvdec_num_gpus = nvdec_num_gpus
204
+ self.nvdec_pool_size = nvdec_pool_size
205
+ self.nvdec_burst_size = nvdec_burst_size
206
+ self.nvdec_frame_width = nvdec_frame_width
207
+ self.nvdec_frame_height = nvdec_frame_height
208
+ self.nvdec_num_slots = nvdec_num_slots
209
+ self.nvdec_target_fps = nvdec_target_fps
210
+
211
+ # SHM configuration (centralized for all workers)
212
+ self.shm_slot_count = shm_slot_count
213
+
214
+ # Validate FFmpeg availability if requested
215
+ if use_ffmpeg and not FFMPEG_AVAILABLE:
216
+ raise RuntimeError(
217
+ "FFmpeg requested but not available. "
218
+ "Install FFmpeg from https://ffmpeg.org/download.html"
219
+ )
220
+
221
+ # Validate NVDEC availability if requested
222
+ if use_nvdec and not NVDEC_AVAILABLE:
223
+ raise RuntimeError(
224
+ "NVDEC requested but not available. "
225
+ "Requires CuPy, PyNvVideoCodec, and cuda_shm_ring_buffer module."
226
+ )
227
+
228
+ # Validate exclusive backend selection
229
+ backends_enabled = sum([use_gstreamer, use_ffmpeg, use_nvdec])
230
+ if backends_enabled > 1:
231
+ raise ValueError("Cannot enable multiple backends (GStreamer, FFmpeg, NVDEC) simultaneously")
53
232
 
54
233
  # Initialize utility for API interactions
55
- self.gateway_util = StreamingGatewayUtil(session, streaming_gateway_id, server_id)
234
+ self.gateway_util = StreamingGatewayUtil(session, streaming_gateway_id, server_id, action_id=action_id)
235
+
236
+ # Determine server_type - fetch from API if not provided
237
+ if server_type is None:
238
+ gateway_info = self.gateway_util.get_streaming_gateway_by_id()
239
+ if gateway_info:
240
+ server_type = gateway_info.get('serverType')
241
+ logging.info(f"Retrieved server_type from API: {server_type}")
242
+ else:
243
+ raise ValueError("server_type is required but could not be retrieved from API")
244
+
245
+ if not server_type:
246
+ raise ValueError("server_type is required (kafka or redis)")
247
+
248
+ self.server_type = server_type
249
+ self.allow_empty_start = allow_empty_start
56
250
 
57
251
  # Get input configurations
58
252
  if inputs_config is None:
59
253
  logging.info("Fetching input configurations from API")
60
- self.inputs_config = self.gateway_util.get_input_streams()
254
+ try:
255
+ self.inputs_config = self.gateway_util.get_input_streams()
256
+ except Exception as exc:
257
+ logging.warning(f"Failed to fetch cameras from API: {exc}")
258
+ if allow_empty_start:
259
+ logging.info("Continuing with zero cameras (allow_empty_start=True)")
260
+ self.inputs_config = []
261
+ else:
262
+ raise
61
263
  else:
62
264
  self.inputs_config = inputs_config if isinstance(inputs_config, list) else [inputs_config]
63
265
 
266
+ # Check if we have cameras
64
267
  if not self.inputs_config:
65
- raise ValueError("No input configurations available")
268
+ if allow_empty_start:
269
+ logging.warning("Starting gateway with zero cameras - use camera_manager.add_camera() to add dynamically")
270
+ else:
271
+ raise ValueError("No input configurations available and allow_empty_start=False")
66
272
 
67
- # Validate inputs
273
+ # Validate inputs (only if we have any)
68
274
  for i, config in enumerate(self.inputs_config):
69
275
  if not isinstance(config, InputStream):
70
276
  raise ValueError(f"Input config {i} must be an InputStream instance")
71
277
 
72
- # Initialize CameraStreamer
73
- self.camera_streamer = CameraStreamer(
74
- session=self.session,
75
- service_id=streaming_gateway_id,
76
- server_type=server_type,
77
- video_codec=video_codec,
78
- gateway_util=self.gateway_util,
79
- )
278
+ # Initialize streaming backend based on configuration
279
+ # Options: use_nvdec, use_ffmpeg, use_gstreamer, use_async_workers (AsyncCameraWorker), or CameraStreamer
280
+ self.camera_streamer: Optional[CameraStreamer] = None
281
+ self.worker_manager: Optional[WorkerManager] = None
282
+ self.gstreamer_streamer: Optional[Any] = None # GStreamerCameraStreamer
283
+ self.gstreamer_worker_manager: Optional[Any] = None # GStreamerWorkerManager
284
+ self.ffmpeg_streamer: Optional[Any] = None # FFmpegCameraStreamer
285
+ self.ffmpeg_worker_manager: Optional[Any] = None # FFmpegWorkerManager
286
+ self.nvdec_worker_manager: Optional[Any] = None # NVDECWorkerManager
80
287
 
81
- # Initialize dynamic camera manager
82
- self.camera_manager = DynamicCameraManager(
83
- camera_streamer=self.camera_streamer,
84
- streaming_gateway_id=streaming_gateway_id,
85
- session=self.session
86
- )
288
+ if self.use_nvdec:
289
+ # NVDEC-based streaming flow (CUDA IPC output, static camera config)
290
+ logging.info(
291
+ f"Initializing NVDEC worker flow - GPUs: {nvdec_num_gpus}, "
292
+ f"pool_size: {nvdec_pool_size}, output: NV12 ({nvdec_frame_width}x{nvdec_frame_height})"
293
+ )
294
+
295
+ # Build stream config (unused by NVDEC but needed for interface consistency)
296
+ stream_config = build_stream_config(
297
+ gateway_util=self.gateway_util,
298
+ server_type=server_type,
299
+ service_id=streaming_gateway_id,
300
+ stream_maxlen=self.shm_slot_count,
301
+ )
302
+
303
+ # Convert InputStream configs to camera_config dicts
304
+ camera_configs = [
305
+ input_stream_to_camera_config(inp) for inp in self.inputs_config
306
+ ]
307
+
308
+ self.nvdec_worker_manager = NVDECWorkerManager(
309
+ camera_configs=camera_configs,
310
+ stream_config=stream_config,
311
+ gpu_id=nvdec_gpu_id,
312
+ num_gpus=nvdec_num_gpus,
313
+ nvdec_pool_size=nvdec_pool_size,
314
+ nvdec_burst_size=nvdec_burst_size,
315
+ frame_width=nvdec_frame_width,
316
+ frame_height=nvdec_frame_height,
317
+ num_slots=nvdec_num_slots,
318
+ target_fps=nvdec_target_fps,
319
+ )
320
+
321
+ # NVDEC uses static camera configuration - no dynamic camera manager
322
+ # Set camera_manager to None to indicate static mode
323
+ self.camera_manager = None
324
+ logging.info("NVDEC backend initialized (static camera configuration)")
325
+
326
+ elif self.use_ffmpeg:
327
+ # FFmpeg-based streaming flow
328
+ # Build stream config for workers
329
+ stream_config = build_stream_config(
330
+ gateway_util=self.gateway_util,
331
+ server_type=server_type,
332
+ service_id=streaming_gateway_id,
333
+ stream_maxlen=self.shm_slot_count,
334
+ )
335
+
336
+ # Create FFmpeg configuration
337
+ ffmpeg_config = FFmpegConfig(
338
+ hwaccel=ffmpeg_hwaccel,
339
+ threads=ffmpeg_threads,
340
+ low_latency=ffmpeg_low_latency,
341
+ pixel_format=ffmpeg_pixel_format,
342
+ )
343
+
344
+ if self.use_async_workers:
345
+ # FFmpeg with worker processes
346
+ logging.info(
347
+ f"Initializing FFmpeg worker flow - hwaccel: {ffmpeg_hwaccel}, "
348
+ f"threads: {ffmpeg_threads}"
349
+ )
350
+
351
+ # Convert InputStream configs to camera_config dicts
352
+ camera_configs = [
353
+ input_stream_to_camera_config(inp) for inp in self.inputs_config
354
+ ]
355
+
356
+ self.ffmpeg_worker_manager = FFmpegWorkerManager(
357
+ camera_configs=camera_configs,
358
+ stream_config=stream_config,
359
+ num_workers=num_workers,
360
+ max_cameras_per_worker=max_cameras_per_worker,
361
+ ffmpeg_config=ffmpeg_config,
362
+ shm_slot_count=self.shm_slot_count,
363
+ )
364
+
365
+ # Initialize dynamic camera manager for FFmpeg workers
366
+ self.camera_manager = DynamicCameraManagerForWorkers(
367
+ worker_manager=self.ffmpeg_worker_manager,
368
+ streaming_gateway_id=streaming_gateway_id,
369
+ session=self.session,
370
+ streaming_gateway=self,
371
+ )
372
+ else:
373
+ # FFmpeg single-threaded mode
374
+ logging.info(
375
+ f"Initializing FFmpeg CameraStreamer - hwaccel: {ffmpeg_hwaccel}"
376
+ )
377
+
378
+ self.ffmpeg_streamer = FFmpegCameraStreamer(
379
+ session=self.session,
380
+ service_id=streaming_gateway_id,
381
+ server_type=server_type,
382
+ video_codec=video_codec,
383
+ gateway_util=self.gateway_util,
384
+ ffmpeg_config=ffmpeg_config,
385
+ )
386
+
387
+ # Initialize dynamic camera manager for FFmpeg streamer
388
+ self.camera_manager = DynamicCameraManager(
389
+ camera_streamer=self.ffmpeg_streamer,
390
+ streaming_gateway_id=streaming_gateway_id,
391
+ session=self.session,
392
+ streaming_gateway=self,
393
+ )
394
+
395
+ elif self.use_gstreamer:
396
+ # GStreamer-based encoding flow
397
+ if self.use_async_workers:
398
+ # GStreamer with worker processes
399
+ logging.info(
400
+ f"Initializing GStreamer worker flow - encoder: {gstreamer_encoder}, "
401
+ f"codec: {gstreamer_codec}, gpu: {gstreamer_gpu_id}"
402
+ )
403
+
404
+ # Build stream config for workers
405
+ stream_config = build_stream_config(
406
+ gateway_util=self.gateway_util,
407
+ server_type=server_type,
408
+ service_id=streaming_gateway_id,
409
+ stream_maxlen=self.shm_slot_count,
410
+ )
411
+
412
+ # Convert InputStream configs to camera_config dicts
413
+ camera_configs = [
414
+ input_stream_to_camera_config(inp) for inp in self.inputs_config
415
+ ]
416
+
417
+ self.gstreamer_worker_manager = GStreamerWorkerManager(
418
+ camera_configs=camera_configs,
419
+ stream_config=stream_config,
420
+ num_workers=num_workers,
421
+ max_cameras_per_worker=max_cameras_per_worker,
422
+ gstreamer_encoder=gstreamer_encoder,
423
+ gstreamer_codec=gstreamer_codec,
424
+ gstreamer_preset=gstreamer_preset,
425
+ gpu_id=gstreamer_gpu_id,
426
+ platform=gstreamer_platform,
427
+ use_hardware_decode=gstreamer_use_hardware_decode,
428
+ use_hardware_jpeg=gstreamer_use_hardware_jpeg,
429
+ jetson_use_nvmm=gstreamer_jetson_use_nvmm,
430
+ frame_optimizer_mode=gstreamer_frame_optimizer_mode,
431
+ fallback_on_error=gstreamer_fallback_on_error,
432
+ verbose_pipeline_logging=gstreamer_verbose_logging,
433
+ )
434
+
435
+ # Initialize dynamic camera manager for GStreamer workers
436
+ # Use the same interface as WorkerManager
437
+ self.camera_manager = DynamicCameraManagerForWorkers(
438
+ worker_manager=self.gstreamer_worker_manager,
439
+ streaming_gateway_id=streaming_gateway_id,
440
+ session=self.session,
441
+ streaming_gateway=self,
442
+ )
443
+ else:
444
+ # GStreamer single-threaded mode
445
+ logging.info(
446
+ f"Initializing GStreamer CameraStreamer - encoder: {gstreamer_encoder}, "
447
+ f"codec: {gstreamer_codec}, gpu: {gstreamer_gpu_id}"
448
+ )
449
+
450
+ gst_config = GStreamerConfig(
451
+ encoder=gstreamer_encoder,
452
+ codec=gstreamer_codec,
453
+ preset=gstreamer_preset,
454
+ gpu_id=gstreamer_gpu_id,
455
+ platform=gstreamer_platform,
456
+ use_hardware_decode=gstreamer_use_hardware_decode,
457
+ use_hardware_jpeg=gstreamer_use_hardware_jpeg,
458
+ jetson_use_nvmm=gstreamer_jetson_use_nvmm,
459
+ frame_optimizer_mode=gstreamer_frame_optimizer_mode,
460
+ fallback_on_error=gstreamer_fallback_on_error,
461
+ verbose_pipeline_logging=gstreamer_verbose_logging,
462
+ )
463
+
464
+ self.gstreamer_streamer = GStreamerCameraStreamer(
465
+ session=self.session,
466
+ service_id=streaming_gateway_id,
467
+ server_type=server_type,
468
+ video_codec=video_codec,
469
+ gateway_util=self.gateway_util,
470
+ gstreamer_config=gst_config,
471
+ )
472
+
473
+ # Initialize dynamic camera manager for GStreamer streamer
474
+ # GStreamerCameraStreamer has the same API as CameraStreamer
475
+ self.camera_manager = DynamicCameraManager(
476
+ camera_streamer=self.gstreamer_streamer,
477
+ streaming_gateway_id=streaming_gateway_id,
478
+ session=self.session,
479
+ streaming_gateway=self,
480
+ )
481
+
482
+ elif self.use_async_workers:
483
+ # New async worker flow using WorkerManager
484
+ logging.info("Initializing async worker flow with WorkerManager")
485
+
486
+ # Build stream config for workers
487
+ stream_config = build_stream_config(
488
+ gateway_util=self.gateway_util,
489
+ server_type=server_type,
490
+ service_id=streaming_gateway_id,
491
+ stream_maxlen=self.shm_slot_count,
492
+ )
493
+
494
+ # Convert InputStream configs to camera_config dicts
495
+ camera_configs = [
496
+ input_stream_to_camera_config(inp) for inp in self.inputs_config
497
+ ]
498
+
499
+ self.worker_manager = WorkerManager(
500
+ camera_configs=camera_configs,
501
+ stream_config=stream_config,
502
+ num_workers=num_workers,
503
+ max_cameras_per_worker=max_cameras_per_worker,
504
+ shm_slot_count=self.shm_slot_count,
505
+ )
506
+
507
+ # Initialize dynamic camera manager for workers
508
+ self.camera_manager = DynamicCameraManagerForWorkers(
509
+ worker_manager=self.worker_manager,
510
+ streaming_gateway_id=streaming_gateway_id,
511
+ session=self.session,
512
+ streaming_gateway=self,
513
+ )
514
+ else:
515
+ # Original CameraStreamer flow
516
+ logging.info("Initializing original CameraStreamer flow")
517
+ self.camera_streamer = CameraStreamer(
518
+ session=self.session,
519
+ service_id=streaming_gateway_id,
520
+ server_type=server_type,
521
+ video_codec=video_codec,
522
+ gateway_util=self.gateway_util,
523
+ )
524
+
525
+ # Initialize dynamic camera manager for CameraStreamer
526
+ self.camera_manager = DynamicCameraManager(
527
+ camera_streamer=self.camera_streamer,
528
+ streaming_gateway_id=streaming_gateway_id,
529
+ session=self.session,
530
+ streaming_gateway=self,
531
+ )
87
532
 
88
533
  # Initialize with current camera configurations
89
- self.camera_manager.initialize_from_config(self.inputs_config)
534
+ # (skip for NVDEC which uses static configuration)
535
+ if self.camera_manager is not None:
536
+ self.camera_manager.initialize_from_config(self.inputs_config)
90
537
 
91
- # Initialize event system (if enabled)
538
+ # Initialize event system (if enabled and camera_manager exists)
539
+ # NVDEC doesn't support dynamic cameras, so event listening is disabled
92
540
  self.event_listener: Optional[EventListener] = None
93
-
94
- if self.enable_event_listening:
541
+
542
+ if self.enable_event_listening and self.camera_manager is not None:
95
543
  try:
96
544
  self.event_listener = EventListener(
97
545
  session=self.session,
@@ -101,12 +549,15 @@ class StreamingGateway:
101
549
  except Exception as e:
102
550
  logging.warning(f"Could not initialize event system: {e}")
103
551
  logging.info("Continuing without event listening")
552
+ elif self.enable_event_listening and self.use_nvdec:
553
+ logging.info("Event listening disabled for NVDEC backend (static camera configuration)")
104
554
 
105
555
  # State management
106
556
  self.is_streaming = False
107
557
  self._stop_event = threading.Event()
108
558
  self._state_lock = threading.RLock()
109
559
  self._my_stream_keys = set()
560
+ self._stream_key_to_camera_id = {} # Mapping of stream_key -> camera_id
110
561
  self._cleanup_registered = False
111
562
 
112
563
  # Statistics
@@ -163,9 +614,13 @@ class StreamingGateway:
163
614
  logging.warning("Streaming is already active")
164
615
  return False
165
616
 
617
+ # Check if we have cameras (allow empty if flag is set)
166
618
  if not self.inputs_config:
167
- logging.error("No input configurations available")
168
- return False
619
+ if self.allow_empty_start:
620
+ logging.warning("Starting streaming with zero cameras - awaiting dynamic camera addition")
621
+ else:
622
+ logging.error("No input configurations available")
623
+ return False
169
624
 
170
625
  # Force stop existing streams if requested
171
626
  self._stop_existing_streams()
@@ -173,45 +628,26 @@ class StreamingGateway:
173
628
  # Register as active
174
629
  self._register_as_active()
175
630
 
176
- # Start streaming for each input
177
- started_streams = []
178
631
  try:
179
- for i, input_config in enumerate(self.inputs_config):
180
- stream_key = input_config.camera_key or f"stream_{i}"
181
-
182
- # Register topic - generate default if not provided
183
- topic = input_config.camera_input_topic
184
- if not topic:
185
- # Generate default topic name
186
- camera_id = input_config.camera_id or stream_key
187
- topic = f"{camera_id}_input_topic"
188
- logging.warning(f"No input topic for camera {input_config.camera_key}, using default: {topic}")
189
-
190
- self.camera_streamer.register_stream_topic(stream_key, topic)
191
-
192
- # Start streaming
193
- success = self.camera_streamer.start_background_stream(
194
- input=input_config.source,
195
- fps=input_config.fps,
196
- stream_key=stream_key,
197
- stream_group_key=input_config.camera_group_key,
198
- quality=input_config.quality,
199
- width=input_config.width,
200
- height=input_config.height,
201
- simulate_video_file_stream=input_config.simulate_video_file_stream,
202
- camera_location=input_config.camera_location,
203
- )
632
+ if self.use_nvdec:
633
+ success = self._start_nvdec_worker_streaming()
634
+ elif self.use_ffmpeg:
635
+ if self.use_async_workers:
636
+ success = self._start_ffmpeg_worker_streaming()
637
+ else:
638
+ success = self._start_ffmpeg_streamer_streaming()
639
+ elif self.use_gstreamer:
640
+ if self.use_async_workers:
641
+ success = self._start_gstreamer_worker_streaming()
642
+ else:
643
+ success = self._start_gstreamer_streamer_streaming()
644
+ elif self.use_async_workers:
645
+ success = self._start_async_worker_streaming()
646
+ else:
647
+ success = self._start_camera_streamer_streaming()
204
648
 
205
- if not success:
206
- logging.error(f"Failed to start streaming for {input_config.source}")
207
- if started_streams:
208
- logging.info("Stopping already started streams")
209
- self.stop_streaming()
210
- return False
211
-
212
- started_streams.append(stream_key)
213
- self._my_stream_keys.add(stream_key)
214
- logging.info(f"Started streaming for camera: {input_config.camera_key}")
649
+ if not success:
650
+ return False
215
651
 
216
652
  with self._state_lock:
217
653
  self._stop_event.clear()
@@ -235,6 +671,314 @@ class StreamingGateway:
235
671
  logging.error(f"Error during cleanup: {cleanup_exc}")
236
672
  return False
237
673
 
674
+ def _start_async_worker_streaming(self) -> bool:
675
+ """Start streaming using new async worker flow.
676
+
677
+ Returns:
678
+ bool: True if started successfully, False otherwise
679
+ """
680
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
681
+ logging.info(f"Starting async worker streaming flow with {num_cameras} cameras")
682
+
683
+ # Build stream key mappings (if we have cameras)
684
+ if self.inputs_config:
685
+ for i, input_config in enumerate(self.inputs_config):
686
+ stream_key = input_config.camera_key or f"stream_{i}"
687
+ camera_id = input_config.camera_id or stream_key
688
+ self._stream_key_to_camera_id[stream_key] = camera_id
689
+ self._my_stream_keys.add(stream_key)
690
+
691
+ # Start the worker manager (this starts all worker processes)
692
+ # WorkerManager handles empty camera lists gracefully
693
+ try:
694
+ self.worker_manager.start()
695
+ logging.info(f"Started WorkerManager with {self.num_workers} workers, {num_cameras} cameras")
696
+ return True
697
+ except Exception as exc:
698
+ logging.error(f"Failed to start WorkerManager: {exc}", exc_info=True)
699
+ return False
700
+
701
+ def _start_camera_streamer_streaming(self) -> bool:
702
+ """Start streaming using original CameraStreamer flow.
703
+
704
+ Returns:
705
+ bool: True if started successfully, False otherwise
706
+ """
707
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
708
+ logging.info(f"Starting CameraStreamer streaming flow with {num_cameras} cameras")
709
+
710
+ # If no cameras, just return success (infrastructure is ready for dynamic cameras)
711
+ if not self.inputs_config:
712
+ logging.info("No cameras to start - awaiting dynamic camera addition")
713
+ return True
714
+
715
+ started_streams = []
716
+
717
+ for i, input_config in enumerate(self.inputs_config):
718
+ stream_key = input_config.camera_key or f"stream_{i}"
719
+
720
+ # Store camera_id mapping for metrics
721
+ camera_id = input_config.camera_id or stream_key
722
+ self._stream_key_to_camera_id[stream_key] = camera_id
723
+
724
+ # Register topic - generate default if not provided
725
+ topic = input_config.camera_input_topic
726
+ if not topic:
727
+ # Generate default topic name
728
+ topic = f"{camera_id}_input_topic"
729
+ logging.warning(f"No input topic for camera {input_config.camera_key}, using default: {topic}")
730
+
731
+ self.camera_streamer.register_stream_topic(stream_key, topic)
732
+
733
+ # Start streaming
734
+ success = self.camera_streamer.start_background_stream(
735
+ input=input_config.source,
736
+ fps=input_config.fps,
737
+ stream_key=stream_key,
738
+ stream_group_key=input_config.camera_group_key,
739
+ quality=input_config.quality,
740
+ width=input_config.width,
741
+ height=input_config.height,
742
+ simulate_video_file_stream=input_config.simulate_video_file_stream,
743
+ camera_location=input_config.camera_location,
744
+ )
745
+
746
+ if not success:
747
+ logging.error(f"Failed to start streaming for {input_config.source}")
748
+ if started_streams:
749
+ logging.info("Stopping already started streams")
750
+ self.stop_streaming()
751
+ return False
752
+
753
+ started_streams.append(stream_key)
754
+ self._my_stream_keys.add(stream_key)
755
+ logging.info(f"Started streaming for camera: {input_config.camera_key}")
756
+
757
+ return True
758
+
759
+ def _start_gstreamer_worker_streaming(self) -> bool:
760
+ """Start streaming using GStreamer worker processes.
761
+
762
+ Returns:
763
+ bool: True if started successfully, False otherwise
764
+ """
765
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
766
+ logging.info(
767
+ f"Starting GStreamer worker streaming with {num_cameras} cameras "
768
+ f"(encoder: {self.gstreamer_encoder}, codec: {self.gstreamer_codec})"
769
+ )
770
+
771
+ # Build stream key mappings
772
+ if self.inputs_config:
773
+ for i, input_config in enumerate(self.inputs_config):
774
+ stream_key = input_config.camera_key or f"stream_{i}"
775
+ camera_id = input_config.camera_id or stream_key
776
+ self._stream_key_to_camera_id[stream_key] = camera_id
777
+ self._my_stream_keys.add(stream_key)
778
+
779
+ # Start the GStreamer worker manager
780
+ try:
781
+ self.gstreamer_worker_manager.start()
782
+ logging.info(
783
+ f"Started GStreamerWorkerManager with {self.num_workers} workers, "
784
+ f"{num_cameras} cameras"
785
+ )
786
+ return True
787
+ except Exception as exc:
788
+ logging.error(f"Failed to start GStreamerWorkerManager: {exc}", exc_info=True)
789
+ return False
790
+
791
+ def _start_gstreamer_streamer_streaming(self) -> bool:
792
+ """Start streaming using GStreamer CameraStreamer (single-threaded).
793
+
794
+ Returns:
795
+ bool: True if started successfully, False otherwise
796
+ """
797
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
798
+ logging.info(
799
+ f"Starting GStreamer CameraStreamer with {num_cameras} cameras "
800
+ f"(encoder: {self.gstreamer_encoder}, codec: {self.gstreamer_codec})"
801
+ )
802
+
803
+ # If no cameras, return success (ready for dynamic cameras)
804
+ if not self.inputs_config:
805
+ logging.info("No cameras to start - awaiting dynamic camera addition")
806
+ return True
807
+
808
+ started_streams = []
809
+
810
+ for i, input_config in enumerate(self.inputs_config):
811
+ stream_key = input_config.camera_key or f"stream_{i}"
812
+
813
+ # Store camera_id mapping
814
+ camera_id = input_config.camera_id or stream_key
815
+ self._stream_key_to_camera_id[stream_key] = camera_id
816
+
817
+ # Register topic
818
+ topic = input_config.camera_input_topic
819
+ if not topic:
820
+ topic = f"{camera_id}_input_topic"
821
+ logging.warning(f"No input topic for camera {input_config.camera_key}, using default: {topic}")
822
+
823
+ self.gstreamer_streamer.register_stream_topic(stream_key, topic)
824
+
825
+ # Start streaming
826
+ success = self.gstreamer_streamer.start_background_stream(
827
+ input=input_config.source,
828
+ fps=input_config.fps,
829
+ stream_key=stream_key,
830
+ stream_group_key=input_config.camera_group_key,
831
+ quality=input_config.quality,
832
+ width=input_config.width,
833
+ height=input_config.height,
834
+ simulate_video_file_stream=input_config.simulate_video_file_stream,
835
+ camera_location=input_config.camera_location,
836
+ )
837
+
838
+ if not success:
839
+ logging.error(f"Failed to start GStreamer streaming for {input_config.source}")
840
+ if started_streams:
841
+ logging.info("Stopping already started streams")
842
+ self.stop_streaming()
843
+ return False
844
+
845
+ started_streams.append(stream_key)
846
+ self._my_stream_keys.add(stream_key)
847
+ logging.info(f"Started GStreamer streaming for camera: {input_config.camera_key}")
848
+
849
+ return True
850
+
851
+ def _start_nvdec_worker_streaming(self) -> bool:
852
+ """Start streaming using NVDEC hardware decode with CUDA IPC output.
853
+
854
+ NVDEC outputs NV12 frames to CUDA IPC ring buffers for zero-copy
855
+ GPU inference pipelines. Unlike other backends, NVDEC:
856
+ - Uses static camera configuration (no dynamic add/remove)
857
+ - Outputs to CUDA IPC ring buffers (not Redis/Kafka)
858
+ - Outputs NV12 format (50% smaller than RGB)
859
+
860
+ Returns:
861
+ bool: True if started successfully, False otherwise
862
+ """
863
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
864
+ logging.info(
865
+ f"Starting NVDEC worker streaming with {num_cameras} cameras "
866
+ f"(GPUs: {self.nvdec_num_gpus}, pool_size: {self.nvdec_pool_size}, "
867
+ f"output: NV12 {self.nvdec_frame_width}x{self.nvdec_frame_height})"
868
+ )
869
+
870
+ # Build stream key mappings for tracking
871
+ if self.inputs_config:
872
+ for i, input_config in enumerate(self.inputs_config):
873
+ stream_key = input_config.camera_key or f"stream_{i}"
874
+ camera_id = input_config.camera_id or stream_key
875
+ self._stream_key_to_camera_id[stream_key] = camera_id
876
+ self._my_stream_keys.add(stream_key)
877
+
878
+ # Start the NVDEC worker manager
879
+ try:
880
+ self.nvdec_worker_manager.start()
881
+ logging.info(
882
+ f"Started NVDECWorkerManager with {self.nvdec_num_gpus} GPU(s), "
883
+ f"{num_cameras} cameras"
884
+ )
885
+ return True
886
+ except Exception as exc:
887
+ logging.error(f"Failed to start NVDECWorkerManager: {exc}", exc_info=True)
888
+ return False
889
+
890
+ def _start_ffmpeg_worker_streaming(self) -> bool:
891
+ """Start streaming using FFmpeg worker processes.
892
+
893
+ Returns:
894
+ bool: True if started successfully, False otherwise
895
+ """
896
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
897
+ logging.info(
898
+ f"Starting FFmpeg worker streaming with {num_cameras} cameras "
899
+ f"(hwaccel: {self.ffmpeg_hwaccel}, threads: {self.ffmpeg_threads})"
900
+ )
901
+
902
+ # Build stream key mappings
903
+ if self.inputs_config:
904
+ for i, input_config in enumerate(self.inputs_config):
905
+ stream_key = input_config.camera_key or f"stream_{i}"
906
+ camera_id = input_config.camera_id or stream_key
907
+ self._stream_key_to_camera_id[stream_key] = camera_id
908
+ self._my_stream_keys.add(stream_key)
909
+
910
+ # Start the FFmpeg worker manager
911
+ try:
912
+ self.ffmpeg_worker_manager.start()
913
+ logging.info(
914
+ f"Started FFmpegWorkerManager with {self.num_workers} workers, "
915
+ f"{num_cameras} cameras"
916
+ )
917
+ return True
918
+ except Exception as exc:
919
+ logging.error(f"Failed to start FFmpegWorkerManager: {exc}", exc_info=True)
920
+ return False
921
+
922
+ def _start_ffmpeg_streamer_streaming(self) -> bool:
923
+ """Start streaming using FFmpeg CameraStreamer (single-threaded).
924
+
925
+ Returns:
926
+ bool: True if started successfully, False otherwise
927
+ """
928
+ num_cameras = len(self.inputs_config) if self.inputs_config else 0
929
+ logging.info(
930
+ f"Starting FFmpeg CameraStreamer with {num_cameras} cameras "
931
+ f"(hwaccel: {self.ffmpeg_hwaccel})"
932
+ )
933
+
934
+ # If no cameras, return success (ready for dynamic cameras)
935
+ if not self.inputs_config:
936
+ logging.info("No cameras to start - awaiting dynamic camera addition")
937
+ return True
938
+
939
+ started_streams = []
940
+
941
+ for i, input_config in enumerate(self.inputs_config):
942
+ stream_key = input_config.camera_key or f"stream_{i}"
943
+
944
+ # Store camera_id mapping
945
+ camera_id = input_config.camera_id or stream_key
946
+ self._stream_key_to_camera_id[stream_key] = camera_id
947
+
948
+ # Register topic
949
+ topic = input_config.camera_input_topic
950
+ if not topic:
951
+ topic = f"{camera_id}_input_topic"
952
+ logging.warning(f"No input topic for camera {input_config.camera_key}, using default: {topic}")
953
+
954
+ self.ffmpeg_streamer.register_stream_topic(stream_key, topic)
955
+
956
+ # Start streaming
957
+ success = self.ffmpeg_streamer.start_background_stream(
958
+ input=input_config.source,
959
+ fps=input_config.fps,
960
+ stream_key=stream_key,
961
+ stream_group_key=input_config.camera_group_key,
962
+ quality=input_config.quality,
963
+ width=input_config.width,
964
+ height=input_config.height,
965
+ simulate_video_file_stream=input_config.simulate_video_file_stream,
966
+ camera_location=input_config.camera_location,
967
+ )
968
+
969
+ if not success:
970
+ logging.error(f"Failed to start FFmpeg streaming for {input_config.source}")
971
+ if started_streams:
972
+ logging.info("Stopping already started streams")
973
+ self.stop_streaming()
974
+ return False
975
+
976
+ started_streams.append(stream_key)
977
+ self._my_stream_keys.add(stream_key)
978
+ logging.info(f"Started FFmpeg streaming for camera: {input_config.camera_key}")
979
+
980
+ return True
981
+
238
982
  def stop_streaming(self):
239
983
  """Stop all streaming operations."""
240
984
  with self._state_lock:
@@ -255,12 +999,94 @@ class StreamingGateway:
255
999
  except Exception as exc:
256
1000
  logging.error(f"Error stopping event listener: {exc}")
257
1001
 
258
- # Stop camera streaming
259
- if self.camera_streamer:
260
- try:
261
- self.camera_streamer.stop_streaming()
262
- except Exception as exc:
263
- logging.error(f"Error stopping camera streaming: {exc}")
1002
+ # Stop streaming backend based on which flow is active
1003
+ if self.use_nvdec:
1004
+ # Stop NVDEC backend
1005
+ if self.nvdec_worker_manager:
1006
+ try:
1007
+ logging.info("Stopping NVDECWorkerManager")
1008
+ self.nvdec_worker_manager.stop()
1009
+ logging.info("NVDEC worker manager stopped")
1010
+ except Exception as exc:
1011
+ logging.error(f"Error stopping NVDECWorkerManager: {exc}")
1012
+ elif self.use_ffmpeg:
1013
+ # Stop FFmpeg backends
1014
+ if self.use_async_workers:
1015
+ # Stop FFmpegWorkerManager
1016
+ if self.ffmpeg_worker_manager:
1017
+ try:
1018
+ logging.info("Stopping FFmpegWorkerManager")
1019
+ self.ffmpeg_worker_manager.stop()
1020
+ logging.info("FFmpeg worker manager stopped")
1021
+ except Exception as exc:
1022
+ logging.error(f"Error stopping FFmpegWorkerManager: {exc}")
1023
+ else:
1024
+ # Stop FFmpegCameraStreamer
1025
+ if self.ffmpeg_streamer:
1026
+ try:
1027
+ logging.info("Stopping FFmpegCameraStreamer")
1028
+ self.ffmpeg_streamer.stop_streaming()
1029
+ # Reset statistics for clean restart
1030
+ self.ffmpeg_streamer.reset_transmission_stats()
1031
+ # Clear pipeline references
1032
+ if hasattr(self.ffmpeg_streamer, 'pipelines'):
1033
+ self.ffmpeg_streamer.pipelines.clear()
1034
+ # Join streaming threads with timeout
1035
+ if hasattr(self.ffmpeg_streamer, 'streaming_threads'):
1036
+ for thread in self.ffmpeg_streamer.streaming_threads:
1037
+ if thread.is_alive():
1038
+ thread.join(timeout=5.0)
1039
+ self.ffmpeg_streamer.streaming_threads.clear()
1040
+ logging.info("FFmpeg cleanup completed")
1041
+ except Exception as exc:
1042
+ logging.error(f"Error stopping FFmpeg streaming: {exc}")
1043
+ elif self.use_gstreamer:
1044
+ # Stop GStreamer backends
1045
+ if self.use_async_workers:
1046
+ # Stop GStreamerWorkerManager
1047
+ if self.gstreamer_worker_manager:
1048
+ try:
1049
+ logging.info("Stopping GStreamerWorkerManager")
1050
+ self.gstreamer_worker_manager.stop()
1051
+ # Reset statistics for clean restart
1052
+ logging.info("Resetting GStreamer worker manager statistics")
1053
+ except Exception as exc:
1054
+ logging.error(f"Error stopping GStreamerWorkerManager: {exc}")
1055
+ else:
1056
+ # Stop GStreamerCameraStreamer
1057
+ if self.gstreamer_streamer:
1058
+ try:
1059
+ logging.info("Stopping GStreamerCameraStreamer")
1060
+ self.gstreamer_streamer.stop_streaming()
1061
+ # Reset statistics for clean restart
1062
+ self.gstreamer_streamer.reset_transmission_stats()
1063
+ # Clear pipeline references
1064
+ if hasattr(self.gstreamer_streamer, 'pipelines'):
1065
+ self.gstreamer_streamer.pipelines.clear()
1066
+ # Join streaming threads with timeout
1067
+ if hasattr(self.gstreamer_streamer, 'streaming_threads'):
1068
+ for thread in self.gstreamer_streamer.streaming_threads:
1069
+ if thread.is_alive():
1070
+ thread.join(timeout=5.0)
1071
+ self.gstreamer_streamer.streaming_threads.clear()
1072
+ logging.info("GStreamer cleanup completed")
1073
+ except Exception as exc:
1074
+ logging.error(f"Error stopping GStreamer streaming: {exc}")
1075
+ elif self.use_async_workers:
1076
+ # Stop WorkerManager
1077
+ if self.worker_manager:
1078
+ try:
1079
+ logging.info("Stopping WorkerManager")
1080
+ self.worker_manager.stop()
1081
+ except Exception as exc:
1082
+ logging.error(f"Error stopping WorkerManager: {exc}")
1083
+ else:
1084
+ # Stop CameraStreamer
1085
+ if self.camera_streamer:
1086
+ try:
1087
+ self.camera_streamer.stop_streaming()
1088
+ except Exception as exc:
1089
+ logging.error(f"Error stopping camera streaming: {exc}")
264
1090
 
265
1091
  # Always attempt to update status to "stopped", even if other steps fail
266
1092
  # This is critical for proper gateway lifecycle management
@@ -295,6 +1121,10 @@ class StreamingGateway:
295
1121
 
296
1122
  logging.info(f"Streaming stopped (status updated: {status_updated})")
297
1123
 
1124
+ def get_camera_id_for_stream_key(self, stream_key: str) -> Optional[str]:
1125
+ """Get camera_id for a given stream_key."""
1126
+ return self._stream_key_to_camera_id.get(stream_key)
1127
+
298
1128
  def get_statistics(self) -> Dict:
299
1129
  """Get streaming statistics."""
300
1130
  with self._state_lock:
@@ -307,14 +1137,100 @@ class StreamingGateway:
307
1137
 
308
1138
  stats["is_streaming"] = self.is_streaming
309
1139
  stats["my_stream_keys"] = list(self._my_stream_keys)
1140
+ stats["stream_key_to_camera_id"] = self._stream_key_to_camera_id.copy()
310
1141
  stats["event_listening_enabled"] = self.enable_event_listening
1142
+ stats["use_async_workers"] = self.use_async_workers
1143
+ stats["use_gstreamer"] = self.use_gstreamer
1144
+ stats["use_ffmpeg"] = self.use_ffmpeg
1145
+ stats["use_nvdec"] = self.use_nvdec
311
1146
 
312
- # Add camera streamer statistics
313
- if self.camera_streamer:
314
- try:
315
- stats["transmission_stats"] = self.camera_streamer.get_transmission_stats()
316
- except Exception as exc:
317
- logging.warning(f"Failed to get transmission stats: {exc}")
1147
+ # Add backend-specific statistics
1148
+ if self.use_nvdec:
1149
+ # NVDEC statistics
1150
+ stats["nvdec_config"] = {
1151
+ "gpu_id": self.nvdec_gpu_id,
1152
+ "num_gpus": self.nvdec_num_gpus,
1153
+ "pool_size": self.nvdec_pool_size,
1154
+ "burst_size": self.nvdec_burst_size,
1155
+ "frame_width": self.nvdec_frame_width,
1156
+ "frame_height": self.nvdec_frame_height,
1157
+ "num_slots": self.nvdec_num_slots,
1158
+ "target_fps": self.nvdec_target_fps,
1159
+ }
1160
+ if self.nvdec_worker_manager:
1161
+ try:
1162
+ stats["worker_stats"] = self.nvdec_worker_manager.get_worker_statistics()
1163
+ stats["camera_assignments"] = self.nvdec_worker_manager.get_camera_assignments()
1164
+ except Exception as exc:
1165
+ logging.warning(f"Failed to get NVDEC worker stats: {exc}")
1166
+ elif self.use_ffmpeg:
1167
+ # FFmpeg statistics
1168
+ stats["ffmpeg_config"] = {
1169
+ "hwaccel": self.ffmpeg_hwaccel,
1170
+ "threads": self.ffmpeg_threads,
1171
+ "low_latency": self.ffmpeg_low_latency,
1172
+ "pixel_format": self.ffmpeg_pixel_format,
1173
+ }
1174
+ if self.use_async_workers:
1175
+ # FFmpegWorkerManager statistics
1176
+ if self.ffmpeg_worker_manager:
1177
+ try:
1178
+ stats["worker_stats"] = self.ffmpeg_worker_manager.get_worker_statistics()
1179
+ stats["camera_assignments"] = self.ffmpeg_worker_manager.get_camera_assignments()
1180
+ except Exception as exc:
1181
+ logging.warning(f"Failed to get FFmpeg worker stats: {exc}")
1182
+ else:
1183
+ # FFmpegCameraStreamer statistics
1184
+ if self.ffmpeg_streamer:
1185
+ try:
1186
+ stats["transmission_stats"] = self.ffmpeg_streamer.get_transmission_stats()
1187
+ except Exception as exc:
1188
+ logging.warning(f"Failed to get FFmpeg transmission stats: {exc}")
1189
+ elif self.use_gstreamer:
1190
+ # GStreamer statistics
1191
+ stats["gstreamer_config"] = {
1192
+ "encoder": self.gstreamer_encoder,
1193
+ "codec": self.gstreamer_codec,
1194
+ "preset": self.gstreamer_preset,
1195
+ "gpu_id": self.gstreamer_gpu_id,
1196
+ "platform": self.gstreamer_platform,
1197
+ "use_hardware_decode": self.gstreamer_use_hardware_decode,
1198
+ "use_hardware_jpeg": self.gstreamer_use_hardware_jpeg,
1199
+ "jetson_use_nvmm": self.gstreamer_jetson_use_nvmm,
1200
+ "frame_optimizer_mode": self.gstreamer_frame_optimizer_mode,
1201
+ "fallback_on_error": self.gstreamer_fallback_on_error,
1202
+ "verbose_logging": self.gstreamer_verbose_logging,
1203
+ }
1204
+ if self.use_async_workers:
1205
+ # GStreamerWorkerManager statistics
1206
+ if self.gstreamer_worker_manager:
1207
+ try:
1208
+ stats["worker_stats"] = self.gstreamer_worker_manager.get_worker_statistics()
1209
+ stats["camera_assignments"] = self.gstreamer_worker_manager.get_camera_assignments()
1210
+ except Exception as exc:
1211
+ logging.warning(f"Failed to get GStreamer worker stats: {exc}")
1212
+ else:
1213
+ # GStreamerCameraStreamer statistics
1214
+ if self.gstreamer_streamer:
1215
+ try:
1216
+ stats["transmission_stats"] = self.gstreamer_streamer.get_transmission_stats()
1217
+ except Exception as exc:
1218
+ logging.warning(f"Failed to get GStreamer transmission stats: {exc}")
1219
+ elif self.use_async_workers:
1220
+ # WorkerManager statistics
1221
+ if self.worker_manager:
1222
+ try:
1223
+ stats["worker_stats"] = self.worker_manager.get_worker_statistics()
1224
+ stats["camera_assignments"] = self.worker_manager.get_camera_assignments()
1225
+ except Exception as exc:
1226
+ logging.warning(f"Failed to get worker manager stats: {exc}")
1227
+ else:
1228
+ # CameraStreamer statistics
1229
+ if self.camera_streamer:
1230
+ try:
1231
+ stats["transmission_stats"] = self.camera_streamer.get_transmission_stats()
1232
+ except Exception as exc:
1233
+ logging.warning(f"Failed to get transmission stats: {exc}")
318
1234
 
319
1235
  # Add camera manager statistics
320
1236
  if self.camera_manager:
@@ -353,6 +1269,38 @@ class StreamingGateway:
353
1269
  "streaming_gateway_id": self.streaming_gateway_id,
354
1270
  "inputs_config": inputs_config_dict,
355
1271
  "force_restart": self.force_restart,
1272
+ "use_async_workers": self.use_async_workers,
1273
+ "num_workers": self.num_workers,
1274
+ "max_cameras_per_worker": self.max_cameras_per_worker,
1275
+ # FFmpeg configuration
1276
+ "use_ffmpeg": self.use_ffmpeg,
1277
+ "ffmpeg_hwaccel": self.ffmpeg_hwaccel,
1278
+ "ffmpeg_threads": self.ffmpeg_threads,
1279
+ "ffmpeg_low_latency": self.ffmpeg_low_latency,
1280
+ "ffmpeg_pixel_format": self.ffmpeg_pixel_format,
1281
+ # GStreamer configuration
1282
+ "use_gstreamer": self.use_gstreamer,
1283
+ "gstreamer_encoder": self.gstreamer_encoder,
1284
+ "gstreamer_codec": self.gstreamer_codec,
1285
+ "gstreamer_preset": self.gstreamer_preset,
1286
+ "gstreamer_gpu_id": self.gstreamer_gpu_id,
1287
+ "gstreamer_platform": self.gstreamer_platform,
1288
+ "gstreamer_use_hardware_decode": self.gstreamer_use_hardware_decode,
1289
+ "gstreamer_use_hardware_jpeg": self.gstreamer_use_hardware_jpeg,
1290
+ "gstreamer_jetson_use_nvmm": self.gstreamer_jetson_use_nvmm,
1291
+ "gstreamer_frame_optimizer_mode": self.gstreamer_frame_optimizer_mode,
1292
+ "gstreamer_fallback_on_error": self.gstreamer_fallback_on_error,
1293
+ "gstreamer_verbose_logging": self.gstreamer_verbose_logging,
1294
+ # NVDEC configuration
1295
+ "use_nvdec": self.use_nvdec,
1296
+ "nvdec_gpu_id": self.nvdec_gpu_id,
1297
+ "nvdec_num_gpus": self.nvdec_num_gpus,
1298
+ "nvdec_pool_size": self.nvdec_pool_size,
1299
+ "nvdec_burst_size": self.nvdec_burst_size,
1300
+ "nvdec_frame_width": self.nvdec_frame_width,
1301
+ "nvdec_frame_height": self.nvdec_frame_height,
1302
+ "nvdec_num_slots": self.nvdec_num_slots,
1303
+ "nvdec_target_fps": self.nvdec_target_fps,
356
1304
  }
357
1305
 
358
1306
  def _emergency_cleanup(self):