safer camerathread
This commit is contained in:
@@ -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")
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user