import cv2 import threading import time from typing import Optional, Callable class VideoFileSource: """ A class to handle video file input using cv2.VideoCapture. It reads frames from a video file and can send them to a pipeline. """ def __init__(self, file_path: str, data_callback: Optional[Callable[[object], None]] = None, frame_callback: Optional[Callable[[object], None]] = None, loop: bool = False): """ Initializes the VideoFileSource. Args: file_path (str): The path to the video file. data_callback (Optional[Callable[[object], None]]): A callback function to send data to the pipeline. frame_callback (Optional[Callable[[object], None]]): A callback function for raw frame updates. loop (bool): Whether to loop the video when it ends. """ self.file_path = file_path self.data_callback = data_callback self.frame_callback = frame_callback self.loop = loop self.cap = None self.running = False self.thread = None self._stop_event = threading.Event() self.fps = 0 def initialize(self) -> bool: """ Initializes the video capture from the file. Returns: bool: True if initialization is successful, False otherwise. """ print(f"Initializing video source from {self.file_path}...") self.cap = cv2.VideoCapture(self.file_path) if not self.cap.isOpened(): print(f"Error: Could not open video file {self.file_path}.") return False self.fps = self.cap.get(cv2.CAP_PROP_FPS) if self.fps == 0: print("Warning: Could not determine video FPS. Defaulting to 30.") self.fps = 30 print(f"Video source initialized successfully. FPS: {self.fps}") return True def start(self): """ Starts the frame reading thread. """ if self.running: print("Video source is already running.") return if not self.cap or not self.cap.isOpened(): if not self.initialize(): return self.running = True self._stop_event.clear() self.thread = threading.Thread(target=self._capture_loop, daemon=True) self.thread.start() print("Video capture thread started.") def stop(self): """ Stops the frame reading thread. """ self.running = False if self.thread and self.thread.is_alive(): self._stop_event.set() self.thread.join(timeout=2) if self.cap and self.cap.isOpened(): self.cap.release() self.cap = None print("Video source stopped.") def _capture_loop(self): """ The main loop for reading frames from the video file. """ while self.running and not self._stop_event.is_set(): ret, frame = self.cap.read() if not ret: if self.loop: print("Video ended, looping...") self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue else: print("Video ended.") self.running = False break if self.data_callback: try: self.data_callback(frame) except Exception as e: print(f"Error in data_callback: {e}") if self.frame_callback: try: self.frame_callback(frame) except Exception as e: print(f"Error in frame_callback: {e}") # Control frame rate time.sleep(1.0 / self.fps) def set_data_callback(self, callback: Callable[[object], None]): """ Sets the data callback function. """ self.data_callback = callback def get_frame(self) -> Optional[object]: """ Gets a single frame from the video. Not recommended for continuous capture. """ if not self.cap or not self.cap.isOpened(): if not self.initialize(): return None ret, frame = self.cap.read() if not ret: return None return frame