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)}")