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, int) # (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 _run = Signal() # Sygnał do uruchomienia pętli wątku 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 self._run.connect(self.run) # Łą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(600 / fps) if fps > 0 else 20 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._run.emit() # Uruchamiamy pętlę wątku za pomocą sygnału 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 run(self): # while self._is_running: # self._process_frame() # time.sleep(0.028) # Krótka przerwa, aby nie zablokować CPU def run(self): """Główna pętla wątku roboczego.""" if self._source is None: logger.warning("No video source provided") return self.cap = cv2.VideoCapture(self._source) if not self.cap.isOpened(): logger.error(f"Failed to open video source: {self._source}") return self.fps = self.cap.get(cv2.CAP_PROP_FPS) if self.fps <= 0: self.fps = 30.0 self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) logger.debug(f"Video opened: {self._source} (fps: {self.fps}, size: {self.width}x{self.height})") frame_interval = 1.0 / (self.fps + 1) self.running = True frame_emit_time = time.perf_counter() read_start_time = 0.0 read_end_time = 0.0 frames_dropped = 0 while self.running: next_frame_time = frame_emit_time + frame_interval ret, frame = self.cap.read() read_end_time = time.perf_counter() self.frame_time.emit(read_start_time, read_end_time, frames_dropped) if not ret: logger.debug("End of video stream or read error") break current_time = time.perf_counter() # self.metrics.last_cap_time = current_time - frame_emit_time if current_time < next_frame_time: sleep_time = next_frame_time - current_time time.sleep(sleep_time) else: frames_dropped += 1 logger.debug(f"Frame drops counted: {frames_dropped}") frame_emit_time = time.perf_counter() read_start_time = time.perf_counter() self.frame_ready.emit(frame) 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)}")