From 1300c41172c0138ba87c279ac89a3ec15aae2ec9 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sat, 1 Nov 2025 18:23:32 +0100 Subject: [PATCH] safer camerathread --- mucapy/CameraThread.py | 646 ++++++++++++++++++++++++----------------- 1 file changed, 382 insertions(+), 264 deletions(-) diff --git a/mucapy/CameraThread.py b/mucapy/CameraThread.py index 653df85..5329fb0 100644 --- a/mucapy/CameraThread.py +++ b/mucapy/CameraThread.py @@ -1,19 +1,25 @@ import time import urllib.parse from enum import Enum +import logging +import traceback +from typing import Optional, Dict, Any import cv2 import numpy as np import requests -from PyQt5.QtCore import QThread, pyqtSignal, QMutex +from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QWaitCondition -# Optional: Try to import rtsp library for better RTSP handling try: import rtsp RTSP_LIB_AVAILABLE = True except ImportError: RTSP_LIB_AVAILABLE = False - print("rtsp library not available. Install with: pip install rtsp") + logging.info("rtsp library not available. Install with: pip install rtsp") + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) class StreamType(Enum): @@ -27,58 +33,123 @@ class StreamType(Enum): class CameraThread(QThread): - """Enhanced thread class for handling various camera connections and frame grabbing""" + # Signals frame_ready = pyqtSignal(int, np.ndarray) error_occurred = pyqtSignal(int, str) connection_status = pyqtSignal(int, bool, str) # camera_id, connected, message + stats_updated = pyqtSignal(int, dict) # camera_id, stats def __init__(self, camera_id, camera_info, parent=None): super().__init__(parent) self.camera_id = camera_id self.camera_info = camera_info self.running = False + self.paused = False self.cap = None - self.rtsp_client = None # For rtsp library client + self.rtsp_client = None self.mutex = QMutex() + self.condition = QWaitCondition() + + # Configuration with safe defaults self.frame_interval = 1.0 / 30 # Default to 30 FPS - self.reconnect_attempts = 5 + self.max_reconnect_attempts = 10 self.reconnect_delay = 2 - self.stream_type = None + self.reconnect_backoff = 1.5 # Exponential backoff factor self.read_timeout = 5.0 self.connection_timeout = 10 - self.use_rtsp_lib = RTSP_LIB_AVAILABLE # Use rtsp library if available + self.max_consecutive_failures = 15 + self.health_check_interval = 5.0 + + # State tracking + self.stream_type = None + self.use_rtsp_lib = RTSP_LIB_AVAILABLE + self.last_successful_frame = 0 + self.consecutive_failures = 0 + self.total_failures = 0 + self.total_frames = 0 + self.last_health_check = 0 + self.connection_attempts = 0 + + # Statistics + self.stats = { + 'fps': 0, + 'total_frames': 0, + 'total_failures': 0, + 'connection_attempts': 0, + 'uptime': 0, + 'start_time': 0, + 'last_frame_time': 0 + } def set_fps(self, fps): """Set the target FPS for frame capture""" - if fps > 0: - self.frame_interval = 1.0 / fps + try: + if fps > 0 and fps <= 120: # Reasonable bounds + self.frame_interval = 1.0 / fps + logger.info(f"Camera {self.camera_id}: FPS set to {fps}") + else: + logger.warning(f"Camera {self.camera_id}: Invalid FPS value {fps}") + except Exception as e: + logger.error(f"Camera {self.camera_id}: Error setting FPS: {e}") + + def safe_emit(self, signal, *args): + try: + if self.isRunning(): + signal.emit(*args) + except Exception as e: + logger.error(f"Camera {self.camera_id}: Signal emit failed: {e}") + + def update_stats(self): + try: + current_time = time.time() + if self.stats['last_frame_time'] > 0: + time_diff = current_time - self.stats['last_frame_time'] + if time_diff < 5: # Only update FPS if we have recent frames + self.stats['fps'] = 1.0 / time_diff if time_diff > 0 else 0 + + self.stats['total_frames'] = self.total_frames + self.stats['total_failures'] = self.total_failures + self.stats['connection_attempts'] = self.connection_attempts + self.stats['uptime'] = current_time - self.stats['start_time'] if self.stats['start_time'] > 0 else 0 + + self.safe_emit(self.stats_updated, self.camera_id, self.stats.copy()) + except Exception as e: + logger.debug(f"Camera {self.camera_id}: Stats update error: {e}") def detect_stream_type(self, url_or_info): - """Detect the type of stream based on URL or camera info""" - if isinstance(url_or_info, (int, str)): - url_str = str(url_or_info) + try: + if isinstance(url_or_info, (int, str)): + url_str = str(url_or_info).strip().lower() - if url_str.isdigit(): - return StreamType.LOCAL - elif url_str.startswith('rtsp://'): - return StreamType.RTSP - elif url_str.startswith('net:'): - return StreamType.NETWORK - elif ':4747' in url_str or 'droidcam' in url_str.lower(): - return StreamType.DROIDCAM - elif url_str.startswith(('http://', 'https://')): - return StreamType.HTTP_MJPEG - else: - return StreamType.IP_CAMERA + if url_str.isdigit(): + return StreamType.LOCAL + elif url_str.startswith('rtsp://'): + return StreamType.RTSP + elif url_str.startswith('net:'): + return StreamType.NETWORK + elif ':4747' in url_str or 'droidcam' in url_str: + return StreamType.DROIDCAM + elif url_str.startswith(('http://', 'https://')): + return StreamType.HTTP_MJPEG + else: + # Try to parse as IP camera + if any(x in url_str for x in ['.', ':']): + return StreamType.IP_CAMERA + return StreamType.LOCAL # Fallback - return StreamType.NETWORK + return StreamType.NETWORK + except Exception as e: + logger.error(f"Camera {self.camera_id}: Stream type detection failed: {e}") + return StreamType.IP_CAMERA # Safe fallback @staticmethod def validate_url(url): - """Validate and normalize URL format""" + """Safely validate and normalize URL format""" try: - url = url.strip() + if not url or not isinstance(url, str): + return None + url = url.strip() if not url: return None @@ -99,11 +170,11 @@ class CameraThread(QThread): return url except Exception as e: - print(f"URL validation error: {e}") + logger.error(f"URL validation error: {e}") return None def construct_camera_url(self, camera_info): - """Construct proper camera URL with authentication if needed""" + """Safely construct proper camera URL with authentication if needed""" try: if isinstance(camera_info, dict): url = camera_info.get('url', '') @@ -129,11 +200,35 @@ class CameraThread(QThread): return url except Exception as e: - print(f"Error constructing camera URL: {e}") + logger.error(f"Camera {self.camera_id}: Error constructing camera URL: {e}") return None + def safe_capture_release(self): + """Safely release OpenCV capture""" + try: + if self.cap is not None: + self.cap.release() + self.cap = None + logger.debug(f"Camera {self.camera_id}: Capture released") + except Exception as e: + logger.debug(f"Camera {self.camera_id}: Error releasing capture: {e}") + finally: + self.cap = None + + def safe_rtsp_close(self): + """Safely close RTSP client""" + try: + if self.rtsp_client is not None: + self.rtsp_client.close() + self.rtsp_client = None + logger.debug(f"Camera {self.camera_id}: RTSP client closed") + except Exception as e: + logger.debug(f"Camera {self.camera_id}: Error closing RTSP client: {e}") + finally: + self.rtsp_client = None + def configure_capture(self, cap, stream_type): - """Configure VideoCapture object based on stream type""" + """Safely configure VideoCapture object based on stream type""" try: # Common settings cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) @@ -151,63 +246,69 @@ class CameraThread(QThread): cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000) + logger.debug(f"Camera {self.camera_id}: Capture configured for {stream_type.value}") except Exception as e: - print(f"Warning: Could not configure capture settings: {e}") + logger.warning(f"Camera {self.camera_id}: Could not configure capture settings: {e}") def test_network_endpoint(self, url, timeout=3): - """Test if a network endpoint is accessible""" + """Safely test if a network endpoint is accessible""" try: response = requests.head(url, timeout=timeout, allow_redirects=True) - return response.status_code in [200, 401] + accessible = response.status_code in [200, 401, 403] # 401/403 means it's there but needs auth + logger.debug(f"Camera {self.camera_id}: Network test for {url}: {accessible}") + return accessible except requests.exceptions.RequestException: try: response = requests.get(url, timeout=timeout, stream=True) response.close() - return response.status_code in [200, 401] - except Exception: + accessible = response.status_code in [200, 401, 403] + logger.debug(f"Camera {self.camera_id}: Network test (GET) for {url}: {accessible}") + return accessible + except Exception as e: + logger.debug(f"Camera {self.camera_id}: Network test failed for {url}: {e}") return False + except Exception as e: + logger.debug(f"Camera {self.camera_id}: Network test error for {url}: {e}") + return False def connect_rtsp_with_library(self, url): - """Connect to RTSP stream using the rtsp library""" + """Safely connect to RTSP stream using the rtsp library""" + if not self.use_rtsp_lib: + return False + try: - print(f" Attempting connection with rtsp library...") + logger.info(f"Camera {self.camera_id}: Attempting RTSP library connection...") self.rtsp_client = rtsp.Client(rtsp_server_uri=url, verbose=False) # Test if connection works if self.rtsp_client.isOpened(): - # Try to read a frame - frame = self.rtsp_client.read() - if frame is not None: - print(f" Successfully connected with rtsp library") - return True - else: - print(f" Failed to read frame with rtsp library") - self.rtsp_client.close() - self.rtsp_client = None - else: - print(f" rtsp library failed to open stream") - self.rtsp_client = None + # Try to read a frame with timeout + start_time = time.time() + while time.time() - start_time < self.read_timeout: + frame = self.rtsp_client.read() + if frame is not None: + logger.info(f"Camera {self.camera_id}: Successfully connected with rtsp library") + return True + time.sleep(0.1) + + logger.warning(f"Camera {self.camera_id}: Failed to connect with rtsp library") + self.safe_rtsp_close() + return False except Exception as e: - print(f" rtsp library error: {e}") - if self.rtsp_client: - try: - self.rtsp_client.close() - except Exception: - pass - self.rtsp_client = None - - return False + logger.warning(f"Camera {self.camera_id}: RTSP library error: {e}") + self.safe_rtsp_close() + return False def connect_rtsp_with_opencv(self, url): - """Connect to RTSP stream using OpenCV with different transport protocols""" + """Safely connect to RTSP stream using OpenCV with different transport protocols""" import os transports = ['tcp', 'udp', 'http'] for transport in transports: try: - print(f" Trying RTSP with {transport.upper()} transport...") + logger.info(f"Camera {self.camera_id}: Trying RTSP with {transport.upper()} transport...") # Set FFMPEG options os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( @@ -221,64 +322,55 @@ class CameraThread(QThread): self.configure_capture(self.cap, StreamType.RTSP) if not self.cap.isOpened(): - print(f" Failed to open with {transport}") - self.cap.release() + logger.debug(f"Camera {self.camera_id}: Failed to open with {transport}") + self.safe_capture_release() continue - # Try to read a frame + # Try to read a frame with timeout start_time = time.time() - while time.time() - start_time < 5: + while time.time() - start_time < self.read_timeout: ret, frame = self.cap.read() if ret and frame is not None and frame.size > 0: - print(f" Successfully connected with {transport.upper()}") + logger.info(f"Camera {self.camera_id}: Successfully connected with {transport.upper()}") return True time.sleep(0.1) - print(f" Failed to read frame with {transport}") - self.cap.release() + logger.debug(f"Camera {self.camera_id}: Failed to read frame with {transport}") + self.safe_capture_release() except Exception as e: - print(f" Error with {transport}: {e}") - if self.cap: - self.cap.release() - self.cap = None + logger.debug(f"Camera {self.camera_id}: Error with {transport}: {e}") + self.safe_capture_release() return False def connect_to_camera(self): - """Attempt to connect to the camera with enhanced retry logic""" - for attempt in range(self.reconnect_attempts): + """Safely attempt to connect to the camera with enhanced retry logic""" + self.connection_attempts += 1 + + for attempt in range(self.max_reconnect_attempts): try: # Clean up existing connections - if self.cap is not None: - try: - self.cap.release() - except Exception: - pass - self.cap = None - - if self.rtsp_client is not None: - try: - self.rtsp_client.close() - except Exception: - pass - self.rtsp_client = None + self.safe_capture_release() + self.safe_rtsp_close() # Determine camera source if isinstance(self.camera_info, str) and self.camera_info.startswith('net:'): name = self.camera_info[4:] detector = self.parent().detector if self.parent() else None - if not detector or name not in detector.network_cameras: - self.connection_status.emit(self.camera_id, False, f"Network camera {name} not found") - return False + if not detector or name not in getattr(detector, 'network_cameras', {}): + self.safe_emit(self.connection_status, self.camera_id, False, f"Network camera {name} not found") + time.sleep(self.reconnect_delay * (self.reconnect_backoff ** attempt)) + continue camera_info = detector.network_cameras[name] url = self.construct_camera_url(camera_info) if not url: - self.connection_status.emit(self.camera_id, False, f"Invalid URL for {name}") - return False + self.safe_emit(self.connection_status, self.camera_id, False, f"Invalid URL for {name}") + time.sleep(self.reconnect_delay * (self.reconnect_backoff ** attempt)) + continue self.stream_type = self.detect_stream_type(url) camera_source = url @@ -287,8 +379,9 @@ class CameraThread(QThread): if isinstance(self.camera_info, dict): url = self.construct_camera_url(self.camera_info) if not url: - self.connection_status.emit(self.camera_id, False, "Invalid camera URL") - return False + self.safe_emit(self.connection_status, self.camera_id, False, "Invalid camera URL") + time.sleep(self.reconnect_delay * (self.reconnect_backoff ** attempt)) + continue camera_source = url self.stream_type = self.detect_stream_type(url) else: @@ -298,227 +391,252 @@ class CameraThread(QThread): if self.stream_type != StreamType.LOCAL: camera_source = self.validate_url(str(camera_source)) if not camera_source: - self.connection_status.emit(self.camera_id, False, "Invalid camera source") - return False + self.safe_emit(self.connection_status, self.camera_id, False, "Invalid camera source") + time.sleep(self.reconnect_delay * (self.reconnect_backoff ** attempt)) + continue - print(f"Attempt {attempt + 1}/{self.reconnect_attempts}: Connecting to {self.stream_type.value} camera...") + logger.info(f"Camera {self.camera_id}: Attempt {attempt + 1}/{self.max_reconnect_attempts} connecting to {self.stream_type.value}...") # Test network endpoint for HTTP streams if self.stream_type in [StreamType.HTTP_MJPEG, StreamType.DROIDCAM, StreamType.IP_CAMERA]: if not self.test_network_endpoint(camera_source): - print(f"Network endpoint not accessible") - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - continue - self.connection_status.emit(self.camera_id, False, "Network endpoint not accessible") - return False + logger.warning(f"Camera {self.camera_id}: Network endpoint not accessible") + time.sleep(self.reconnect_delay * (self.reconnect_backoff ** attempt)) + continue # Connect based on stream type + success = False + if self.stream_type == StreamType.LOCAL: - self.cap = cv2.VideoCapture(int(camera_source)) - self.configure_capture(self.cap, self.stream_type) + try: + self.cap = cv2.VideoCapture(int(camera_source)) + self.configure_capture(self.cap, self.stream_type) - if not self.cap.isOpened(): - print("Failed to open local camera") - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - continue - return False - - # Test frame reading - ret, frame = self.cap.read() - if not ret or frame is None: - print("Failed to read from local camera") - self.cap.release() - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - continue - return False + if self.cap.isOpened(): + # Test frame reading + ret, frame = self.cap.read() + if ret and frame is not None: + success = True + except Exception as e: + logger.warning(f"Camera {self.camera_id}: Local camera error: {e}") elif self.stream_type == StreamType.RTSP: # Try rtsp library first if available if self.use_rtsp_lib and self.connect_rtsp_with_library(camera_source): - self.connection_status.emit(self.camera_id, True, "Connected (rtsp lib)") - return True - - # Fall back to OpenCV with different transports - if self.connect_rtsp_with_opencv(camera_source): - self.connection_status.emit(self.camera_id, True, "Connected (opencv)") - return True - - print("All RTSP connection methods failed") - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - continue - return False + success = True + elif self.connect_rtsp_with_opencv(camera_source): + success = True else: # HTTP MJPEG, DroidCam, IP Camera - self.cap = cv2.VideoCapture(camera_source, cv2.CAP_FFMPEG) - self.configure_capture(self.cap, self.stream_type) + try: + self.cap = cv2.VideoCapture(camera_source, cv2.CAP_FFMPEG) + self.configure_capture(self.cap, self.stream_type) - if not self.cap.isOpened(): - print("Failed to open stream") - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - continue - return False + if self.cap.isOpened(): + # Test frame reading with timeout + start_time = time.time() + ret, frame = False, None + while time.time() - start_time < self.read_timeout: + ret, frame = self.cap.read() + if ret and frame is not None and frame.size > 0: + success = True + break + time.sleep(0.1) + except Exception as e: + logger.warning(f"Camera {self.camera_id}: Network camera error: {e}") - # Test frame reading - start_time = time.time() - ret, frame = False, None - while time.time() - start_time < self.read_timeout: - ret, frame = self.cap.read() - if ret and frame is not None and frame.size > 0: - break - time.sleep(0.1) + if success: + logger.info(f"Camera {self.camera_id}: Successfully connected") + self.safe_emit(self.connection_status, self.camera_id, True, "Connected") + self.consecutive_failures = 0 + return True + else: + logger.warning(f"Camera {self.camera_id}: Connection attempt {attempt + 1} failed") + self.safe_capture_release() + self.safe_rtsp_close() - if not ret or frame is None or frame.size == 0: - print("Failed to read frames") - self.cap.release() - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - continue - return False - - print(f"Successfully connected to camera") - self.connection_status.emit(self.camera_id, True, "Connected") - return True + if attempt < self.max_reconnect_attempts - 1: + delay = self.reconnect_delay * (self.reconnect_backoff ** attempt) + logger.info(f"Camera {self.camera_id}: Retrying in {delay:.1f}s...") + time.sleep(delay) except Exception as e: - print(f"Connection attempt {attempt + 1} failed: {str(e)}") + logger.error(f"Camera {self.camera_id}: Connection attempt {attempt + 1} error: {e}") + self.safe_capture_release() + self.safe_rtsp_close() - if self.cap: - try: - self.cap.release() - except Exception: - pass - self.cap = None - - if self.rtsp_client: - try: - self.rtsp_client.close() - except Exception: - pass - self.rtsp_client = None - - if attempt < self.reconnect_attempts - 1: - time.sleep(self.reconnect_delay) - else: - self.connection_status.emit(self.camera_id, False, str(e)) - self.error_occurred.emit(self.camera_id, str(e)) - return False + if attempt < self.max_reconnect_attempts - 1: + time.sleep(self.reconnect_delay * (self.reconnect_backoff ** attempt)) + logger.error(f"Camera {self.camera_id}: All connection attempts failed") + self.safe_emit(self.connection_status, self.camera_id, False, "Connection failed") + self.safe_emit(self.error_occurred, self.camera_id, "Failed to connect after multiple attempts") return False def run(self): - """Main thread loop with enhanced error handling""" + self.stats['start_time'] = time.time() + try: + logger.info(f"Camera {self.camera_id}: Thread starting") + if not self.connect_to_camera(): - self.error_occurred.emit(self.camera_id, "Failed to connect after multiple attempts") + logger.error(f"Camera {self.camera_id}: Initial connection failed") return self.running = True - last_frame_time = time.time() - consecutive_failures = 0 - last_reconnect_time = time.time() + last_frame_time = 0 + self.last_health_check = time.time() while self.running: - self.mutex.lock() - should_continue = self.running - self.mutex.unlock() - - if not should_continue: - break - - # Frame rate limiting - current_time = time.time() - if current_time - last_frame_time < self.frame_interval: - time.sleep(0.001) - continue - - # Read frame based on connection type try: - if self.rtsp_client: - # Using rtsp library - frame = self.rtsp_client.read() - ret = frame is not None - if ret: - # Convert PIL Image to numpy array - frame = np.array(frame) - # Convert RGB to BGR for OpenCV compatibility - if len(frame.shape) == 3 and frame.shape[2] == 3: - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - else: - # Using OpenCV - ret, frame = self.cap.read() + # Check if paused + if self.paused: + time.sleep(0.1) + continue + + # Frame rate limiting + current_time = time.time() + if current_time - last_frame_time < self.frame_interval: + time.sleep(0.001) + continue + + # Health check + if current_time - self.last_health_check > self.health_check_interval: + if self.consecutive_failures > self.max_consecutive_failures / 2: + logger.warning(f"Camera {self.camera_id}: Health check failed, reconnecting...") + if not self.connect_to_camera(): + break + self.last_health_check = current_time + + # Read frame based on connection type + frame = None + ret = False + + try: + if self.rtsp_client and self.rtsp_client.isOpened(): + frame = self.rtsp_client.read() + ret = frame is not None + if ret: + # Convert PIL Image to numpy array if needed + if hasattr(frame, 'size'): # Likely PIL Image + frame = np.array(frame) + if len(frame.shape) == 3 and frame.shape[2] == 3: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + elif self.cap and self.cap.isOpened(): + ret, frame = self.cap.read() + else: + ret = False + except Exception as e: + logger.debug(f"Camera {self.camera_id}: Frame read error: {e}") + ret = False if ret and frame is not None and frame.size > 0: - consecutive_failures = 0 - self.frame_ready.emit(self.camera_id, frame) - last_frame_time = current_time + # Validate frame + if (isinstance(frame, np.ndarray) and + len(frame.shape) in [2, 3] and + frame.shape[0] > 0 and frame.shape[1] > 0): + + self.consecutive_failures = 0 + self.total_frames += 1 + self.stats['last_frame_time'] = current_time + last_frame_time = current_time + + self.safe_emit(self.frame_ready, self.camera_id, frame) + self.update_stats() + else: + self.handle_frame_failure() else: - consecutive_failures += 1 + self.handle_frame_failure() - if consecutive_failures >= 10: - if current_time - last_reconnect_time > 5: - print("Multiple failures, attempting reconnection...") - self.connection_status.emit(self.camera_id, False, "Reconnecting...") - - if self.cap: - self.cap.release() - if self.rtsp_client: - self.rtsp_client.close() - - if self.connect_to_camera(): - consecutive_failures = 0 - last_reconnect_time = current_time - else: - self.error_occurred.emit(self.camera_id, "Reconnection failed") - break - else: - consecutive_failures = 0 - - time.sleep(0.1) + # Brief sleep to prevent CPU overload + time.sleep(0.001) except Exception as e: - print(f"Error reading frame: {e}") - consecutive_failures += 1 - time.sleep(0.1) + logger.error(f"Camera {self.camera_id}: Main loop error: {e}") + self.handle_frame_failure() + time.sleep(0.1) # Longer sleep on error except Exception as e: - self.error_occurred.emit(self.camera_id, f"Thread error: {str(e)}") - + logger.critical(f"Camera {self.camera_id}: Critical thread error: {e}") + self.safe_emit(self.error_occurred, self.camera_id, f"Thread crash: {str(e)}") finally: + logger.info(f"Camera {self.camera_id}: Thread stopping") self.cleanup() + def handle_frame_failure(self): + """Handle frame reading failures with reconnection logic""" + self.consecutive_failures += 1 + self.total_failures += 1 + + if self.consecutive_failures >= self.max_consecutive_failures: + logger.warning(f"Camera {self.camera_id}: Too many failures, attempting reconnection...") + self.safe_emit(self.connection_status, self.camera_id, False, "Reconnecting...") + + if not self.connect_to_camera(): + logger.error(f"Camera {self.camera_id}: Reconnection failed, stopping thread") + self.running = False + else: + self.consecutive_failures = 0 + def stop(self): - """Stop the thread safely""" + """Safely stop the thread""" + logger.info(f"Camera {self.camera_id}: Stopping thread...") + self.mutex.lock() self.running = False self.mutex.unlock() - if not self.wait(5000): - print(f"Warning: Camera thread {self.camera_id} did not stop gracefully") - self.terminate() + # Wake up thread if it's waiting + self.condition.wakeAll() + + if not self.wait(3000): # 3 second timeout + logger.warning(f"Camera {self.camera_id}: Thread did not stop gracefully, terminating...") + try: + self.terminate() + if not self.wait(1000): + logger.error(f"Camera {self.camera_id}: Thread termination failed") + except Exception as e: + logger.error(f"Camera {self.camera_id}: Error during termination: {e}") + else: + logger.info(f"Camera {self.camera_id}: Thread stopped gracefully") + + def pause(self): + """Pause frame capture""" + self.paused = True + logger.info(f"Camera {self.camera_id}: Paused") + + def resume(self): + """Resume frame capture""" + self.paused = False + logger.info(f"Camera {self.camera_id}: Resumed") def cleanup(self): - """Clean up camera resources""" - print(f"Cleaning up camera {self.camera_id}") - try: - if self.cap: - self.cap.release() - self.cap = None - except Exception as e: - print(f"Error during cap cleanup: {e}") + """Comprehensive cleanup of all resources""" + logger.info(f"Camera {self.camera_id}: Cleaning up resources...") try: - if self.rtsp_client: - self.rtsp_client.close() - self.rtsp_client = None - except Exception as e: - print(f"Error during rtsp client cleanup: {e}") - - finally: self.running = False - self.connection_status.emit(self.camera_id, False, "Disconnected") \ No newline at end of file + self.safe_capture_release() + self.safe_rtsp_close() + + self.safe_emit(self.connection_status, self.camera_id, False, "Disconnected") + self.update_stats() + + logger.info(f"Camera {self.camera_id}: Cleanup completed") + except Exception as e: + logger.error(f"Camera {self.camera_id}: Cleanup error: {e}") + + def get_status(self) -> Dict[str, Any]: + """Get current camera status""" + return { + 'running': self.running, + 'paused': self.paused, + 'connected': (self.cap is not None and self.cap.isOpened()) or + (self.rtsp_client is not None and self.rtsp_client.isOpened()), + 'stream_type': self.stream_type.value if self.stream_type else 'unknown', + 'consecutive_failures': self.consecutive_failures, + 'total_frames': self.total_frames, + 'total_failures': self.total_failures, + 'stats': self.stats.copy() + } \ No newline at end of file