160 lines
5.2 KiB
Python
160 lines
5.2 KiB
Python
import cv2
|
|
import requests
|
|
import numpy as np
|
|
from PySide6.QtCore import QThread, Signal, QRect, QPoint
|
|
from PySide6.QtGui import QImage, QPainter, QPen, QColor, Qt, QFontMetrics, QFont
|
|
from io import BytesIO
|
|
|
|
|
|
class VideoThread(QThread):
|
|
frame_ready = Signal(QImage)
|
|
error_occurred = Signal(str)
|
|
|
|
def __init__(self, ip: str, camera=1):
|
|
super().__init__()
|
|
self.camera_ip = ip
|
|
self.running = False
|
|
self.session = None
|
|
self.last_good_frame = None
|
|
self.camera: int = camera
|
|
self.is_busy = False
|
|
|
|
def set_camera_ip(self, ip: str):
|
|
self.camera_ip = ip
|
|
|
|
def set_busy(self, busy: bool):
|
|
self.is_busy = busy
|
|
|
|
def run(self):
|
|
# MJPEG URL
|
|
mjpeg_url = f"http://{self.camera_ip}/axis-cgi/mjpg/video.cgi?resolution=1280x720&fps=20&compression=30&camera={self.camera}"
|
|
|
|
# Create a session for connection reuse
|
|
self.session = requests.Session()
|
|
|
|
# Set connection timeout and read timeout
|
|
timeout = (1, 3) # (connect_timeout, read_timeout)
|
|
|
|
try:
|
|
# Start streaming
|
|
response = self.session.get(mjpeg_url, stream=True, timeout=timeout)
|
|
|
|
if response.status_code != 200:
|
|
self.error_occurred.emit(
|
|
f"Could not connect to camera at {self.camera_ip}. Status: {response.status_code}"
|
|
)
|
|
return
|
|
|
|
self.running = True
|
|
|
|
# Buffer to accumulate data
|
|
buffer = b""
|
|
|
|
# MJPEG boundary detection
|
|
boundary = None
|
|
|
|
for chunk in response.iter_content(chunk_size=1024):
|
|
if not self.running or self.isInterruptionRequested():
|
|
break
|
|
|
|
if chunk:
|
|
buffer += chunk
|
|
|
|
# Find boundary on first iteration
|
|
if boundary is None:
|
|
boundary_start = buffer.find(b"--")
|
|
if boundary_start != -1:
|
|
boundary_end = buffer.find(b"\r\n", boundary_start)
|
|
if boundary_end != -1:
|
|
boundary = buffer[boundary_start:boundary_end]
|
|
|
|
# Process frames if we have a boundary
|
|
if boundary:
|
|
self._process_buffer(buffer, boundary)
|
|
# Keep only the last part of buffer that might contain partial frame
|
|
last_boundary = buffer.rfind(boundary)
|
|
if last_boundary != -1:
|
|
buffer = buffer[last_boundary:]
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
self.error_occurred.emit(f"Connection error: {str(e)}")
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Unexpected error: {str(e)}")
|
|
finally:
|
|
self.running = False
|
|
if self.session:
|
|
try:
|
|
self.session.close()
|
|
except Exception:
|
|
pass
|
|
self.session = None
|
|
|
|
def _process_buffer(self, buffer, boundary):
|
|
"""Process buffer to extract JPEG frames"""
|
|
parts = buffer.split(boundary)
|
|
|
|
for part in parts[:-1]: # Skip the last part as it might be incomplete
|
|
if not part:
|
|
continue
|
|
|
|
# Find the start of JPEG data (after headers)
|
|
jpeg_start = part.find(b"\xff\xd8") # JPEG SOI marker
|
|
if jpeg_start == -1:
|
|
continue
|
|
|
|
# Find the end of JPEG data
|
|
jpeg_end = part.find(b"\xff\xd9", jpeg_start) # JPEG EOI marker
|
|
if jpeg_end == -1:
|
|
continue
|
|
|
|
# Extract JPEG data
|
|
jpeg_data = part[jpeg_start : jpeg_end + 2]
|
|
|
|
try:
|
|
# Decode JPEG using OpenCV
|
|
nparr = np.frombuffer(jpeg_data, np.uint8)
|
|
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
|
|
if frame is not None and frame.shape[0] > 0 and frame.shape[1] > 0:
|
|
# Convert BGR to RGB
|
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
|
|
# Convert to QImage
|
|
height, width, channel = rgb_frame.shape
|
|
bytes_per_line = 3 * width
|
|
qt_image = QImage(
|
|
rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888
|
|
)
|
|
|
|
# Store as last good frame
|
|
self.last_good_frame = qt_image
|
|
self.frame_ready.emit(qt_image)
|
|
|
|
except Exception:
|
|
# If frame processing fails, use last good frame if available
|
|
if self.last_good_frame is not None:
|
|
self.frame_ready.emit(self.last_good_frame)
|
|
|
|
def stop(self):
|
|
self.running = False
|
|
self.requestInterruption()
|
|
|
|
session = self.session
|
|
if session is not None:
|
|
try:
|
|
session.close()
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
self.session = None
|
|
|
|
try:
|
|
self.quit()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.wait(5000)
|
|
except Exception:
|
|
pass
|