Files
duck-stain-ocr/app/video_stream/worker.py

119 lines
4.2 KiB
Python

import cv2
import numpy as np
import time
from PySide6.QtCore import QObject, Signal, Slot, QTimer
from logging import getLogger
logger = getLogger(__name__)
class VideoStreamWorker(QObject):
"""
Worker class responsible for reading frames from OpenCV VideoCapture.
Runs in a separate QThread.
"""
frame_ready = Signal(np.ndarray)
frame_time = Signal(float, float) # (start_time, end_time)
error_occurred = Signal(str)
started = Signal(float, tuple) # (fps, (width, height))
stopped = Signal()
source_changed = Signal(object) # Nowy sygnał dla zmiany źródła
def __init__(self):
super().__init__()
self._cap = None
self._source = 0 # Default to first camera
self._is_running = False
self._timer = QTimer()
self._timer.timeout.connect(self._process_frame)
self.source_changed.connect(self._on_source_changed) # Łączymy sygnał ze slotem
@Slot(object)
def set_source(self, source):
"""
Sets the video source asynchronously.
source: int (camera index) or str (file path).
"""
self.source_changed.emit(source) # Emitujemy sygnał zamiast synchronicznej zmiany
@Slot(object)
def _on_source_changed(self, source):
"""
Slot obsługujący zmianę źródła w wątku roboczym.
"""
logger.debug(f"Changing video source to: {source}")
was_running = self._is_running
if was_running:
self.stop() # Zatrzymujemy synchronicznie w tym samym wątku
self._source = source
if was_running:
self.start() # Uruchamiamy ponownie w tym samym wątku
@Slot()
def start(self):
if self._is_running:
logger.warning("VideoStreamWorker is already running.")
return
if self._source is None:
logger.error("No video source specified.")
self.error_occurred.emit("No source specified.")
return
self._cap = cv2.VideoCapture(self._source)
if not self._cap.isOpened():
logger.error(f"Could not open source: {self._source}")
self.error_occurred.emit(f"Could not open source: {self._source}")
return
self._is_running = True
# Using a timer for consistent frame rate and to play nice with the event loop
# For files, we might want to calculate the interval from FPS.
fps = self._cap.get(cv2.CAP_PROP_FPS)
interval = int(1000 / fps) if fps > 0 else 30
video_res = (int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
logger.debug(f"Video opened: {self._source} (fps: {fps}, width: {video_res[0]}, height: {video_res[1]})")
self._timer.start(interval)
self.started.emit(fps, video_res)
@Slot()
def stop(self):
logger.debug("Stopping VideoStreamWorker.")
self._is_running = False
self._timer.stop()
if self._cap:
self._cap.release()
self._cap = None
self.stopped.emit()
def _process_frame(self):
if not self._is_running:
logger.debug("VideoStreamWorker is not running. Skipping frame processing.")
return
if self._cap is None or not self._cap.isOpened():
logger.error("VideoCapture is not initialized or opened.")
self.error_occurred.emit("VideoCapture not initialized.")
self.stop()
return
try:
read_start_time = time.perf_counter()
ret, frame = self._cap.read()
if ret:
self.frame_ready.emit(frame)
else:
# End of video file or lost connection
if isinstance(self._source, str):
self.stop()
else:
logger.error("Lost connection to camera.")
self.error_occurred.emit("Lost connection to camera.")
self.stop()
read_end_time = time.perf_counter()
self.frame_time.emit(read_start_time, read_end_time)
except Exception as e:
logger.error(f"Error reading frame: {str(e)}")
self.error_occurred.emit(f"Error reading frame: {str(e)}")