diff --git a/config.json b/config.json index 854b102..0d061f9 100644 --- a/config.json +++ b/config.json @@ -2,6 +2,8 @@ "poll_interval_ms": 100, "max_streams": 20, "target_fps": 2, - "reconnect_interval_sec": 5, - "max_retries": -1 + "reconnect_interval_sec": 10, + "max_retries": -1, + "rtsp_buffer_size": 3, + "rtsp_tcp_transport": true } diff --git a/core/streaming/readers.py b/core/streaming/readers.py index d675907..a48840a 100644 --- a/core/streaming/readers.py +++ b/core/streaming/readers.py @@ -38,8 +38,8 @@ class RTSPReader: # Frame processing parameters self.frame_interval = 1.0 / self.expected_fps # ~167ms for 6fps - self.error_recovery_delay = 2.0 - self.max_consecutive_errors = 10 + self.error_recovery_delay = 5.0 # Increased from 2.0 for stability + self.max_consecutive_errors = 30 # Increased from 10 to handle network jitter self.stream_timeout = 30.0 def set_frame_callback(self, callback: Callable[[str, np.ndarray], None]): @@ -107,9 +107,15 @@ class RTSPReader: consecutive_errors = 0 time.sleep(self.error_recovery_delay) else: - # Skip corrupted frame and continue - logger.debug(f"Camera {self.camera_id}: Frame read failed (error {consecutive_errors})") - time.sleep(0.1) + # Skip corrupted frame and continue with exponential backoff + if consecutive_errors <= 5: + logger.debug(f"Camera {self.camera_id}: Frame read failed (error {consecutive_errors})") + elif consecutive_errors % 10 == 0: # Log every 10th error after 5 + logger.warning(f"Camera {self.camera_id}: Continuing frame read failures (error {consecutive_errors})") + + # Exponential backoff with cap at 1 second + sleep_time = min(0.1 * (1.5 ** min(consecutive_errors, 10)), 1.0) + time.sleep(sleep_time) continue # Validate frame dimensions @@ -169,7 +175,18 @@ class RTSPReader: logger.info(f"Initializing capture for camera {self.camera_id}") - # Create capture with FFMPEG backend + # Create capture with FFMPEG backend and TCP transport for reliability + # Use TCP instead of UDP to prevent packet loss + rtsp_url_tcp = self.rtsp_url.replace('rtsp://', 'rtsp://') + if '?' in rtsp_url_tcp: + rtsp_url_tcp += '&tcp' + else: + rtsp_url_tcp += '?tcp' + + # Alternative: Set environment variable for RTSP transport + import os + os.environ['OPENCV_FFMPEG_CAPTURE_OPTIONS'] = 'rtsp_transport;tcp' + self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG) if not self.cap.isOpened(): @@ -181,8 +198,9 @@ class RTSPReader: self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.expected_height) self.cap.set(cv2.CAP_PROP_FPS, self.expected_fps) - # Set small buffer to reduce latency and avoid accumulating corrupted frames - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + # Set moderate buffer to handle network jitter while avoiding excessive latency + # Buffer of 3 frames provides resilience without major delay + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) # Set FFMPEG options for better H.264 handling self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264')) @@ -208,13 +226,23 @@ class RTSPReader: return False def _reinitialize_capture(self): - """Reinitialize capture after errors.""" + """Reinitialize capture after errors with retry logic.""" logger.info(f"Reinitializing capture for camera {self.camera_id}") if self.cap: self.cap.release() self.cap = None - time.sleep(1.0) - self._initialize_capture() + + # Longer delay before reconnection to avoid rapid reconnect loops + time.sleep(3.0) + + # Retry initialization up to 3 times + for attempt in range(3): + if self._initialize_capture(): + logger.info(f"Successfully reinitialized camera {self.camera_id} on attempt {attempt + 1}") + break + else: + logger.warning(f"Failed to reinitialize camera {self.camera_id} on attempt {attempt + 1}") + time.sleep(2.0) def _is_frame_corrupted(self, frame: np.ndarray) -> bool: """Check if frame is corrupted (all black, all white, or excessive noise)."""