matrice-streaming 0.1.67__tar.gz → 0.1.69__tar.gz

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 (71) hide show
  1. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/PKG-INFO +1 -1
  2. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/matrice_streaming.egg-info/PKG-INFO +1 -1
  3. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +141 -33
  4. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/LICENSE.txt +0 -0
  5. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/README.md +0 -0
  6. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/matrice_streaming.egg-info/SOURCES.txt +0 -0
  7. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/matrice_streaming.egg-info/dependency_links.txt +0 -0
  8. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/matrice_streaming.egg-info/not-zip-safe +0 -0
  9. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/matrice_streaming.egg-info/top_level.txt +0 -0
  10. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/pyproject.toml +0 -0
  11. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/setup.cfg +0 -0
  12. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/setup.py +0 -0
  13. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/__init__.py +0 -0
  14. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/client/__init__.py +0 -0
  15. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/client/client.py +0 -0
  16. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/client/client_utils.py +0 -0
  17. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/deployment/__init__.py +0 -0
  18. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/deployment/camera_manager.py +0 -0
  19. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/deployment/deployment.py +0 -0
  20. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/deployment/inference_pipeline.py +0 -0
  21. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/deployment/streaming_gateway_manager.py +0 -0
  22. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/deployment/todo.txt +0 -0
  23. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/py.typed +0 -0
  24. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/__init__.py +0 -0
  25. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/ARCHITECTURE.md +0 -0
  26. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/__init__.py +0 -0
  27. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +0 -0
  28. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +0 -0
  29. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +0 -0
  30. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +0 -0
  31. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/encoder_manager.py +0 -0
  32. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +0 -0
  33. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +0 -0
  34. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +0 -0
  35. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +0 -0
  36. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/frame_processor.py +0 -0
  37. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +0 -0
  38. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +0 -0
  39. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +0 -0
  40. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +0 -0
  41. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +0 -0
  42. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +0 -0
  43. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/retry_manager.py +0 -0
  44. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +0 -0
  45. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +0 -0
  46. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +0 -0
  47. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/README.md +0 -0
  48. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/__init__.py +0 -0
  49. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/benchmark.py +0 -0
  50. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +0 -0
  51. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/debug_stream_backend.py +0 -0
  52. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +0 -0
  53. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/debug_utils.py +0 -0
  54. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/example_debug_streaming.py +0 -0
  55. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/debug/test_videoplayback.py +0 -0
  56. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/dynamic_camera_manager.py +0 -0
  57. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/event_listener.py +0 -0
  58. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/metrics_reporter.py +0 -0
  59. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/streaming_action.py +0 -0
  60. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/streaming_gateway.py +0 -0
  61. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/streaming_gateway_utils.py +0 -0
  62. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/src/matrice_streaming/streaming_gateway/streaming_status_listener.py +0 -0
  63. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_async_infrastructure.py +0 -0
  64. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_batch_auto_calculation.py +0 -0
  65. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_batching_verification.py +0 -0
  66. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_e2e_production.py +0 -0
  67. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_flatten_binary.py +0 -0
  68. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_gstreamer_integration.py +0 -0
  69. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_msgpack_fix.py +0 -0
  70. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_phase1_unit.py +0 -0
  71. {matrice_streaming-0.1.67 → matrice_streaming-0.1.69}/tests/test_phase2_scaling.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_streaming
3
- Version: 0.1.67
3
+ Version: 0.1.69
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_streaming
3
- Version: 0.1.67
3
+ Version: 0.1.69
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -96,8 +96,23 @@ except (ImportError, AttributeError, RuntimeError, TypeError):
96
96
  CudaIpcRingBuffer = None
97
97
  GlobalFrameCounter = None
98
98
 
99
+ try:
100
+ from matrice_common.stream.gpu_camera_map import GpuCameraMap
101
+ GPU_CAMERA_MAP_AVAILABLE = True
102
+ except ImportError:
103
+ GPU_CAMERA_MAP_AVAILABLE = False
104
+ GpuCameraMap = None
105
+
99
106
  logger = logging.getLogger(__name__)
100
107
 
108
+ # =============================================================================
109
+ # Fixed dimensions for NV12 output - ALL cameras resize to this size
110
+ # This matches the TensorRT model input requirements (640x640)
111
+ # =============================================================================
112
+ TARGET_WIDTH = 640
113
+ TARGET_HEIGHT = 640
114
+ NV12_HEIGHT = TARGET_HEIGHT + TARGET_HEIGHT // 2 # 960 = H * 1.5 for NV12 format
115
+
101
116
  def setup_logging(quiet: bool = True):
102
117
  """Configure logging level based on quiet mode."""
103
118
  level = logging.WARNING if quiet else logging.INFO
@@ -407,6 +422,8 @@ class StreamState:
407
422
  width: int = 640
408
423
  height: int = 640
409
424
  empty_packets: int = 0
425
+ decode_errors: int = 0 # Consecutive decode errors
426
+ MAX_DECODE_ERRORS: int = 5 # Restart demuxer after this many consecutive errors
410
427
 
411
428
 
412
429
  # =============================================================================
@@ -420,8 +437,8 @@ def _get_nv12_resize_kernel():
420
437
  """Get or compile the NV12 resize kernel.
421
438
 
422
439
  This kernel resizes NV12 directly (no color conversion).
423
- Output: concatenated Y (H×W) + UV ((H/2)×W) = H×W×1.5 bytes
424
- This is 50% smaller than RGB (H×W×3 bytes).
440
+ Output: concatenated Y (H*W) + UV ((H/2)*W) = H*W*1.5 bytes
441
+ This is 50% smaller than RGB (H*W*3 bytes).
425
442
 
426
443
  Consumer will do: NV12→RGB→CHW→FP16 in one fused kernel.
427
444
  """
@@ -429,9 +446,9 @@ def _get_nv12_resize_kernel():
429
446
  if _nv12_resize_kernel is None and CUPY_AVAILABLE:
430
447
  _nv12_resize_kernel = cp.RawKernel(r'''
431
448
  extern "C" __global__ void nv12_resize(
432
- const unsigned char* src_y, // Source Y plane
433
- const unsigned char* src_uv, // Source UV plane (interleaved)
434
- unsigned char* dst, // Output: Y (H×W) followed by UV ((H/2)×W)
449
+ const unsigned char* src_y,
450
+ const unsigned char* src_uv,
451
+ unsigned char* dst,
435
452
  int src_h, int src_w,
436
453
  int dst_h, int dst_w,
437
454
  int y_stride, int uv_stride
@@ -477,8 +494,8 @@ def nv12_resize(y_plane: cp.ndarray, uv_plane: cp.ndarray,
477
494
  dst_h: int = 640, dst_w: int = 640) -> cp.ndarray:
478
495
  """Resize NV12 without color conversion.
479
496
 
480
- Output: concatenated Y (H×W) + UV ((H/2)×W) as single buffer.
481
- Total size: H×W + (H/2)×W = H×W×1.5 bytes (50% of RGB).
497
+ Output: concatenated Y (H*W) + UV ((H/2)*W) as single buffer.
498
+ Total size: H*W + (H/2)*W = H*W*1.5 bytes (50% of RGB).
482
499
  """
483
500
  kernel = _get_nv12_resize_kernel()
484
501
  if kernel is None:
@@ -504,7 +521,7 @@ def surface_to_nv12(frame, target_h: int = 640, target_w: int = 640) -> Optional
504
521
  """Convert NVDEC surface to resized NV12 (50% smaller than RGB).
505
522
 
506
523
  Output: (H + H/2, W) uint8 - concatenated Y + UV planes.
507
- Total size: H×W×1.5 bytes (vs H×W×3 for RGB).
524
+ Total size: H*W*1.5 bytes (vs H*W*3 for RGB).
508
525
  """
509
526
  if not CUPY_AVAILABLE or frame is None:
510
527
  return None
@@ -552,11 +569,14 @@ def surface_to_nv12(frame, target_h: int = 640, target_w: int = 640) -> Optional
552
569
  return nv12_frame[:, :, cp.newaxis] if nv12_frame is not None else None
553
570
 
554
571
  except Exception as e:
555
- # Safely encode error message (some CUDA errors contain non-ASCII chars like '×')
572
+ # Safely handle any characters in error message (CUDA errors may contain Unicode like ×)
556
573
  try:
557
574
  err_msg = str(e).encode('ascii', errors='replace').decode('ascii')
558
575
  except Exception:
559
- err_msg = "unknown error"
576
+ try:
577
+ err_msg = repr(str(e))[:200]
578
+ except Exception:
579
+ err_msg = "failed to format error message"
560
580
  logger.warning(f"surface_to_nv12 failed: {err_msg}")
561
581
  return None
562
582
 
@@ -569,7 +589,7 @@ class NVDECDecoderPool:
569
589
  """Pool of NVDEC decoders that time-multiplex streams.
570
590
 
571
591
  Each decoder is exclusively owned by one worker thread.
572
- Outputs NV12: 1.5×H×W bytes (50% smaller than RGB).
592
+ Outputs NV12: 1.5*H*W bytes (50% smaller than RGB).
573
593
  """
574
594
 
575
595
  def __init__(self, pool_size: int, gpu_id: int = 0):
@@ -661,18 +681,44 @@ class NVDECDecoderPool:
661
681
  break
662
682
 
663
683
  frames_before = frames_this_stream
664
- for surface in decoder.Decode(packet):
665
- tensor = surface_to_nv12(surface, target_h, target_w)
666
-
667
- if tensor is not None:
668
- decoded_frames.append((stream.camera_id, tensor))
669
- frames_this_stream += 1
670
- stream.frames_decoded += 1
671
- total_frames += 1
672
- stream.empty_packets = 0
673
684
 
674
- if frames_this_stream >= frames_per_stream:
675
- break
685
+ # Wrap decode loop in try/except to catch decode errors
686
+ try:
687
+ for surface in decoder.Decode(packet):
688
+ tensor = surface_to_nv12(surface, target_h, target_w)
689
+
690
+ if tensor is not None:
691
+ decoded_frames.append((stream.camera_id, tensor))
692
+ frames_this_stream += 1
693
+ stream.frames_decoded += 1
694
+ total_frames += 1
695
+ stream.empty_packets = 0
696
+ stream.decode_errors = 0 # Reset on success
697
+
698
+ if frames_this_stream >= frames_per_stream:
699
+ break
700
+
701
+ except Exception as decode_err:
702
+ # Handle decode errors - these can happen with corrupted packets
703
+ stream.decode_errors += 1
704
+ if stream.decode_errors == 1:
705
+ # Only log first error per stream to avoid spam
706
+ logger.warning(f"{stream.camera_id}: Decode error: {decode_err}")
707
+
708
+ if stream.decode_errors >= stream.MAX_DECODE_ERRORS:
709
+ # Too many consecutive errors - restart demuxer
710
+ logger.warning(
711
+ f"{stream.camera_id}: {stream.decode_errors} consecutive decode errors, "
712
+ f"restarting demuxer"
713
+ )
714
+ try:
715
+ stream.demuxer = nvc.CreateDemuxer(stream.video_path)
716
+ stream.decode_errors = 0
717
+ stream.empty_packets = 0
718
+ logger.info(f"{stream.camera_id}: Demuxer restarted successfully")
719
+ except Exception as restart_err:
720
+ logger.error(f"{stream.camera_id}: Failed to restart demuxer: {restart_err}")
721
+ break # Exit this stream's loop
676
722
 
677
723
  if frames_this_stream == frames_before:
678
724
  stream.empty_packets += 1
@@ -680,7 +726,17 @@ class NVDECDecoderPool:
680
726
  stream.demuxer = nvc.CreateDemuxer(stream.video_path)
681
727
  stream.empty_packets = 0
682
728
 
683
- except Exception:
729
+ except Exception as demux_err:
730
+ # Handle demux errors
731
+ logger.warning(f"{stream.camera_id}: Demux error: {demux_err}")
732
+ stream.decode_errors += 1
733
+ if stream.decode_errors >= stream.MAX_DECODE_ERRORS:
734
+ try:
735
+ stream.demuxer = nvc.CreateDemuxer(stream.video_path)
736
+ stream.decode_errors = 0
737
+ logger.info(f"{stream.camera_id}: Demuxer restarted after demux errors")
738
+ except Exception:
739
+ pass
684
740
  break
685
741
 
686
742
  if frames_this_stream >= frames_per_stream:
@@ -775,16 +831,28 @@ def nvdec_pool_worker(
775
831
 
776
832
  try:
777
833
  with cuda_stream:
834
+ # Always use fixed TARGET dimensions to ensure consistent NV12 output
778
835
  num_frames, decoded_frames = pool.decode_round(
779
836
  decoder_idx,
780
837
  frames_per_stream=burst_size,
781
- target_h=target_h,
782
- target_w=target_w
838
+ target_h=TARGET_HEIGHT, # Always 640
839
+ target_w=TARGET_WIDTH # Always 640
783
840
  )
784
841
 
785
842
  for cam_id, tensor in decoded_frames:
786
843
  if cam_id in ring_buffers:
787
844
  try:
845
+ # Validate frame shape matches expected NV12 dimensions
846
+ expected_shape = (NV12_HEIGHT, TARGET_WIDTH, 1) # (960, 640, 1)
847
+ if tensor.shape != expected_shape:
848
+ local_errors += 1
849
+ if local_errors <= 5:
850
+ logger.error(
851
+ f"Worker {worker_id} shape mismatch for {cam_id}: "
852
+ f"got {tensor.shape}, expected {expected_shape}"
853
+ )
854
+ continue # Skip this frame
855
+
788
856
  ring_buffers[cam_id].write_frame_fast(tensor, sync=False)
789
857
  local_frames += 1
790
858
  frames_since_counter_update += 1
@@ -808,8 +876,14 @@ def nvdec_pool_worker(
808
876
  if local_errors <= 3:
809
877
  logger.error(f"Worker {worker_id} write error: {e}")
810
878
 
811
- if decoded_frames and len(ring_buffers) > 0:
812
- next(iter(ring_buffers.values())).sync_writes()
879
+ if decoded_frames:
880
+ # Sync all buffers that received frames in this round
881
+ # Critical for cross-container IPC - ensures GPU writes are visible
882
+ synced_cams = set()
883
+ for cam_id, _ in decoded_frames:
884
+ if cam_id in ring_buffers and cam_id not in synced_cams:
885
+ ring_buffers[cam_id].sync_writes()
886
+ synced_cams.add(cam_id)
813
887
 
814
888
  if num_frames == 0:
815
889
  time.sleep(0.0001)
@@ -923,30 +997,64 @@ def nvdec_pool_process(
923
997
  })
924
998
  return
925
999
 
926
- # Create NV12 ring buffers: (H + H/2, W, 1) = 0.6 MB/frame
1000
+ # Create NV12 ring buffers with FIXED dimensions (640x640 -> 960x640 for NV12)
1001
+ # ALL cameras resize to TARGET_WIDTH x TARGET_HEIGHT regardless of source resolution
927
1002
  ring_buffers: Dict[str, CudaIpcRingBuffer] = {}
928
- frame_size_mb = target_h * target_w * 1.5 / 1e6
1003
+ frame_size_mb = TARGET_WIDTH * NV12_HEIGHT * 1 / 1e6 # 640 * 960 * 1 channel
929
1004
 
930
1005
  try:
1006
+ # Initialize GPU camera map (producer side) - process 0 creates, others connect
1007
+ gpu_map = None
1008
+ if GPU_CAMERA_MAP_AVAILABLE:
1009
+ gpu_map = GpuCameraMap(is_producer=True)
1010
+ if process_id == 0:
1011
+ if gpu_map.initialize():
1012
+ logger.info(f"Process {process_id}: GpuCameraMap initialized")
1013
+ else:
1014
+ logger.warning(f"Process {process_id}: Failed to initialize GpuCameraMap")
1015
+ gpu_map = None
1016
+ else:
1017
+ # Wait for process 0 to initialize
1018
+ for retry in range(50):
1019
+ if gpu_map.connect():
1020
+ logger.info(f"Process {process_id}: Connected to GpuCameraMap")
1021
+ break
1022
+ time.sleep(0.1)
1023
+ else:
1024
+ logger.warning(f"Process {process_id}: Failed to connect to GpuCameraMap")
1025
+ gpu_map = None
1026
+
1027
+ # Collect GPU mappings for bulk write
1028
+ gpu_mappings = {}
1029
+
931
1030
  for i, config in enumerate(camera_configs):
1031
+ # Use fixed dimensions - ALL cameras output 640x640 (960x640 for NV12)
932
1032
  rb = CudaIpcRingBuffer.create_producer(
933
1033
  config.camera_id,
934
1034
  gpu_id=config.gpu_id,
935
1035
  num_slots=num_slots,
936
- width=config.width,
937
- height=config.height + config.height // 2, # H * 1.5 for NV12
1036
+ width=TARGET_WIDTH, # Always 640
1037
+ height=NV12_HEIGHT, # Always 960 (640 * 1.5 for NV12)
938
1038
  channels=1,
939
1039
  )
940
1040
  ring_buffers[config.camera_id] = rb
941
1041
 
1042
+ # Track GPU assignment for this camera
1043
+ gpu_mappings[config.camera_id] = config.gpu_id
1044
+
942
1045
  pool.assign_stream(
943
1046
  stream_id=i,
944
1047
  camera_id=config.camera_id,
945
1048
  video_path=config.video_path,
946
- width=config.width,
947
- height=config.height
1049
+ width=TARGET_WIDTH, # Use fixed width
1050
+ height=TARGET_HEIGHT # Use fixed height
948
1051
  )
949
1052
 
1053
+ # Write all GPU mappings at once
1054
+ if gpu_map and gpu_mappings:
1055
+ gpu_map.set_bulk_mapping(gpu_mappings)
1056
+ logger.info(f"Process {process_id}: Wrote {len(gpu_mappings)} camera-GPU mappings")
1057
+
950
1058
  logger.info(f"Process {process_id}: {pool.actual_pool_size} decoders, "
951
1059
  f"{len(camera_configs)} streams, NV12 ({frame_size_mb:.1f} MB/frame)")
952
1060