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.
- matrice_streaming/__init__.py +44 -32
- matrice_streaming/streaming_gateway/camera_streamer/__init__.py +68 -1
- matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +1388 -0
- matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +966 -0
- matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +188 -24
- matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +507 -0
- matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +136 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +1048 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +192 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +470 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +1368 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +1063 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +546 -0
- matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +60 -15
- matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +1330 -0
- matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +412 -0
- matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +680 -0
- matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +111 -4
- matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +223 -27
- matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +694 -0
- matrice_streaming/streaming_gateway/debug/__init__.py +27 -2
- matrice_streaming/streaming_gateway/debug/benchmark.py +727 -0
- matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +599 -0
- matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +245 -95
- matrice_streaming/streaming_gateway/debug/debug_utils.py +29 -0
- matrice_streaming/streaming_gateway/debug/test_videoplayback.py +318 -0
- matrice_streaming/streaming_gateway/dynamic_camera_manager.py +656 -39
- matrice_streaming/streaming_gateway/metrics_reporter.py +676 -139
- matrice_streaming/streaming_gateway/streaming_action.py +71 -20
- matrice_streaming/streaming_gateway/streaming_gateway.py +1026 -78
- matrice_streaming/streaming_gateway/streaming_gateway_utils.py +175 -20
- matrice_streaming/streaming_gateway/streaming_status_listener.py +89 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/METADATA +1 -1
- matrice_streaming-0.1.65.dist-info/RECORD +56 -0
- matrice_streaming-0.1.14.dist-info/RECORD +0 -38
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/WHEEL +0 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/licenses/LICENSE.txt +0 -0
- {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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|