Files
AareDAQ/src/aare/gui/threads/axis_video_thread.py
T
perl_d 18da2fd0b4
Build and Publish / test (pull_request) Failing after 1m18s
Build and Publish / build (pull_request) Skipped
Build and Publish / Build and Deploy Docs (pull_request) Skipped
style: format with ruff
2026-07-06 11:52:21 +02:00

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