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
@@ -5,6 +5,7 @@ import threading
5
5
  import time
6
6
  from typing import Dict, Optional, Union, Any
7
7
  from matrice_common.stream.matrice_stream import MatriceStream, StreamType
8
+ from matrice_common.optimize import FrameOptimizer
8
9
 
9
10
  # Import our modular components
10
11
  from .video_capture_manager import VideoCaptureManager
@@ -40,9 +41,11 @@ class CameraStreamer:
40
41
  gateway_util: StreamingGatewayUtil = None,
41
42
  connection_refresh_threshold: int = 10,
42
43
  connection_refresh_interval: float = 60.0,
44
+ frame_optimizer_enabled: bool = True,
45
+ frame_optimizer_config: Optional[Dict[str, Any]] = None,
43
46
  ):
44
47
  """Initialize CameraStreamer.
45
-
48
+
46
49
  Args:
47
50
  session: Session object for making RPC calls
48
51
  service_id: ID of the deployment
@@ -55,6 +58,8 @@ class CameraStreamer:
55
58
  gateway_util: StreamingGatewayUtil instance for fetching connection info
56
59
  connection_refresh_threshold: Number of consecutive failures before refreshing connection
57
60
  connection_refresh_interval: Minimum seconds between connection refresh attempts
61
+ frame_optimizer_enabled: Enable frame optimization to skip similar frames
62
+ frame_optimizer_config: Configuration for FrameOptimizer (scale, diff_threshold, etc.)
58
63
  """
59
64
  self.session = session
60
65
  self.service_id = service_id
@@ -66,6 +71,17 @@ class CameraStreamer:
66
71
  self.encoder_manager = EncoderManager(h265_mode, h265_quality, use_hardware)
67
72
  self.statistics = StreamStatistics()
68
73
  self.message_builder = StreamMessageBuilder(service_id, strip_input_content)
74
+
75
+ # Initialize frame optimizer for skipping similar frames
76
+ optimizer_config = frame_optimizer_config or {}
77
+ self.frame_optimizer = FrameOptimizer(
78
+ enabled=frame_optimizer_enabled,
79
+ scale=optimizer_config.get("scale", 0.4),
80
+ diff_threshold=optimizer_config.get("diff_threshold", 15),
81
+ similarity_threshold=optimizer_config.get("similarity_threshold", 0.05),
82
+ bg_update_interval=optimizer_config.get("bg_update_interval", 10),
83
+ )
84
+ self._last_sent_frame_ids: Dict[str, str] = {} # stream_key -> last sent frame_id
69
85
 
70
86
  # Video codec configuration
71
87
  self.video_codec = self._normalize_video_codec(video_codec, h265_mode)
@@ -82,6 +98,12 @@ class CameraStreamer:
82
98
  self.connection_refresh_interval = connection_refresh_interval
83
99
  self._send_failure_count = 0
84
100
  self._last_connection_refresh_time = 0.0
101
+
102
+ # Metrics logging state
103
+ self._last_metrics_log_time = time.time()
104
+ self._last_aggregated_log_time = time.time() # Separate tracker for aggregated stats
105
+ self._metrics_log_interval = 30.0 # seconds (configurable)
106
+ self._aggregated_log_interval = 60.0 # seconds for aggregated stats
85
107
  self._connection_lock = threading.RLock()
86
108
 
87
109
  # Initialize MatriceStream
@@ -93,7 +115,17 @@ class CameraStreamer:
93
115
  self.logger = logging.getLogger(__name__)
94
116
  self.logger.error("No gateway_util provided, connection refresh will be disabled")
95
117
  self.stream_config = {}
96
-
118
+
119
+ # Add Redis configuration with conservative batching defaults
120
+ # (Worker manager will automatically optimize based on camera count)
121
+ if self.server_type == "redis":
122
+ self.stream_config.update({
123
+ 'pool_max_connections': 500, # High connection pool for 1000+ cameras
124
+ 'enable_batching': True, # Enable message batching
125
+ 'batch_size': 10, # Conservative default (low latency for single camera)
126
+ 'batch_timeout': 0.01 # 10ms timeout (minimal delay)
127
+ })
128
+
97
129
  self.matrice_stream = MatriceStream(
98
130
  StreamType.REDIS if self.server_type == "redis" else StreamType.KAFKA,
99
131
  **self.stream_config
@@ -620,8 +652,8 @@ class CameraStreamer:
620
652
  read_time
621
653
  )
622
654
 
623
- # Log statistics periodically
624
- self.statistics.log_periodic_stats(stream_key, read_time, encoding_time, write_time)
655
+ # Log statistics periodically (time-based)
656
+ self._maybe_log_metrics(stream_key)
625
657
 
626
658
  # Maintain FPS for non-RTSP streams
627
659
  if not is_rtsp:
@@ -650,7 +682,7 @@ class CameraStreamer:
650
682
  camera_location: str,
651
683
  read_time: float
652
684
  ) -> tuple:
653
- """Process frame: encode, build message, and send."""
685
+ """Process frame: check similarity, encode if needed, build message, and send."""
654
686
  # Build metadata
655
687
  metadata = self.message_builder.build_frame_metadata(
656
688
  source, video_props, fps, quality, actual_width, actual_height,
@@ -660,8 +692,60 @@ class CameraStreamer:
660
692
  metadata["feed_type"] = "disk" if source_type == "video_file" else "camera"
661
693
  metadata["frame_count"] = 1
662
694
  metadata["stream_unit"] = "segment" if is_video_chunk else "frame"
663
-
664
- # Encode frame
695
+
696
+ # Check frame similarity BEFORE encoding (saves CPU if frame is similar)
697
+ is_similar, similarity_score = self.frame_optimizer.is_similar(frame, stream_key)
698
+ reference_frame_id = self._last_sent_frame_ids.get(stream_key)
699
+
700
+ # Get timing stats
701
+ last_read, last_write, last_process = self.statistics.get_timing(stream_key)
702
+ input_order = self.statistics.get_next_input_order(stream_key)
703
+
704
+ if is_similar and reference_frame_id:
705
+ # Frame is similar to previous - send message with empty content + cached_frame_id
706
+ encoding_time = 0.0 # No encoding needed
707
+ metadata["similarity_score"] = similarity_score
708
+
709
+ write_start = time.time()
710
+ try:
711
+ message = self.message_builder.build_message(
712
+ frame_data=b"", # EMPTY content for cached frame
713
+ stream_key=stream_key,
714
+ stream_group_key=stream_group_key,
715
+ codec="cached", # Special codec to indicate cached frame
716
+ metadata=metadata,
717
+ topic=topic,
718
+ broker_config=self.matrice_stream.config.get('bootstrap_servers', 'localhost:9092'),
719
+ input_order=input_order,
720
+ last_read_time=last_read,
721
+ last_write_time=last_write,
722
+ last_process_time=last_process,
723
+ cached_frame_id=reference_frame_id, # Reference to cached frame
724
+ )
725
+
726
+ self.matrice_stream.add_message(
727
+ topic_or_channel=topic,
728
+ message=message,
729
+ key=str(stream_key)
730
+ )
731
+ write_time = time.time() - write_start
732
+
733
+ # Record success
734
+ self._record_send_success()
735
+
736
+ # Update statistics - frame was skipped (no encoding)
737
+ self.statistics.increment_frames_skipped()
738
+ process_time = read_time + write_time
739
+ self.statistics.update_timing(stream_key, read_time, write_time, process_time, 0, encoding_time)
740
+
741
+ except Exception as e:
742
+ write_time = time.time() - write_start
743
+ self.logger.error(f"Failed to send cached frame message for {stream_key}: {e}")
744
+ self._record_send_failure()
745
+
746
+ return encoding_time, write_time
747
+
748
+ # Frame is different - encode and send full frame
665
749
  encoding_start = time.time()
666
750
  if self.video_codec in ["h265-frame", "h265-chunk"]:
667
751
  frame_data, metadata, codec = self.encoder_manager.encode_frame(
@@ -673,52 +757,59 @@ class CameraStreamer:
673
757
  # Keep codec as "h264" for downstream compatibility, but actual data is JPEG
674
758
  encode_success, jpeg_buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality])
675
759
  if encode_success:
676
- frame_data = jpeg_buffer.tobytes()
760
+ # ZERO-COPY: Use buffer directly instead of tobytes()
761
+ # jpeg_buffer is numpy array - convert via memoryview to avoid copy
762
+ # Only final bytes() call creates copy, but minimizes intermediate copies
763
+ frame_data = bytes(jpeg_buffer.data)
677
764
  codec = "h264" # Keep as h264 for downstream systems
678
765
  metadata["encoding_type"] = "jpeg" # Actual encoding format
679
766
  else:
680
767
  self.logger.error(f"JPEG encoding failed for {stream_key}, using raw fallback")
681
- frame_data = frame.tobytes()
768
+ # ZERO-COPY: Use buffer protocol instead of tobytes()
769
+ frame_data = bytes(memoryview(frame).cast('B'))
682
770
  codec = "h264"
683
771
  metadata["encoding_type"] = "raw"
684
772
  encoding_time = time.time() - encoding_start
685
-
686
- # Get timing stats
687
- last_read, last_write, last_process = self.statistics.get_timing(stream_key)
688
- input_order = self.statistics.get_next_input_order(stream_key)
689
-
690
- # Build and send message
773
+
774
+ # Build and send message (normal frame with content)
691
775
  write_start = time.time()
692
776
  try:
693
777
  message = self.message_builder.build_message(
694
778
  frame_data, stream_key, stream_group_key, codec, metadata, topic,
695
779
  self.matrice_stream.config.get('bootstrap_servers', 'localhost:9092'),
696
- input_order, last_read, last_write, last_process
780
+ input_order, last_read, last_write, last_process,
781
+ cached_frame_id=None, # Normal frame, no cache reference
697
782
  )
698
-
783
+
699
784
  self.matrice_stream.add_message(
700
785
  topic_or_channel=topic,
701
786
  message=message,
702
787
  key=str(stream_key)
703
788
  )
704
789
  write_time = time.time() - write_start
705
-
790
+
706
791
  # Record success
707
792
  self._record_send_success()
708
-
793
+
794
+ # Track this frame_id as the last sent for future cached frames
795
+ new_frame_id = message.get("frame_id")
796
+ if new_frame_id:
797
+ self._last_sent_frame_ids[stream_key] = new_frame_id
798
+ self.frame_optimizer.set_last_frame_id(stream_key, new_frame_id)
799
+
709
800
  # Update statistics
710
801
  self.statistics.increment_frames_sent()
711
802
  process_time = read_time + write_time
712
803
  frame_size = len(frame_data) if frame_data else 0
713
- self.statistics.update_timing(stream_key, read_time, write_time, process_time, frame_size)
714
-
804
+ self.statistics.update_timing(stream_key, read_time, write_time, process_time, frame_size, encoding_time)
805
+
715
806
  except Exception as e:
716
807
  write_time = time.time() - write_start
717
808
  self.logger.error(f"Failed to send message for {stream_key}: {e}")
718
-
809
+
719
810
  # Record failure (will trigger connection refresh if threshold reached)
720
811
  self._record_send_failure()
721
-
812
+
722
813
  return encoding_time, write_time
723
814
 
724
815
  # ========================================================================
@@ -746,12 +837,85 @@ class CameraStreamer:
746
837
 
747
838
  return masked
748
839
 
840
+ @staticmethod
841
+ def calculate_batch_parameters(num_cameras: int, fps: int = 10) -> Dict[str, Any]:
842
+ """Calculate optimal batch parameters based on number of cameras.
843
+
844
+ Uses conservative defaults for small camera counts to minimize latency,
845
+ then scales up for better throughput with many cameras.
846
+
847
+ Note: This should be called with per-worker camera count, not total
848
+ cameras in deployment. WorkerManager calls this for each worker.
849
+
850
+ Args:
851
+ num_cameras: Number of cameras being streamed (per worker)
852
+ fps: Target frames per second (default: 10)
853
+
854
+ Returns:
855
+ Dictionary with 'batch_size' and 'batch_timeout' parameters
856
+
857
+ Examples:
858
+ - 1-10 cameras: batch_size=10, timeout=10ms (low latency)
859
+ - 11-50 cameras: batch_size=50, timeout=20ms
860
+ - 51-200 cameras: batch_size=100, timeout=50ms
861
+ - 201-500 cameras: batch_size=100, timeout=20ms
862
+ - 500+ cameras: batch_size=50, timeout=10ms (per-worker optimized)
863
+ """
864
+ if num_cameras <= 10:
865
+ # Single/few cameras: Use conservative defaults for low latency
866
+ return {
867
+ 'batch_size': 10,
868
+ 'batch_timeout': 0.01 # 10ms
869
+ }
870
+ elif num_cameras <= 50:
871
+ # Small deployment: Fast flushes for low latency
872
+ return {
873
+ 'batch_size': 25,
874
+ 'batch_timeout': 0.01 # 10ms
875
+ }
876
+ else:
877
+ # 50+ cameras per worker: Minimize batching to reduce write latency
878
+ # With many concurrent frame arrivals, large batches cause queue backup
879
+ # Small batch + fast timeout = frames flush immediately
880
+ return {
881
+ 'batch_size': 10,
882
+ 'batch_timeout': 0.005 # 5ms - flush quickly
883
+ }
884
+
885
+ def _maybe_log_metrics(self, stream_key: str) -> None:
886
+ """Log metrics if interval has elapsed.
887
+
888
+ Args:
889
+ stream_key: Stream identifier
890
+ """
891
+ current_time = time.time()
892
+ if (current_time - self._last_metrics_log_time) >= self._metrics_log_interval:
893
+ # Log per-stream metrics
894
+ self.statistics.log_detailed_stats(stream_key)
895
+
896
+ # Log frame optimizer metrics
897
+ if self.frame_optimizer.enabled:
898
+ opt_metrics = self.frame_optimizer.get_metrics()
899
+ self.logger.info(
900
+ f"Frame Optimizer: "
901
+ f"similarity_rate={opt_metrics['similarity_rate']:.1f}%, "
902
+ f"active_streams={opt_metrics['active_streams']}, "
903
+ f"threshold={opt_metrics['config']['similarity_threshold']}"
904
+ )
905
+
906
+ self._last_metrics_log_time = current_time
907
+
908
+ # Log aggregated stats less frequently (separate timer to avoid bug)
909
+ if (current_time - self._last_aggregated_log_time) >= self._aggregated_log_interval:
910
+ self.statistics.log_aggregated_stats()
911
+ self._last_aggregated_log_time = current_time
912
+
749
913
  @staticmethod
750
914
  def _normalize_video_codec(video_codec: Optional[str], h265_mode: str) -> str:
751
915
  """Normalize codec selection."""
752
916
  if not video_codec or str(video_codec).strip() == "":
753
917
  return "h264"
754
-
918
+
755
919
  vc = str(video_codec).lower().strip()
756
920
  if vc in {"h264", "h265-frame", "h265-chunk"}:
757
921
  return vc